From 0f10aafa43f90fbdbcdc79e0c72a6bcdda69b620 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Sat, 6 Jun 2026 17:37:17 +0300 Subject: [PATCH] Add Calendar dialog, Prescriptions & Messages pages, note delete confirm - Appointments: a Calendar button next to Add opens a month-grid dialog (reuses the Calendar primitive + shared ScheduleList); the schedule day is marked and selecting it lists that day's appointments. - Patients sub-nav: new Prescriptions page (mock list + KPIs) with a compact "New prescription" dialog reusing the patient quick-search pattern. - Notes: deleting a note now goes through a reusable ConfirmDialog instead of deleting immediately. - New top-level Messages page: two-pane email-style inbox (list + reading pane with mock reply composer), mirroring the Notes layout. - Sidebar logo bumped from size-9 to size-10. Co-Authored-By: Claude Opus 4.8 --- frontend/app/(app)/messages/page.tsx | 10 + frontend/app/(app)/prescriptions/page.tsx | 10 + .../appointments/appointments-view.tsx | 48 +++- .../appointments/calendar-dialog.tsx | 99 +++++++ .../components/messages/messages-view.tsx | 226 +++++++++++++++ frontend/components/notes/notes-editor.tsx | 15 +- .../prescriptions/add-prescription-dialog.tsx | 268 ++++++++++++++++++ .../prescriptions/prescriptions-view.tsx | 249 ++++++++++++++++ .../components/sidebar-02/app-sidebar.tsx | 6 +- frontend/components/ui/confirm-dialog.tsx | 63 ++++ frontend/lib/i18n/locales/en/translation.json | 2 + frontend/lib/nav.ts | 9 + 12 files changed, 990 insertions(+), 15 deletions(-) create mode 100644 frontend/app/(app)/messages/page.tsx create mode 100644 frontend/app/(app)/prescriptions/page.tsx create mode 100644 frontend/components/appointments/calendar-dialog.tsx create mode 100644 frontend/components/messages/messages-view.tsx create mode 100644 frontend/components/prescriptions/add-prescription-dialog.tsx create mode 100644 frontend/components/prescriptions/prescriptions-view.tsx create mode 100644 frontend/components/ui/confirm-dialog.tsx diff --git a/frontend/app/(app)/messages/page.tsx b/frontend/app/(app)/messages/page.tsx new file mode 100644 index 0000000..0ab9e5a --- /dev/null +++ b/frontend/app/(app)/messages/page.tsx @@ -0,0 +1,10 @@ +import { MessagesView } from "@/components/messages/messages-view"; +import { SidebarInset } from "@/components/ui/sidebar"; + +export default function MessagesPage() { + return ( + + + + ); +} diff --git a/frontend/app/(app)/prescriptions/page.tsx b/frontend/app/(app)/prescriptions/page.tsx new file mode 100644 index 0000000..cc4c1cd --- /dev/null +++ b/frontend/app/(app)/prescriptions/page.tsx @@ -0,0 +1,10 @@ +import { PrescriptionsView } from "@/components/prescriptions/prescriptions-view"; +import { SidebarInset } from "@/components/ui/sidebar"; + +export default function PrescriptionsPage() { + return ( + + + + ); +} diff --git a/frontend/components/appointments/appointments-view.tsx b/frontend/components/appointments/appointments-view.tsx index f71e6ba..39b1303 100644 --- a/frontend/components/appointments/appointments-view.tsx +++ b/frontend/components/appointments/appointments-view.tsx @@ -1,12 +1,20 @@ "use client"; -import { CalendarClock, Clock, Plus, Stethoscope, Users } from "lucide-react"; +import { + CalendarClock, + CalendarDays, + Clock, + Plus, + Stethoscope, + Users, +} from "lucide-react"; import { type ReactNode, useState } from "react"; import { AddAppointmentDialog, type NewAppointment, } from "@/components/appointments/add-appointment-dialog"; +import { CalendarDialog } from "@/components/appointments/calendar-dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -17,7 +25,7 @@ import { Card } from "@/components/ui/card"; type ApptStatus = "confirmed" | "checked-in" | "completed" | "cancelled"; -type Appointment = { +export type Appointment = { time: string; name: string; initials: string; @@ -171,7 +179,7 @@ function ApptRow({ appt }: { appt: Appointment }) { ); } -function ScheduleList({ items }: { items: Appointment[] }) { +export function ScheduleList({ items }: { items: Appointment[] }) { return (
{items.map((appt) => ( @@ -205,6 +213,7 @@ function Section({ export function AppointmentsView() { const [addOpen, setAddOpen] = useState(false); + const [calendarOpen, setCalendarOpen] = useState(false); const [schedule, setSchedule] = useState(today); // Insert a new (mock) appointment into today's schedule, kept time-sorted. @@ -227,14 +236,25 @@ export function AppointmentsView() { Today's clinic schedule and what's coming up. Sample data.

- +
+ + +
@@ -258,6 +278,12 @@ export function AppointmentsView() { onOpenChange={setAddOpen} open={addOpen} /> + +
); } diff --git a/frontend/components/appointments/calendar-dialog.tsx b/frontend/components/appointments/calendar-dialog.tsx new file mode 100644 index 0000000..2c71c1e --- /dev/null +++ b/frontend/components/appointments/calendar-dialog.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState } from "react"; + +import { + type Appointment, + ScheduleList, +} from "@/components/appointments/appointments-view"; +import { Calendar } from "@/components/ui/calendar"; +import { + Dialog, + DialogDescription, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "@/components/ui/dialog"; + +// The mock schedule all belongs to this day (matches "Wednesday, June 5" in the +// Appointments view). Month is zero-based, so 5 = June. +const SCHEDULE_DATE = new Date(2026, 5, 5); + +const sameDay = (a: Date, b: Date) => + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate(); + +const fullDate = (d: Date) => + d.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + }); + +// A month-grid calendar (à la Google Calendar) shown in a dialog. The day that +// owns the mock schedule is ringed; selecting it lists those appointments, while +// any other day shows an empty note. Mock-only — there's no per-date backend. +export function CalendarDialog({ + open, + onOpenChange, + schedule, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + schedule: Appointment[]; +}) { + const [selected, setSelected] = useState(SCHEDULE_DATE); + + const dayItems = sameDay(selected, SCHEDULE_DATE) ? schedule : []; + + return ( + + + + Calendar + + Browse the schedule by date. Sample data. + + + + +
+ d && setSelected(d)} + selected={selected} + /> +
+ +
+
+

+ {fullDate(selected)} +

+

+ {dayItems.length === 1 + ? "1 appointment" + : `${dayItems.length} appointments`} +

+
+ {dayItems.length > 0 ? ( + + ) : ( +
+ No appointments on this day. +
+ )} +
+
+
+
+ ); +} diff --git a/frontend/components/messages/messages-view.tsx b/frontend/components/messages/messages-view.tsx new file mode 100644 index 0000000..3feecf8 --- /dev/null +++ b/frontend/components/messages/messages-view.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { Mail, SendHorizonal } from "lucide-react"; +import { useState } from "react"; + +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty"; +import { Textarea } from "@/components/ui/textarea"; +import { notify } from "@/lib/toast"; +import { cn } from "@/lib/utils"; + +// All messages here are mock/placeholder data — there is no messaging backend. +// They illustrate the email-style inbox layout (left list, right reading pane). + +type Message = { + id: string; + sender: string; + initials: string; + subject: string; + preview: string; + body: string[]; + time: string; + read: boolean; +}; + +const seed: Message[] = [ + { + id: "1", + sender: "Dr. Stein", + initials: "DS", + subject: "Lab results ready", + preview: "The lab results for Amina Yusuf are now available…", + body: [ + "Hi,", + "The lab results for Amina Yusuf (file #10293) are now available for your review. The lipid panel and HbA1c both came back within the expected range.", + "Let me know if you'd like to adjust her current plan before the follow-up.", + "— Dr. Stein", + ], + time: "10:24", + read: false, + }, + { + id: "2", + sender: "Dr. Okafor", + initials: "DO", + subject: "Re: Daniel Mensah intake", + preview: "Thanks for sending the intake notes over. I've…", + body: [ + "Thanks for sending the intake notes over.", + "I've added him to tomorrow's schedule at 10:00. Could you confirm whether his prior records were imported?", + "— Dr. Okafor", + ], + time: "09:12", + read: false, + }, + { + id: "3", + sender: "Care team", + initials: "CT", + subject: "Vaccination stock update", + preview: "A reminder that the seasonal vaccine stock has…", + body: [ + "A reminder that the seasonal vaccine stock has been replenished.", + "Slots are open across the week — please direct eligible patients to the front desk to book.", + ], + time: "Yesterday", + read: true, + }, + { + id: "4", + sender: "Reception", + initials: "RC", + subject: "Schedule change for Friday", + preview: "Two afternoon appointments were rescheduled…", + body: [ + "Two afternoon appointments on Friday were rescheduled to next Monday at the patients' request.", + "The updated times are reflected in the schedule.", + ], + time: "Mon", + read: true, + }, +]; + +export function MessagesView() { + const [messages, setMessages] = useState(seed); + const [selectedId, setSelectedId] = useState(null); + const [reply, setReply] = useState(""); + + const selected = messages.find((m) => m.id === selectedId) ?? null; + + const open = (id: string) => { + setSelectedId(id); + setReply(""); + setMessages((prev) => + prev.map((m) => (m.id === id ? { ...m, read: true } : m)), + ); + }; + + const send = () => { + if (!(reply.trim() && selected)) return; + notify.success("Reply sent", `To ${selected.sender}`); + setReply(""); + }; + + return ( +
+ {/* Left: inbox list */} + + + {/* Right: reading pane or empty state */} +
+ {selected ? ( +
+
+

+ {selected.subject} +

+
+ + {selected.initials} + +
+ + {selected.sender} + + + to me · {selected.time} + +
+
+
+ +
+
+ {selected.body.map((para) => ( +

{para}

+ ))} +
+
+ +
+