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 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-06 17:37:17 +03:00
parent 67137c722d
commit 0f10aafa43
12 changed files with 990 additions and 15 deletions
@@ -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 (
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
{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<Appointment[]>(today);
// Insert a new (mock) appointment into today's schedule, kept time-sorted.
@@ -227,14 +236,25 @@ export function AppointmentsView() {
Today&apos;s clinic schedule and what&apos;s coming up. Sample data.
</p>
</div>
<Button
className="rounded-3xl"
onClick={() => setAddOpen(true)}
type="button"
>
<Plus className="size-4" />
Add
</Button>
<div className="flex items-center gap-2">
<Button
className="rounded-3xl"
onClick={() => setCalendarOpen(true)}
type="button"
variant="outline"
>
<CalendarDays className="size-4" />
Calendar
</Button>
<Button
className="rounded-3xl"
onClick={() => setAddOpen(true)}
type="button"
>
<Plus className="size-4" />
Add
</Button>
</div>
</div>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
@@ -258,6 +278,12 @@ export function AppointmentsView() {
onOpenChange={setAddOpen}
open={addOpen}
/>
<CalendarDialog
onOpenChange={setCalendarOpen}
open={calendarOpen}
schedule={schedule}
/>
</div>
);
}
@@ -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<Date>(SCHEDULE_DATE);
const dayItems = sameDay(selected, SCHEDULE_DATE) ? schedule : [];
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogPopup className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Calendar</DialogTitle>
<DialogDescription>
Browse the schedule by date. Sample data.
</DialogDescription>
</DialogHeader>
<DialogPanel className="flex flex-col gap-5 sm:flex-row sm:gap-6">
<div className="flex justify-center sm:block">
<Calendar
className="rounded-2xl border bg-card/30 p-3"
defaultMonth={SCHEDULE_DATE}
mode="single"
modifiers={{ scheduled: SCHEDULE_DATE }}
modifiersClassNames={{
scheduled: "[&_button]:ring-2 [&_button]:ring-primary",
}}
onSelect={(d) => d && setSelected(d)}
selected={selected}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-3">
<div>
<h3 className="font-medium text-foreground text-sm">
{fullDate(selected)}
</h3>
<p className="text-muted-foreground text-xs">
{dayItems.length === 1
? "1 appointment"
: `${dayItems.length} appointments`}
</p>
</div>
{dayItems.length > 0 ? (
<ScheduleList items={dayItems} />
) : (
<div className="flex flex-1 items-center justify-center rounded-2xl border border-dashed bg-card/20 px-4 py-10 text-center text-muted-foreground text-sm">
No appointments on this day.
</div>
)}
</div>
</DialogPanel>
</DialogPopup>
</Dialog>
);
}