253 lines
11 KiB
TypeScript
253 lines
11 KiB
TypeScript
import {
|
|
int,
|
|
mysqlEnum,
|
|
mysqlTable,
|
|
text,
|
|
timestamp,
|
|
varchar,
|
|
boolean,
|
|
json,
|
|
index,
|
|
uniqueIndex,
|
|
} from "drizzle-orm/mysql-core";
|
|
|
|
// ─── Users (médecins) ────────────────────────────────────────────────────────
|
|
export const users = mysqlTable(
|
|
"users",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
email: varchar("email", { length: 320 }).notNull(),
|
|
passwordHash: varchar("passwordHash", { length: 255 }).notNull(),
|
|
name: text("name"),
|
|
openId: varchar("openId", { length: 64 }),
|
|
loginMethod: varchar("loginMethod", { length: 64 }).default("password").notNull(),
|
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
|
resetToken: varchar("resetToken", { length: 255 }),
|
|
resetTokenExpiry: timestamp("resetTokenExpiry"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
emailIdx: uniqueIndex("users_email_idx").on(table.email),
|
|
openIdIdx: index("users_openId_idx").on(table.openId),
|
|
})
|
|
);
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type InsertUser = typeof users.$inferInsert;
|
|
|
|
// ─── Subscriptions ───────────────────────────────────────────────────────────
|
|
export const subscriptions = mysqlTable(
|
|
"subscriptions",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
stripeCustomerId: varchar("stripeCustomerId", { length: 128 }),
|
|
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 128 }),
|
|
stripePriceId: varchar("stripePriceId", { length: 128 }),
|
|
plan: mysqlEnum("plan", ["trial", "basic", "pro"]).default("trial").notNull(),
|
|
status: mysqlEnum("status", ["trialing", "active", "past_due", "canceled", "expired"])
|
|
.default("trialing")
|
|
.notNull(),
|
|
trialStartedAt: timestamp("trialStartedAt").defaultNow().notNull(),
|
|
trialEndsAt: timestamp("trialEndsAt").notNull(),
|
|
currentPeriodStart: timestamp("currentPeriodStart"),
|
|
currentPeriodEnd: timestamp("currentPeriodEnd"),
|
|
canceledAt: timestamp("canceledAt"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdx: index("subscriptions_userId_idx").on(table.userId),
|
|
})
|
|
);
|
|
|
|
export type Subscription = typeof subscriptions.$inferSelect;
|
|
export type InsertSubscription = typeof subscriptions.$inferInsert;
|
|
|
|
// ─── Clinics (cabinets médicaux) ─────────────────────────────────────────────
|
|
export const clinics = mysqlTable(
|
|
"clinics",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
name: varchar("name", { length: 255 }).notNull(),
|
|
address: text("address"),
|
|
phone: varchar("phone", { length: 32 }),
|
|
color: varchar("color", { length: 16 }).default("#10b981"),
|
|
// Numéro WhatsApp Business du cabinet
|
|
whatsappPhone: varchar("whatsappPhone", { length: 32 }),
|
|
isActive: boolean("isActive").default(true).notNull(),
|
|
// QR code token rotatif anti-triche
|
|
qrToken: varchar("qrToken", { length: 64 }).notNull(),
|
|
qrTokenExpiresAt: timestamp("qrTokenExpiresAt"),
|
|
qrRotationMinutes: int("qrRotationMinutes").default(30),
|
|
// Paramètres file d'attente
|
|
avgConsultationMinutes: int("avgConsultationMinutes").default(15),
|
|
maxQueueSize: int("maxQueueSize").default(50),
|
|
isQueueOpen: boolean("isQueueOpen").default(false).notNull(),
|
|
currentTicketNumber: int("currentTicketNumber").default(0).notNull(),
|
|
// Timer absent automatique : marque absent après N minutes sans réponse (0 = désactivé)
|
|
autoAbsentMinutes: int("autoAbsentMinutes").default(0).notNull(),
|
|
// Paramètres enrichis cabinet
|
|
welcomeMessage: text("welcomeMessage"),
|
|
// Horaires d'ouverture JSON : { monday: { open, close, closed }, ... }
|
|
openingHours: json("openingHours"),
|
|
// Langue de l'interface patient : "fr" | "en" | "ar" | "pt" | "es"
|
|
patientLanguage: varchar("patientLanguage", { length: 8 }).default("fr"),
|
|
// Templates WhatsApp personnalisables (null = template par défaut)
|
|
whatsappTemplateJoined: text("whatsappTemplateJoined"),
|
|
whatsappTemplateSoon: text("whatsappTemplateSoon"),
|
|
whatsappTemplateCalled: text("whatsappTemplateCalled"),
|
|
whatsappTemplateWithdrawn: text("whatsappTemplateWithdrawn"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
userIdx: index("clinics_userId_idx").on(table.userId),
|
|
qrTokenIdx: uniqueIndex("clinics_qrToken_idx").on(table.qrToken),
|
|
})
|
|
);
|
|
|
|
export type Clinic = typeof clinics.$inferSelect;
|
|
export type InsertClinic = typeof clinics.$inferInsert;
|
|
|
|
// ─── Queue Entries (patients en file) ────────────────────────────────────────
|
|
export const queueEntries = mysqlTable(
|
|
"queue_entries",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
clinicId: int("clinicId").notNull(),
|
|
ticketNumber: int("ticketNumber").notNull(),
|
|
// Identifiant de session anonyme du patient
|
|
patientToken: varchar("patientToken", { length: 64 }).notNull(),
|
|
patientName: varchar("patientName", { length: 128 }),
|
|
patientPhone: varchar("patientPhone", { length: 32 }),
|
|
status: mysqlEnum("status", [
|
|
"waiting",
|
|
"called",
|
|
"in_consultation",
|
|
"done",
|
|
"absent",
|
|
"canceled",
|
|
])
|
|
.default("waiting")
|
|
.notNull(),
|
|
position: int("position").notNull(),
|
|
joinedAt: timestamp("joinedAt").defaultNow().notNull(),
|
|
calledAt: timestamp("calledAt"),
|
|
consultationStartAt: timestamp("consultationStartAt"),
|
|
consultationEndAt: timestamp("consultationEndAt"),
|
|
estimatedWaitMinutes: int("estimatedWaitMinutes"),
|
|
notificationSent: boolean("notificationSent").default(false).notNull(),
|
|
// Motif de visite patient
|
|
visitReason: mysqlEnum("visitReason", [
|
|
"consultation",
|
|
"urgence",
|
|
"certificat_scolaire",
|
|
"certificat_sportif",
|
|
"arret_travail",
|
|
"administratif",
|
|
"autre",
|
|
]).default("consultation"),
|
|
visitNote: text("visitNote"),
|
|
// Timing consultation supplémentaire (pour stats)
|
|
consultationStartedAt: timestamp("consultationStartedAt"),
|
|
// Notifications WhatsApp
|
|
whatsappPhone: varchar("whatsappPhone", { length: 32 }),
|
|
whatsappSentJoined: boolean("whatsappSentJoined").default(false).notNull(),
|
|
whatsappSentSoon: boolean("whatsappSentSoon").default(false).notNull(),
|
|
whatsappSentCalled: boolean("whatsappSentCalled").default(false).notNull(),
|
|
// Pour l'impression de ticket
|
|
isPrinted: boolean("isPrinted").default(false).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
clinicIdx: index("queue_clinicId_idx").on(table.clinicId),
|
|
statusIdx: index("queue_status_idx").on(table.status),
|
|
tokenIdx: index("queue_patientToken_idx").on(table.patientToken),
|
|
})
|
|
);
|
|
|
|
export type QueueEntry = typeof queueEntries.$inferSelect;
|
|
export type InsertQueueEntry = typeof queueEntries.$inferInsert;
|
|
|
|
// ─── Analytics Events ─────────────────────────────────────────────────────────
|
|
export const analyticsEvents = mysqlTable(
|
|
"analytics_events",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
clinicId: int("clinicId").notNull(),
|
|
eventType: mysqlEnum("eventType", [
|
|
"patient_joined",
|
|
"patient_called",
|
|
"patient_done",
|
|
"patient_absent",
|
|
"queue_opened",
|
|
"queue_closed",
|
|
]).notNull(),
|
|
ticketNumber: int("ticketNumber"),
|
|
waitMinutes: int("waitMinutes"),
|
|
consultationMinutes: int("consultationMinutes"),
|
|
queueSizeAtEvent: int("queueSizeAtEvent"),
|
|
hourOfDay: int("hourOfDay"),
|
|
dayOfWeek: int("dayOfWeek"),
|
|
metadata: json("metadata"),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
clinicIdx: index("analytics_clinicId_idx").on(table.clinicId),
|
|
createdIdx: index("analytics_createdAt_idx").on(table.createdAt),
|
|
})
|
|
);
|
|
|
|
export type AnalyticsEvent = typeof analyticsEvents.$inferSelect;
|
|
export type InsertAnalyticsEvent = typeof analyticsEvents.$inferInsert;
|
|
|
|
// ─── WhatsApp Country Codes ──────────────────────────────────────────────────
|
|
// Indicatifs pays disponibles pour les notifications WhatsApp patients
|
|
export const whatsappCountryCodes = mysqlTable(
|
|
"whatsapp_country_codes",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
code: varchar("code", { length: 4 }).notNull(),
|
|
dialCode: varchar("dialCode", { length: 8 }).notNull(),
|
|
nameFr: varchar("nameFr", { length: 128 }).notNull(),
|
|
flag: varchar("flag", { length: 8 }).notNull(),
|
|
enabled: boolean("enabled").default(false).notNull(),
|
|
sortOrder: int("sortOrder").default(100).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
codeIdx: uniqueIndex("whatsapp_country_codes_code_idx").on(table.code),
|
|
})
|
|
);
|
|
|
|
export type WhatsappCountryCode = typeof whatsappCountryCodes.$inferSelect;
|
|
export type InsertWhatsappCountryCode = typeof whatsappCountryCodes.$inferInsert;
|
|
|
|
// ─── WhatsApp Logs ────────────────────────────────────────────────────────────
|
|
// Trace chaque message WhatsApp envoyé par cabinet
|
|
export const whatsappLogs = mysqlTable(
|
|
"whatsapp_logs",
|
|
{
|
|
id: int("id").autoincrement().primaryKey(),
|
|
clinicId: int("clinicId").notNull(),
|
|
phoneMasked: varchar("phoneMasked", { length: 32 }).notNull(),
|
|
messageType: mysqlEnum("messageType", ["joined", "soon", "called", "withdrawn", "test"]).notNull(),
|
|
status: mysqlEnum("status", ["sent", "failed"]).notNull(),
|
|
errorMessage: text("errorMessage"),
|
|
messagePreview: varchar("messagePreview", { length: 120 }),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
},
|
|
(table) => ({
|
|
clinicIdx: index("whatsapp_logs_clinicId_idx").on(table.clinicId),
|
|
createdIdx: index("whatsapp_logs_createdAt_idx").on(table.createdAt),
|
|
})
|
|
);
|
|
|
|
export type WhatsappLog = typeof whatsappLogs.$inferSelect;
|
|
export type InsertWhatsappLog = typeof whatsappLogs.$inferInsert;
|