Task #704: أوقات حجز محددة يديرها الأدمن + مهلة حجز مسبقة
- DB: protocol_booking_slots (وقت "HH:mm" بتوقيت الرياض، مدة، تفعيل، ترتيب) + protocol_settings (booking.minLeadMinutes، الافتراضي 60 دقيقة). - API: CRUD /protocol/booking-slots (الكتابة تتطلب protocol.rooms.manage، تكرار الوقت → 409 duplicate_slot)، GET/PATCH /protocol/settings (upsert + سجل تدقيق)، ونقطة عامة /protocol/public/availability (محدّد معدل خاص، تعيد booked/tooSoon/available لكل وقت). - الفرض في الخادم: الطلب العام يجب أن يطابق وقتاً مفعّلاً + يحترم المهلة (400 invalid_slot / lead_time برسائل عربية)؛ الموظفون ملزمون بالأوقات لكن معفيون من المهلة. بدون أوقات مفعّلة يعمل الوضع الحر القديم. - الواجهة: نموذج الطلب العام يعرض شبكة أوقات حسب التوفر (مع fallback قديم)؛ تبويب القاعات يعرض قسم إدارة الأوقات + المهلة لمن يملك canManageRooms؛ حوار الحجز الداخلي يتحول لتاريخ + قائمة أوقات عند وجود أوقات مفعّلة. - i18n: مفاتيح protocol.slots.* بالعربية والإنجليزية. - اختبارات: tests/protocol-booking-slots.test.mjs — 10 اختبارات ناجحة (CRUD/تكرار، الإعدادات، التوفر، invalid_slot، lead_time، تعارض، وقت معطّل، إعفاء الموظفين، صلاحيات القراءة/الكتابة، الحذف). - مراجعة المعماري: PASS؛ عولجت الملاحظات (توثيق قرار الصلاحيات + اختبار regression له، ومنع الإرسال في الواجهة قبل اختيار التاريخ والوقت).
This commit is contained in:
@@ -344,6 +344,66 @@ export const protocolPhotosTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bookable time slots (أوقات الحجز).
|
||||
//
|
||||
// Slots are GLOBAL — they define the daily time grid available for booking in
|
||||
// every room (per-room availability still comes from the conflict rule).
|
||||
// Keeping them global was a deliberate simplicity choice: the admin manages
|
||||
// one list of times instead of one list per room, matching how the team
|
||||
// actually schedules. `startTime` is a wall-clock "HH:mm" in Asia/Riyadh.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const protocolBookingSlotsTable = pgTable(
|
||||
"protocol_booking_slots",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
// Wall-clock start time in Asia/Riyadh, canonical "HH:mm" (24h, zero
|
||||
// padded). Uniqueness is enforced so two slots can't start together.
|
||||
startTime: varchar("start_time", { length: 5 }).notNull(),
|
||||
durationMinutes: integer("duration_minutes").notNull().default(30),
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
sortOrder: integer("sort_order").notNull().default(0),
|
||||
createdBy: integer("created_by").references(() => usersTable.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(t) => ({
|
||||
activeIdx: index("protocol_slots_active_idx").on(t.isActive),
|
||||
startUnique: uniqueIndex("protocol_slots_start_unique").on(t.startTime),
|
||||
}),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module settings (key/value). Currently only `booking.minLeadMinutes` — the
|
||||
// minimum advance notice (in minutes) required before a PUBLIC booking's
|
||||
// start time. Stored as a string value to keep the table generic.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const protocolSettingsTable = pgTable(
|
||||
"protocol_settings",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
key: varchar("key", { length: 100 }).notNull(),
|
||||
value: text("value").notNull(),
|
||||
updatedBy: integer("updated_by").references(() => usersTable.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(t) => ({
|
||||
keyUnique: uniqueIndex("protocol_settings_key_unique").on(t.key),
|
||||
}),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit log (scoped to the protocol module only)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -424,3 +484,13 @@ export type InsertProtocolPhoto = z.infer<typeof insertProtocolPhotoSchema>;
|
||||
export type ProtocolPhoto = typeof protocolPhotosTable.$inferSelect;
|
||||
|
||||
export type ProtocolAuditLog = typeof protocolAuditLogsTable.$inferSelect;
|
||||
|
||||
export const insertProtocolBookingSlotSchema = createInsertSchema(
|
||||
protocolBookingSlotsTable,
|
||||
).omit({ id: true, createdAt: true, updatedAt: true });
|
||||
export type InsertProtocolBookingSlot = z.infer<
|
||||
typeof insertProtocolBookingSlotSchema
|
||||
>;
|
||||
export type ProtocolBookingSlot = typeof protocolBookingSlotsTable.$inferSelect;
|
||||
|
||||
export type ProtocolSetting = typeof protocolSettingsTable.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user