From 00152eb3cb004802953c21b649275c5b8d768f65 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Sat, 6 Jun 2026 18:18:48 +0300 Subject: [PATCH] Redesign Calendar + Messages, fix favicon, add date picker, Tasks & Activity - Appointments now carry a date; the Calendar button opens a Google-Calendar style month grid (color-coded event chips, month nav, Today, click a day for its list). Today/Upcoming sections derive from dates. - Add-appointment dialog gains a date picker (Popover + Calendar). - Messages rebuilt as chat threads: an input-based composer that appends to a two-way timeline (no more toast covering the field), and the unread count is now a filter toggle. - Favicon: the brand logo now shows in the tab. Removed the stale default app/favicon.ico (which shadowed it) and added app/icon.png + app/apple-icon.png generated from the logo; dropped the redundant metadata.icons. - Sidebar: swapped Notes <-> Messages order (both kept). - New pages: Activity (signed-change audit timeline with hash chips + approval status) and Tasks (two-pane care-team to-do with add dialog). - Prescriptions: adding a medication now surfaces mock drug-interaction and allergy warnings against the patient's record. Co-Authored-By: Claude Opus 4.8 --- frontend/app/(app)/activity/page.tsx | 10 + frontend/app/(app)/tasks/page.tsx | 10 + frontend/app/apple-icon.png | Bin 0 -> 8701 bytes frontend/app/favicon.ico | Bin 25931 -> 0 bytes frontend/app/icon.png | Bin 0 -> 16055 bytes frontend/app/layout.tsx | 2 +- .../components/activity/activity-view.tsx | 211 +++++++++ .../appointments/add-appointment-dialog.tsx | 68 ++- .../appointments/appointments-view.tsx | 145 ++++-- .../appointments/calendar-dialog.tsx | 223 ++++++++-- .../components/messages/messages-view.tsx | 353 +++++++++------ .../prescriptions/add-prescription-dialog.tsx | 66 ++- frontend/components/tasks/tasks-view.tsx | 413 ++++++++++++++++++ frontend/lib/i18n/locales/en/translation.json | 2 + frontend/lib/nav.ts | 6 +- 15 files changed, 1286 insertions(+), 223 deletions(-) create mode 100644 frontend/app/(app)/activity/page.tsx create mode 100644 frontend/app/(app)/tasks/page.tsx create mode 100644 frontend/app/apple-icon.png delete mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/icon.png create mode 100644 frontend/components/activity/activity-view.tsx create mode 100644 frontend/components/tasks/tasks-view.tsx diff --git a/frontend/app/(app)/activity/page.tsx b/frontend/app/(app)/activity/page.tsx new file mode 100644 index 0000000..c0b3f2f --- /dev/null +++ b/frontend/app/(app)/activity/page.tsx @@ -0,0 +1,10 @@ +import { ActivityView } from "@/components/activity/activity-view"; +import { SidebarInset } from "@/components/ui/sidebar"; + +export default function ActivityPage() { + return ( + + + + ); +} diff --git a/frontend/app/(app)/tasks/page.tsx b/frontend/app/(app)/tasks/page.tsx new file mode 100644 index 0000000..c67eca5 --- /dev/null +++ b/frontend/app/(app)/tasks/page.tsx @@ -0,0 +1,10 @@ +import { TasksView } from "@/components/tasks/tasks-view"; +import { SidebarInset } from "@/components/ui/sidebar"; + +export default function TasksPage() { + return ( + + + + ); +} diff --git a/frontend/app/apple-icon.png b/frontend/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e254b94fd238e7ba901bbacf35fbea6eb855283e GIT binary patch literal 8701 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 literal 0 HcmV?d00001 diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/frontend/app/icon.png b/frontend/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..45bbdd6d07c73eadf4cd0b051adbd1f341e175f3 GIT binary patch literal 16055 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 F{{`_K>O=ql literal 0 HcmV?d00001 diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 0ed646e..6b533a1 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { title: "temetro — AI assistant for clinicians", description: "Retrieve patient information by simply asking. The open-source AI assistant for clinicians.", - icons: { icon: "/temetro-logo.png", apple: "/temetro-logo.png" }, + // Icons come from the app/icon.png + app/apple-icon.png file conventions. }; export default function RootLayout({ diff --git a/frontend/components/activity/activity-view.tsx b/frontend/components/activity/activity-view.tsx new file mode 100644 index 0000000..8376aea --- /dev/null +++ b/frontend/components/activity/activity-view.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { + Clock, + FileText, + Hash, + type LucideIcon, + NotebookPen, + Pill, + ShieldCheck, + Stethoscope, + TriangleAlert, +} from "lucide-react"; + +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +// All entries here are mock/placeholder data — there is no signing/ledger +// backend yet. They illustrate temetro's patient-owned, signed-change vision: +// every record change is signed (blockchain-style) and awaits patient approval. + +type ActivityStatus = "signed" | "pending"; + +type ActivityEntry = { + id: string; + actor: string; + initials: string; + action: string; + patient: string; + fileNumber: string; + time: string; + hash: string; + status: ActivityStatus; + icon: LucideIcon; +}; + +const entries: ActivityEntry[] = [ + { + id: "1", + actor: "Dr. Okafor", + initials: "DO", + action: "Updated vitals", + patient: "Amina Yusuf", + fileNumber: "10293", + time: "Today, 10:24", + hash: "0x9f3a…c21", + status: "pending", + icon: Stethoscope, + }, + { + id: "2", + actor: "Dr. Okafor", + initials: "DO", + action: "Added prescription — Lisinopril 10mg", + patient: "Amina Yusuf", + fileNumber: "10293", + time: "Today, 10:21", + hash: "0x4b8e…7df", + status: "pending", + icon: Pill, + }, + { + id: "3", + actor: "Dr. Stein", + initials: "DS", + action: "Created note — Lab review", + patient: "Leila Haddad", + fileNumber: "10342", + time: "Today, 09:48", + hash: "0x1c07…a90", + status: "signed", + icon: NotebookPen, + }, + { + id: "4", + actor: "Dr. Stein", + initials: "DS", + action: "Edited allergies — added Penicillin", + patient: "Daniel Mensah", + fileNumber: "10311", + time: "Yesterday, 16:05", + hash: "0xab12…44e", + status: "signed", + icon: TriangleAlert, + }, + { + id: "5", + actor: "Dr. Okafor", + initials: "DO", + action: "Imported prior records", + patient: "Carlos Rivera", + fileNumber: "10358", + time: "Yesterday, 14:30", + hash: "0x77f0…b3c", + status: "signed", + icon: FileText, + }, +]; + +const kpis = [ + { label: "Pending approvals", value: "2", icon: Clock }, + { label: "Signed today", value: "1", icon: ShieldCheck }, + { label: "Changes this week", value: "37", icon: Hash }, +]; + +function Kpi({ + label, + value, + icon: Icon, +}: { + label: string; + value: string; + icon: LucideIcon; +}) { + return ( + +
+ +
+
+ {label} + + {value} + +
+
+ ); +} + +export function ActivityView() { + return ( +
+
+

Activity

+

+ A signed, tamper-evident log of record changes awaiting patient + approval. Sample data. +

+
+ +
+ {kpis.map((k) => ( + + ))} +
+ +
    + {entries.map((entry, i) => { + const Icon = entry.icon; + const isLast = i === entries.length - 1; + return ( +
  1. +
    +
    + +
    + {!isLast &&
    } +
    + +
    +
    + + {entry.action} + + {entry.status === "signed" ? ( + + + Signed + + ) : ( + + + Pending approval + + )} +
    + +
    + + + {entry.initials} + + + + {entry.actor} · {entry.patient} (#{entry.fileNumber}) + +
    + +
    + {entry.time} + + + {entry.hash} + +
    +
    +
  2. + ); + })} +
+
+ ); +} diff --git a/frontend/components/appointments/add-appointment-dialog.tsx b/frontend/components/appointments/add-appointment-dialog.tsx index 7bce241..7176d35 100644 --- a/frontend/components/appointments/add-appointment-dialog.tsx +++ b/frontend/components/appointments/add-appointment-dialog.tsx @@ -1,9 +1,11 @@ "use client"; -import { Search } from "lucide-react"; +import { CalendarDays, Search } from "lucide-react"; import { type FormEvent, type ReactNode, useEffect, useMemo, useState } from "react"; +import { TODAY } from "@/components/appointments/appointments-view"; import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; import { Dialog, DialogClose, @@ -15,6 +17,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover"; import { listPatients, type Patient } from "@/lib/patients"; import { notify } from "@/lib/toast"; @@ -22,11 +25,18 @@ export type NewAppointment = { fileNumber: string; name: string; initials: string; + date: string; // ISO YYYY-MM-DD time: string; type: string; provider: string; }; +// Local-date ISO key (avoids UTC drift from toISOString). +const keyOf = (d: Date) => + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String( + d.getDate(), + ).padStart(2, "0")}`; + const TYPES = [ "Follow-up", "New patient", @@ -62,6 +72,8 @@ export function AddAppointmentDialog({ const [patients, setPatients] = useState([]); const [query, setQuery] = useState(""); const [selected, setSelected] = useState(null); + const [date, setDate] = useState(() => new Date(`${TODAY}T00:00:00`)); + const [dateOpen, setDateOpen] = useState(false); const [time, setTime] = useState("09:00"); const [type, setType] = useState(TYPES[0]); const [provider, setProvider] = useState(""); @@ -96,6 +108,8 @@ export function AddAppointmentDialog({ const reset = () => { setQuery(""); setSelected(null); + setDate(new Date(`${TODAY}T00:00:00`)); + setDateOpen(false); setTime("09:00"); setType(TYPES[0]); setProvider(""); @@ -111,6 +125,7 @@ export function AddAppointmentDialog({ fileNumber: selected.fileNumber, name: selected.name, initials: selected.initials, + date: keyOf(date), time, type, provider: provider.trim() || selected.pcp, @@ -206,6 +221,39 @@ export function AddAppointmentDialog({
+
+ Date + + + + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + } + /> + + { + if (d) { + setDate(d); + setDateOpen(false); + } + }} + selected={date} + /> + + +
setTime(event.target.value)} @@ -213,6 +261,9 @@ export function AddAppointmentDialog({ value={time} /> +
+ +
setProvider(event.target.value)} + placeholder="e.g. Dr. Okafor" + value={provider} + /> +
- - - setProvider(event.target.value)} - placeholder="e.g. Dr. Okafor" - value={provider} - /> - diff --git a/frontend/components/appointments/appointments-view.tsx b/frontend/components/appointments/appointments-view.tsx index 39b1303..2df1555 100644 --- a/frontend/components/appointments/appointments-view.tsx +++ b/frontend/components/appointments/appointments-view.tsx @@ -8,7 +8,7 @@ import { Stethoscope, Users, } from "lucide-react"; -import { type ReactNode, useState } from "react"; +import { type ReactNode, useMemo, useState } from "react"; import { AddAppointmentDialog, @@ -23,9 +23,14 @@ import { Card } from "@/components/ui/card"; // All figures here are mock/placeholder data — there is no scheduling backend. // They illustrate the Appointments & Schedule layout. +// Anchor "today" to a fixed date so the mock copy ("Wednesday, June 5") lines up +// across the page, the calendar dialog, and the add dialog. ISO YYYY-MM-DD. +export const TODAY = "2026-06-05"; + type ApptStatus = "confirmed" | "checked-in" | "completed" | "cancelled"; export type Appointment = { + date: string; // ISO YYYY-MM-DD time: string; name: string; initials: string; @@ -58,8 +63,10 @@ const kpis = [ { label: "Utilization", value: "87%", icon: Stethoscope }, ]; -const today: Appointment[] = [ +// Mock schedule spread across June 2026 so the month calendar looks populated. +const seed: Appointment[] = [ { + date: TODAY, time: "09:00", name: "Amina Yusuf", initials: "AY", @@ -68,6 +75,7 @@ const today: Appointment[] = [ status: "completed", }, { + date: TODAY, time: "09:30", name: "Daniel Mensah", initials: "DM", @@ -76,6 +84,7 @@ const today: Appointment[] = [ status: "checked-in", }, { + date: TODAY, time: "10:15", name: "Leila Haddad", initials: "LH", @@ -84,6 +93,7 @@ const today: Appointment[] = [ status: "confirmed", }, { + date: TODAY, time: "11:00", name: "Carlos Rivera", initials: "CR", @@ -92,6 +102,7 @@ const today: Appointment[] = [ status: "confirmed", }, { + date: TODAY, time: "13:30", name: "Priya Nair", initials: "PN", @@ -100,6 +111,7 @@ const today: Appointment[] = [ status: "cancelled", }, { + date: TODAY, time: "14:45", name: "Tom Becker", initials: "TB", @@ -107,32 +119,66 @@ const today: Appointment[] = [ provider: "Dr. Okafor", status: "confirmed", }, -]; - -const upcoming: { day: string; items: Appointment[] }[] = [ { - day: "Tomorrow", - items: [ - { - time: "08:45", - name: "Grace Lin", - initials: "GL", - type: "Follow-up", - provider: "Dr. Stein", - status: "confirmed", - }, - { - time: "10:00", - name: "Omar Farouk", - initials: "OF", - type: "New patient", - provider: "Dr. Okafor", - status: "confirmed", - }, - ], + date: "2026-06-06", + time: "08:45", + name: "Grace Lin", + initials: "GL", + type: "Follow-up", + provider: "Dr. Stein", + status: "confirmed", + }, + { + date: "2026-06-06", + time: "10:00", + name: "Omar Farouk", + initials: "OF", + type: "New patient", + provider: "Dr. Okafor", + status: "confirmed", + }, + { + date: "2026-06-09", + time: "11:30", + name: "Sofia Marin", + initials: "SM", + type: "Consultation", + provider: "Dr. Stein", + status: "confirmed", + }, + { + date: "2026-06-12", + time: "15:00", + name: "Henry Adeyemi", + initials: "HA", + type: "Lab review", + provider: "Dr. Okafor", + status: "confirmed", + }, + { + date: "2026-06-18", + time: "09:15", + name: "Nadia Petrova", + initials: "NP", + type: "Follow-up", + provider: "Dr. Stein", + status: "confirmed", }, ]; +// "2026-06-05" -> "Wednesday, June 5" +function formatDayKey(key: string): string { + return new Date(`${key}T00:00:00`).toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + }); +} + +function byTime(a: Appointment, b: Appointment) { + return a.time.localeCompare(b.time); +} + function Kpi({ label, value, @@ -214,17 +260,40 @@ function Section({ export function AppointmentsView() { const [addOpen, setAddOpen] = useState(false); const [calendarOpen, setCalendarOpen] = useState(false); - const [schedule, setSchedule] = useState(today); + const [appointments, setAppointments] = useState(seed); - // Insert a new (mock) appointment into today's schedule, kept time-sorted. + // Insert a new (mock) appointment at the date/time chosen in the dialog. const addAppointment = (appt: NewAppointment) => { - setSchedule((prev) => - [...prev, { ...appt, status: "confirmed" as const }].sort((a, b) => - a.time.localeCompare(b.time), - ), - ); + setAppointments((prev) => [ + ...prev, + { + date: appt.date, + time: appt.time, + name: appt.name, + initials: appt.initials, + type: appt.type, + provider: appt.provider, + status: "confirmed" as const, + }, + ]); }; + const todayItems = useMemo( + () => appointments.filter((a) => a.date === TODAY).sort(byTime), + [appointments], + ); + + // Group future dates (after TODAY) into day sections, soonest first. + const upcoming = useMemo(() => { + const keys = [ + ...new Set(appointments.map((a) => a.date).filter((d) => d > TODAY)), + ].sort(); + return keys.map((key) => ({ + key, + items: appointments.filter((a) => a.date === key).sort(byTime), + })); + }, [appointments]); + return (
@@ -263,12 +332,18 @@ export function AppointmentsView() { ))}
-
- +
+ {todayItems.length > 0 ? ( + + ) : ( +

+ Nothing scheduled today. +

+ )}
{upcoming.map((group) => ( -
+
))} @@ -280,9 +355,9 @@ export function AppointmentsView() { />
); diff --git a/frontend/components/appointments/calendar-dialog.tsx b/frontend/components/appointments/calendar-dialog.tsx index 2c71c1e..938c2e1 100644 --- a/frontend/components/appointments/calendar-dialog.tsx +++ b/frontend/components/appointments/calendar-dialog.tsx @@ -1,93 +1,228 @@ "use client"; -import { useState } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { useMemo, useState } from "react"; import { type Appointment, ScheduleList, + TODAY, } from "@/components/appointments/appointments-view"; -import { Calendar } from "@/components/ui/calendar"; +import { Button } from "@/components/ui/button"; import { Dialog, - DialogDescription, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; -// 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 WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -const sameDay = (a: Date, b: Date) => - a.getFullYear() === b.getFullYear() && - a.getMonth() === b.getMonth() && - a.getDate() === b.getDate(); +// Local-date ISO key, e.g. "2026-06-05" (avoids UTC drift from toISOString). +const keyOf = (d: Date) => + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String( + d.getDate(), + ).padStart(2, "0")}`; -const fullDate = (d: Date) => - d.toLocaleDateString("en-US", { +const parseKey = (key: string) => new Date(`${key}T00:00:00`); + +const formatDayKey = (key: string) => + parseKey(key).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. +const byTime = (a: Appointment, b: Appointment) => a.time.localeCompare(b.time); + +// Event chip color by status, using semantic tokens. +const chipClass: Record = { + confirmed: "bg-secondary text-secondary-foreground", + "checked-in": "bg-success/15 text-success", + completed: "bg-muted text-muted-foreground", + cancelled: "bg-destructive/15 text-destructive line-through", +}; + +// A Google-Calendar-style month grid in a dialog: a 6×7 grid of day cells with +// color-coded event chips; navigate months and click a day to list its +// appointments. Mock-only — there's no per-date scheduling backend. export function CalendarDialog({ open, onOpenChange, - schedule, + appointments, }: { open: boolean; onOpenChange: (open: boolean) => void; - schedule: Appointment[]; + appointments: Appointment[]; }) { - const [selected, setSelected] = useState(SCHEDULE_DATE); + // First-of-month for the displayed month; defaults to TODAY's month. + const [viewMonth, setViewMonth] = useState(() => { + const t = parseKey(TODAY); + return new Date(t.getFullYear(), t.getMonth(), 1); + }); + const [selectedKey, setSelectedKey] = useState(TODAY); - const dayItems = sameDay(selected, SCHEDULE_DATE) ? schedule : []; + const byDate = useMemo(() => { + const map = new Map(); + for (const a of appointments) { + const list = map.get(a.date) ?? []; + list.push(a); + map.set(a.date, list); + } + return map; + }, [appointments]); + + // 42 cells (6 weeks) starting from the Sunday on/before the 1st. + const cells = useMemo(() => { + const first = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1); + const start = new Date( + first.getFullYear(), + first.getMonth(), + 1 - first.getDay(), + ); + return Array.from( + { length: 42 }, + (_, i) => + new Date(start.getFullYear(), start.getMonth(), start.getDate() + i), + ); + }, [viewMonth]); + + const monthLabel = viewMonth.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + + const selectedItems = (byDate.get(selectedKey) ?? []).slice().sort(byTime); + + const shiftMonth = (delta: number) => + setViewMonth( + (m) => new Date(m.getFullYear(), m.getMonth() + delta, 1), + ); + + const goToday = () => { + const t = parseKey(TODAY); + setViewMonth(new Date(t.getFullYear(), t.getMonth(), 1)); + setSelectedKey(TODAY); + }; return ( - + - Calendar - - Browse the schedule by date. Sample data. - +
+ {monthLabel} +
+ + + +
+
- -
- d && setSelected(d)} - selected={selected} - /> + +
+
+ {WEEKDAYS.map((d) => ( +
+ {d} +
+ ))} +
+
+ {cells.map((date) => { + const key = keyOf(date); + const inMonth = date.getMonth() === viewMonth.getMonth(); + const isToday = key === TODAY; + const isSelected = key === selectedKey; + const items = (byDate.get(key) ?? []).slice().sort(byTime); + return ( + + ); + })} +
-
+

- {fullDate(selected)} + {formatDayKey(selectedKey)}

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

- {dayItems.length > 0 ? ( - + {selectedItems.length > 0 ? ( + ) : ( -
+
No appointments on this day.
)} diff --git a/frontend/components/messages/messages-view.tsx b/frontend/components/messages/messages-view.tsx index 3feecf8..6884d10 100644 --- a/frontend/components/messages/messages-view.tsx +++ b/frontend/components/messages/messages-view.tsx @@ -1,7 +1,7 @@ "use client"; import { Mail, SendHorizonal } from "lucide-react"; -import { useState } from "react"; +import { type FormEvent, useMemo, useState } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; @@ -12,198 +12,287 @@ import { EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; -import { Textarea } from "@/components/ui/textarea"; -import { notify } from "@/lib/toast"; +import { Input } from "@/components/ui/input"; 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). +// All conversations here are mock/placeholder data — there is no messaging +// backend. They illustrate the inbox + chat-thread timeline layout. -type Message = { +type ChatMessage = { id: string; - sender: string; - initials: string; - subject: string; - preview: string; - body: string[]; + direction: "in" | "out"; + text: string; time: string; - read: boolean; }; -const seed: Message[] = [ +type Conversation = { + id: string; + name: string; + initials: string; + role: string; + unread: boolean; + messages: ChatMessage[]; +}; + +const seed: Conversation[] = [ { id: "1", - sender: "Dr. Stein", + name: "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", + role: "Endocrinology", + unread: true, + messages: [ + { + id: "1a", + direction: "in", + text: "The lab results for Amina Yusuf are back — lipid panel and HbA1c are both in range.", + time: "10:24", + }, + { + id: "1b", + direction: "in", + text: "Want me to adjust her plan before the follow-up?", + time: "10:25", + }, ], - time: "10:24", - read: false, }, { id: "2", - sender: "Dr. Okafor", + name: "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", + role: "Family medicine", + unread: true, + messages: [ + { + id: "2a", + direction: "out", + text: "Sent over Daniel Mensah's intake notes — can you take a look?", + time: "08:50", + }, + { + id: "2b", + direction: "in", + text: "Thanks! Added him to tomorrow at 10:00. Were his prior records imported?", + time: "09:12", + }, ], - time: "09:12", - read: false, }, { id: "3", - sender: "Care team", + name: "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.", + role: "Clinic-wide", + unread: false, + messages: [ + { + id: "3a", + direction: "in", + text: "Seasonal vaccine stock has been replenished — slots are open all week.", + time: "Yesterday", + }, + { + id: "3b", + direction: "out", + text: "Great, I'll direct eligible patients to the front desk.", + time: "Yesterday", + }, ], - time: "Yesterday", - read: true, }, { id: "4", - sender: "Reception", + name: "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.", + role: "Front desk", + unread: false, + messages: [ + { + id: "4a", + direction: "in", + text: "Two Friday afternoon appointments were moved to next Monday at the patients' request.", + time: "Mon", + }, ], - time: "Mon", - read: true, }, ]; -export function MessagesView() { - const [messages, setMessages] = useState(seed); - const [selectedId, setSelectedId] = useState(null); - const [reply, setReply] = useState(""); +const now = () => + new Date().toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); - const selected = messages.find((m) => m.id === selectedId) ?? null; +export function MessagesView() { + const [conversations, setConversations] = useState(seed); + const [selectedId, setSelectedId] = useState(null); + const [showUnreadOnly, setShowUnreadOnly] = useState(false); + const [draft, setDraft] = useState(""); + + const unreadCount = conversations.filter((c) => c.unread).length; + const selected = conversations.find((c) => c.id === selectedId) ?? null; + + const visible = useMemo( + () => (showUnreadOnly ? conversations.filter((c) => c.unread) : conversations), + [conversations, showUnreadOnly], + ); const open = (id: string) => { setSelectedId(id); - setReply(""); - setMessages((prev) => - prev.map((m) => (m.id === id ? { ...m, read: true } : m)), + setDraft(""); + setConversations((prev) => + prev.map((c) => (c.id === id ? { ...c, unread: false } : c)), ); }; - const send = () => { - if (!(reply.trim() && selected)) return; - notify.success("Reply sent", `To ${selected.sender}`); - setReply(""); + const send = (event: FormEvent) => { + event.preventDefault(); + const text = draft.trim(); + if (!(text && selected)) return; + const message: ChatMessage = { + id: `${selected.id}-${Date.now()}`, + direction: "out", + text, + time: now(), + }; + setConversations((prev) => + prev.map((c) => + c.id === selected.id + ? { ...c, messages: [...c.messages, message] } + : c, + ), + ); + setDraft(""); }; return (
- {/* Left: inbox list */} + {/* Left: conversation list */}
- {/* Right: reading pane or empty state */} + {/* Right: conversation timeline or empty state */}
{selected ? (
-
-

- {selected.subject} -

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

{para}

+
+ {selected.messages.map((m) => ( +
+
+ {m.text} +
+ + {m.time} + +
))}
-
-