From e3484819dcdd6bc19d339c360b9c88bdc4a0a590 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Thu, 9 Jul 2026 05:54:18 +0000 Subject: [PATCH] =?UTF-8?q?Task=20#716:=20Booking=20slots=20=E2=80=94=20ed?= =?UTF-8?q?it=20dialog=20+=20compact=20numbered=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SlotsAdminSection (artifacts/tx-os/src/pages/protocol.tsx): replaced the 2-column card grid of booking slots with a compact shadcn Table sorted by startTime: # | time range | duration | days | status | actions. Rows are dense (py-1.5, small text, h-7 icon buttons), RTL/LTR safe. - New edit flow: pencil button opens a Dialog to change start time, duration (5–480, step 5) and days-of-week (same day-chip UI as the generator). Saves via existing PATCH /api/protocol/booking-slots/:id (no backend changes needed — route already accepted startTime/durationMinutes/daysOfWeek). 409 duplicate_slot shows a dedicated destructive toast; other errors fall back to the shared onError. Invalidates slots-admin + parent slots queries. - i18n: new keys under protocol.slots in ar.json/en.json (colTime, colStatus, colActions, edit, editTitle, editSaved, duplicateSlot, durationValue with full Arabic plural categories). - tsc --noEmit passes for tx-os. Architect review: Pass; added the suggested <=480 client-side duration guard. - Follow-up #717 proposed (UI test for edit + 409 toast). --- artifacts/tx-os/src/locales/ar.json | 15 +- artifacts/tx-os/src/locales/en.json | 11 +- artifacts/tx-os/src/pages/protocol.tsx | 272 ++++++++++++++++++++----- 3 files changed, 246 insertions(+), 52 deletions(-) diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index d958c47c..7651bfab 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -1968,7 +1968,20 @@ "generated": "تم توليد {{created}} وقتًا، ودمج الأيام في {{merged}}، وتجاهل {{skipped}} مكررًا.", "generatedNone": "لا جديد — كل الأوقات والأيام موجودة مسبقًا.", "pickDay": "اختر يومًا واحدًا على الأقل", - "noSlotsForDay": "لا توجد أوقات متاحة في هذا اليوم" + "noSlotsForDay": "لا توجد أوقات متاحة في هذا اليوم", + "colTime": "الوقت", + "colStatus": "الحالة", + "colActions": "إجراءات", + "edit": "تعديل", + "editTitle": "تعديل وقت الحجز", + "editSaved": "تم حفظ التعديل", + "duplicateSlot": "يوجد وقت حجز يبدأ في نفس التوقيت.", + "durationValue_zero": "{{count}} دقيقة", + "durationValue_one": "دقيقة واحدة", + "durationValue_two": "دقيقتان", + "durationValue_few": "{{count}} دقائق", + "durationValue_many": "{{count}} دقيقة", + "durationValue_other": "{{count}} دقيقة" } } } \ No newline at end of file diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 9f246d9a..acf4c037 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -1813,7 +1813,16 @@ "generated": "{{created}} slot(s) created, days merged into {{merged}}, {{skipped}} duplicate(s) skipped.", "generatedNone": "Nothing new — all start times and days already exist.", "pickDay": "Pick at least one day", - "noSlotsForDay": "No slots available on this day" + "noSlotsForDay": "No slots available on this day", + "colTime": "Time", + "colStatus": "Status", + "colActions": "Actions", + "edit": "Edit", + "editTitle": "Edit booking slot", + "editSaved": "Changes saved", + "duplicateSlot": "A slot with the same start time already exists.", + "durationValue_one": "{{count}} min", + "durationValue_other": "{{count}} min" } } } \ No newline at end of file diff --git a/artifacts/tx-os/src/pages/protocol.tsx b/artifacts/tx-os/src/pages/protocol.tsx index f3a29974..c9a98dbd 100644 --- a/artifacts/tx-os/src/pages/protocol.tsx +++ b/artifacts/tx-os/src/pages/protocol.tsx @@ -2554,6 +2554,23 @@ function SlotsAdminSection({ ? prev.filter((x) => x !== d) : [...prev, d].sort((a, b) => a - b), ); + // Inline slot editing (dialog) — startTime / duration / days. + const [editing, setEditing] = useState(null); + const [editTime, setEditTime] = useState(""); + const [editDuration, setEditDuration] = useState("30"); + const [editDays, setEditDays] = useState([]); + const openEdit = (s: Slot) => { + setEditing(s); + setEditTime(s.startTime); + setEditDuration(String(s.durationMinutes)); + setEditDays([...s.daysOfWeek]); + }; + const toggleEditDay = (d: number) => + setEditDays((prev) => + prev.includes(d) + ? prev.filter((x) => x !== d) + : [...prev, d].sort((a, b) => a - b), + ); // Full slot list (incl. inactive) — admin-only endpoint; this component is // only rendered for canManageRooms. @@ -2562,6 +2579,9 @@ function SlotsAdminSection({ queryFn: () => apiJson(`${API}/protocol/booking-slots`), }); const slots = slotsQuery.data ?? []; + const sortedSlots = [...slots].sort((a, b) => + a.startTime < b.startTime ? -1 : a.startTime > b.startTime ? 1 : 0, + ); const changed = () => { qc.invalidateQueries({ queryKey: ["protocol", "slots-admin"] }); onChanged(); @@ -2630,6 +2650,32 @@ function SlotsAdminSection({ onSuccess: changed, onError, }); + const editMut = useMutation({ + mutationFn: (s: Slot) => + apiJson(`${API}/protocol/booking-slots/${s.id}`, { + method: "PATCH", + body: JSON.stringify({ + startTime: editTime, + durationMinutes: Number(editDuration), + daysOfWeek: editDays, + }), + }), + onSuccess: () => { + setEditing(null); + toast({ title: t("protocol.slots.editSaved") }); + changed(); + }, + onError: (e: unknown) => { + if (e instanceof ApiError && e.status === 409) { + toast({ + title: t("protocol.slots.duplicateSlot"), + variant: "destructive", + }); + return; + } + onError(e); + }, + }); const deleteMut = useMutation({ mutationFn: (id: number) => apiJson(`${API}/protocol/booking-slots/${id}`, { method: "DELETE" }), @@ -2651,6 +2697,11 @@ function SlotsAdminSection({ }); const canAdd = /^\d{2}:\d{2}$/.test(newTime) && Number(newDuration) >= 5; + const canSaveEdit = + /^\d{2}:\d{2}$/.test(editTime) && + Number(editDuration) >= 5 && + Number(editDuration) <= 480 && + editDays.length > 0; const canGenerate = /^\d{2}:\d{2}$/.test(intFrom) && /^\d{2}:\d{2}$/.test(intTo) && @@ -2807,64 +2858,185 @@ function SlotsAdminSection({ {slots.length === 0 ? (

{t("protocol.slots.empty")}

) : ( -
- {slots.map((s) => ( -
-
-
- + + + + + # + + + {t("protocol.slots.colTime")} + + + {t("protocol.slots.duration")} + + + {t("protocol.slots.days")} + + + {t("protocol.slots.colStatus")} + + + {t("protocol.slots.colActions")} + + + + + {sortedSlots.map((s, i) => ( + + + {i + 1} + + {fmtSlotRange(s.startTime, s.durationMinutes, isAr)} - - + + + {t("protocol.slots.durationValue", { + count: s.durationMinutes, + })} + + {fmtSlotDays(s.daysOfWeek, t, isAr)} - - - - {s.isActive - ? t("protocol.rooms.active") - : t("protocol.rooms.inactive")} - - -
- - -
- - ))} +
+ + + {s.isActive + ? t("protocol.rooms.active") + : t("protocol.rooms.inactive")} + + + +
+ + + +
+
+
+ ))} +
+
)}
+ { + if (!open) setEditing(null); + }} + > + + + {t("protocol.slots.editTitle")} + +
+
+
+ + setEditTime(e.target.value)} + /> +
+
+ + setEditDuration(e.target.value)} + /> +
+
+
+ +
+ {ALL_DAYS.map((d) => ( + + ))} +
+ {editDays.length === 0 && ( +

+ {t("protocol.slots.pickDay")} +

+ )} +
+
+ + + + +
+
+

{t("protocol.slots.leadTitle")}