141 lines
5.3 KiB
TypeScript
Executable File
141 lines
5.3 KiB
TypeScript
Executable File
import { z } from 'zod';
|
|
|
|
// ─── Enums matching DB constraints ─────────────────────────────────────
|
|
|
|
const patientStatusEnum = z.enum(["prospecto", "activo", "inactivo"]);
|
|
const slotStatusEnum = z.enum(["available", "confirmed", "cancelled", "completed", "reschedule", "blocked"]);
|
|
const blockTypeEnum = z.enum(["surgery", "admin", "vacation", "event", "appointment"]);
|
|
const messageDirectionEnum = z.enum(["in", "out"]);
|
|
const messageTypeEnum = z.enum(["text", "audio", "image", "document"]);
|
|
const conversationStatusEnum = z.enum(["active", "pending_human", "resolved"]);
|
|
const channelEnum = z.enum(["whatsapp", "messenger", "instagram", "tiktok", "web", "manual"]);
|
|
const templateTypeEnum = z.enum(["confirmation", "reminder", "reschedule", "post_op", "notification", "news", "surgeon_instructions"]);
|
|
const notificationTypeEnum = z.enum([
|
|
"new_appointment",
|
|
"appointment_cancelled",
|
|
"human_handoff",
|
|
"new_patient",
|
|
"appointment_reminder_2h",
|
|
"daily_summary",
|
|
"appointment_rescheduled",
|
|
"appointment_reminder_next_day",
|
|
]);
|
|
|
|
// ─── Domain Schemas ─────────────────────────────────────────────────────
|
|
|
|
const phoneRegex = /^[+]?[\d\s\-().]{7,30}$/;
|
|
|
|
export const PatientSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
name: z.string().min(2, "Name must be at least 2 characters").max(100, "Name too long").trim(),
|
|
phone: z.string().regex(phoneRegex, "Invalid phone format").max(30).optional().nullable(),
|
|
email: z.string().email("Invalid email").max(255).optional().nullable(),
|
|
status: patientStatusEnum.default("prospecto"),
|
|
});
|
|
|
|
export const PatientProfileSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
patient_id: z.string().uuid(),
|
|
interested_procedures: z.array(z.string().max(100)).default([]),
|
|
notes: z.string().max(5000).optional().nullable(),
|
|
last_interaction: z.string().datetime().optional().nullable(),
|
|
});
|
|
|
|
export const ProcedureSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
name: z.string().min(2, "Procedure name is required").max(120),
|
|
description: z.string().max(2000).optional().nullable(),
|
|
duration_min: z.number().int().positive("Duration is required"),
|
|
preparation_notes: z.string().max(2000).optional().nullable(),
|
|
is_active: z.boolean().default(true),
|
|
});
|
|
|
|
export const SlotSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
start_at: z.string().datetime(),
|
|
end_at: z.string().datetime(),
|
|
block_type: blockTypeEnum,
|
|
patient_id: z.string().uuid().optional().nullable(),
|
|
procedure_id: z.string().uuid().optional().nullable(),
|
|
status: slotStatusEnum.default("available"),
|
|
channel: channelEnum.optional().nullable(),
|
|
notes: z.string().max(2000).optional().nullable(),
|
|
});
|
|
|
|
export const MessageSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
patient_id: z.string().uuid(),
|
|
conversation_id: z.string().uuid().optional().nullable(),
|
|
channel: channelEnum,
|
|
direction: messageDirectionEnum,
|
|
type: messageTypeEnum,
|
|
content: z.string().max(4000).optional().nullable(),
|
|
audio_url: z.string().url().optional().nullable(),
|
|
transcript: z.string().max(4000).optional().nullable(),
|
|
created_at: z.string().datetime().optional(),
|
|
});
|
|
|
|
export const ConversationSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
patient_id: z.string().uuid(),
|
|
channel: channelEnum,
|
|
status: conversationStatusEnum.default("active"),
|
|
ai_context: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
last_message_at: z.string().datetime().optional(),
|
|
});
|
|
|
|
export const StaffSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
name: z.string().min(2).max(100).trim(),
|
|
role: z.enum(["admin", "staff"]),
|
|
email: z.string().trim().email().max(255).toLowerCase(),
|
|
is_active: z.boolean().default(true),
|
|
});
|
|
|
|
export const StaffNotificationSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
staff_id: z.string().uuid(),
|
|
type: notificationTypeEnum,
|
|
payload: z.record(z.string(), z.unknown()).default({}),
|
|
is_read: z.boolean().default(false),
|
|
created_at: z.string().datetime().optional(),
|
|
});
|
|
|
|
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
|
|
// WorkingHours: day-keyed object matching DB structure { "mon": ["08:00","18:00"], ... }
|
|
export const WorkingHoursSchema = z.record(
|
|
z.string(),
|
|
z.tuple([z.string().regex(timeRegex), z.string().regex(timeRegex)]).nullable()
|
|
);
|
|
|
|
export const FaqItemSchema = z.object({
|
|
q: z.string().min(1).max(500),
|
|
a: z.string().min(1).max(2000),
|
|
});
|
|
|
|
export const AgentConfigSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
system_prompt: z.string().min(1).max(8000),
|
|
working_hours: WorkingHoursSchema,
|
|
faq: z.array(FaqItemSchema).default([]),
|
|
enabled_channels: z.array(channelEnum).default(["whatsapp", "web"]),
|
|
updated_at: z.string().datetime().optional(),
|
|
});
|
|
|
|
export const MessageTemplateSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
name: z.string().min(1).max(120),
|
|
type: templateTypeEnum,
|
|
content: z.string().min(1).max(4000),
|
|
channel: channelEnum.optional().nullable(),
|
|
is_active: z.boolean().default(true),
|
|
});
|
|
|
|
export const ChatStatusSchema = z.object({
|
|
id: z.string().uuid().optional(),
|
|
patient_id: z.string().uuid(),
|
|
ia_activa: z.boolean().default(true),
|
|
last_updated: z.string().datetime().optional().nullable(),
|
|
});
|