feat: MedSchedule модуль — семейный медицинский помощник с Telegram/MAX ботом, n8n OCR и /med UI
This commit is contained in:
198
shared/schema.ts
198
shared/schema.ts
@@ -1,5 +1,5 @@
|
||||
import { sql, relations } from "drizzle-orm";
|
||||
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, unique, foreignKey, uniqueIndex, index, numeric, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, varchar, integer, boolean, timestamp, jsonb, serial, unique, foreignKey, uniqueIndex, index, numeric, real, date, uuid, type AnyPgColumn, customType } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -3623,3 +3623,199 @@ export const FINANCE_COLORS = [
|
||||
"#6366f1", "#f43f5e", "#10b981", "#d946ef",
|
||||
"#0ea5e9", "#f97316", "#22c55e", "#e879f9",
|
||||
];
|
||||
|
||||
// =====================
|
||||
// MedSchedule — семейное медицинское расписание
|
||||
// =====================
|
||||
|
||||
export const medFamilyGroups = pgTable("med_family_groups", {
|
||||
id: serial("id").primaryKey(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
inviteCode: text("invite_code").unique().notNull(),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
orgIdx: index("med_family_groups_org_idx").on(table.organizationId),
|
||||
inviteIdx: uniqueIndex("med_family_groups_invite_idx").on(table.inviteCode),
|
||||
}));
|
||||
|
||||
export type MedFamilyGroup = typeof medFamilyGroups.$inferSelect;
|
||||
export type InsertMedFamilyGroup = typeof medFamilyGroups.$inferInsert;
|
||||
|
||||
export const medFamilyMembers = pgTable("med_family_members", {
|
||||
id: serial("id").primaryKey(),
|
||||
familyId: integer("family_id").notNull().references(() => medFamilyGroups.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }),
|
||||
profileName: text("profile_name").notNull(),
|
||||
profileType: text("profile_type").notNull().default("person"),
|
||||
dateOfBirth: date("date_of_birth"),
|
||||
gender: text("gender"),
|
||||
notes: text("notes"),
|
||||
telegramChatId: text("telegram_chat_id"),
|
||||
maxChatId: text("max_chat_id"),
|
||||
role: text("role").notNull().default("member"),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
familyIdx: index("med_family_members_family_idx").on(table.familyId),
|
||||
userIdx: index("med_family_members_user_idx").on(table.userId),
|
||||
}));
|
||||
|
||||
export type MedFamilyMember = typeof medFamilyMembers.$inferSelect;
|
||||
export type InsertMedFamilyMember = typeof medFamilyMembers.$inferInsert;
|
||||
|
||||
export const medMedications = pgTable("med_medications", {
|
||||
id: serial("id").primaryKey(),
|
||||
familyId: integer("family_id").notNull().references(() => medFamilyGroups.id, { onDelete: "cascade" }),
|
||||
memberId: integer("member_id").notNull().references(() => medFamilyMembers.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
dosage: text("dosage"),
|
||||
form: text("form"),
|
||||
schedule: jsonb("schedule").$type<{
|
||||
type: "daily" | "weekly" | "interval" | "as_needed";
|
||||
times: string[];
|
||||
days?: number[];
|
||||
intervalHours?: number;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
}>().notNull(),
|
||||
instructions: text("instructions"),
|
||||
color: text("color").default("#4CAF50"),
|
||||
isActive: boolean("is_active").default(true),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
familyIdx: index("med_medications_family_idx").on(table.familyId),
|
||||
memberIdx: index("med_medications_member_idx").on(table.memberId),
|
||||
}));
|
||||
|
||||
export type MedMedication = typeof medMedications.$inferSelect;
|
||||
export type InsertMedMedication = typeof medMedications.$inferInsert;
|
||||
|
||||
export const medIntakeLogs = pgTable("med_intake_logs", {
|
||||
id: serial("id").primaryKey(),
|
||||
medicationId: integer("medication_id").notNull().references(() => medMedications.id, { onDelete: "cascade" }),
|
||||
memberId: integer("member_id").notNull().references(() => medFamilyMembers.id, { onDelete: "cascade" }),
|
||||
scheduledAt: timestamp("scheduled_at").notNull(),
|
||||
takenAt: timestamp("taken_at"),
|
||||
status: text("status").$type<"pending" | "taken" | "skipped" | "late" | "snoozed">().default("pending").notNull(),
|
||||
note: text("note"),
|
||||
remindedVia: text("reminded_via").array().default(sql`'{}'::text[]`),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
medicationIdx: index("med_intake_logs_medication_idx").on(table.medicationId),
|
||||
memberIdx: index("med_intake_logs_member_idx").on(table.memberId),
|
||||
scheduledIdx: index("med_intake_logs_scheduled_idx").on(table.scheduledAt),
|
||||
statusIdx: index("med_intake_logs_status_idx").on(table.status),
|
||||
}));
|
||||
|
||||
export type MedIntakeLog = typeof medIntakeLogs.$inferSelect;
|
||||
export type InsertMedIntakeLog = typeof medIntakeLogs.$inferInsert;
|
||||
|
||||
export const medCabinetItems = pgTable("med_cabinet_items", {
|
||||
id: serial("id").primaryKey(),
|
||||
familyId: integer("family_id").notNull().references(() => medFamilyGroups.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
form: text("form"),
|
||||
dosage: text("dosage"),
|
||||
quantity: real("quantity").default(0),
|
||||
unit: text("unit").default("шт"),
|
||||
expiryDate: date("expiry_date"),
|
||||
indication: text("indication"),
|
||||
location: text("location"),
|
||||
photoUrl: text("photo_url"),
|
||||
addedBy: integer("added_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
familyIdx: index("med_cabinet_items_family_idx").on(table.familyId),
|
||||
expiryIdx: index("med_cabinet_items_expiry_idx").on(table.expiryDate),
|
||||
}));
|
||||
|
||||
export type MedCabinetItem = typeof medCabinetItems.$inferSelect;
|
||||
export type InsertMedCabinetItem = typeof medCabinetItems.$inferInsert;
|
||||
|
||||
export const medCabinetStock = pgTable("med_cabinet_stock", {
|
||||
id: serial("id").primaryKey(),
|
||||
cabinetItemId: integer("cabinet_item_id").notNull().references(() => medCabinetItems.id, { onDelete: "cascade" }),
|
||||
change: real("change").notNull(),
|
||||
reason: text("reason"),
|
||||
note: text("note"),
|
||||
changedBy: integer("changed_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
itemIdx: index("med_cabinet_stock_item_idx").on(table.cabinetItemId),
|
||||
}));
|
||||
|
||||
export type MedCabinetStock = typeof medCabinetStock.$inferSelect;
|
||||
export type InsertMedCabinetStock = typeof medCabinetStock.$inferInsert;
|
||||
|
||||
export const medMedicalRecords = pgTable("med_medical_records", {
|
||||
id: serial("id").primaryKey(),
|
||||
familyId: integer("family_id").notNull().references(() => medFamilyGroups.id, { onDelete: "cascade" }),
|
||||
memberId: integer("member_id").notNull().references(() => medFamilyMembers.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
recordType: text("record_type").notNull(),
|
||||
recordDate: date("record_date"),
|
||||
doctorName: text("doctor_name"),
|
||||
clinicName: text("clinic_name"),
|
||||
rawText: text("raw_text"),
|
||||
aiSummary: text("ai_summary"),
|
||||
aiFindings: jsonb("ai_findings").$type<Array<{ finding: string; value: string; norm?: string }>>(),
|
||||
fileUrls: text("file_urls").array().default(sql`'{}'::text[]`),
|
||||
addedBy: integer("added_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
familyIdx: index("med_medical_records_family_idx").on(table.familyId),
|
||||
memberIdx: index("med_medical_records_member_idx").on(table.memberId),
|
||||
dateIdx: index("med_medical_records_date_idx").on(table.recordDate),
|
||||
}));
|
||||
|
||||
export type MedMedicalRecord = typeof medMedicalRecords.$inferSelect;
|
||||
export type InsertMedMedicalRecord = typeof medMedicalRecords.$inferInsert;
|
||||
|
||||
export const medDailyCheckins = pgTable("med_daily_checkins", {
|
||||
id: serial("id").primaryKey(),
|
||||
familyId: integer("family_id").notNull().references(() => medFamilyGroups.id, { onDelete: "cascade" }),
|
||||
memberId: integer("member_id").references(() => medFamilyMembers.id, { onDelete: "cascade" }),
|
||||
checkInDate: date("check_in_date").notNull(),
|
||||
status: text("status").$type<"pending" | "answered" | "skipped">().default("pending").notNull(),
|
||||
question: text("question").notNull(),
|
||||
answerText: text("answer_text"),
|
||||
aiSummary: text("ai_summary"),
|
||||
aiAlert: text("ai_alert"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
familyIdx: index("med_daily_checkins_family_idx").on(table.familyId),
|
||||
memberIdx: index("med_daily_checkins_member_idx").on(table.memberId),
|
||||
dateIdx: index("med_daily_checkins_date_idx").on(table.checkInDate),
|
||||
}));
|
||||
|
||||
export type MedDailyCheckin = typeof medDailyCheckins.$inferSelect;
|
||||
export type InsertMedDailyCheckin = typeof medDailyCheckins.$inferInsert;
|
||||
|
||||
export const medBotBindings = pgTable("med_bot_bindings", {
|
||||
id: serial("id").primaryKey(),
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
telegramChatId: text("telegram_chat_id"),
|
||||
maxChatId: text("max_chat_id"),
|
||||
telegramUsername: text("telegram_username"),
|
||||
maxUsername: text("max_username"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
userOrgIdx: uniqueIndex("med_bot_bindings_user_org_idx").on(table.userId, table.organizationId),
|
||||
telegramIdx: uniqueIndex("med_bot_bindings_telegram_idx").on(table.organizationId, table.telegramChatId),
|
||||
maxIdx: uniqueIndex("med_bot_bindings_max_idx").on(table.organizationId, table.maxChatId),
|
||||
}));
|
||||
|
||||
export type MedBotBinding = typeof medBotBindings.$inferSelect;
|
||||
export type InsertMedBotBinding = typeof medBotBindings.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user