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 0000000..e254b94 Binary files /dev/null and b/frontend/app/apple-icon.png differ diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/frontend/app/favicon.ico and /dev/null differ diff --git a/frontend/app/icon.png b/frontend/app/icon.png new file mode 100644 index 0000000..45bbdd6 Binary files /dev/null and b/frontend/app/icon.png differ 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} + +
))}
-
-