mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
Tasks/notes/prescriptions/appointments/patients UX fixes
- Task dialog: rename Title→Subject, add a "Details" textarea (notes), widen to sm:max-w-lg. - Tasks & Notes: replace split-panes with full-width lists that open a right-side Sheet on click (TaskDetailSheet, NoteDetailSheet), mirroring the Patients table → detail Sheet pattern. - Prescriptions: rows are now clickable and open PrescriptionDetailSheet with the full Rx details (incl. duration + notes). - New prescription dialog: Duration is a dropdown of presets with an "Other" → custom input, plus a Notes textarea; NewPrescription gains notes and threads duration/notes through to the list. - Appointments: header search filtering by patient/type/provider, grouped by date, with an empty state. - Patients: pressing Enter in the search box opens the top match's record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
CalendarDays,
|
||||
Clock,
|
||||
Plus,
|
||||
Search,
|
||||
Stethoscope,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
@@ -19,6 +20,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
// All figures here are mock/placeholder data — there is no scheduling backend.
|
||||
// They illustrate the Appointments & Schedule layout.
|
||||
@@ -261,6 +263,7 @@ export function AppointmentsView() {
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||
const [appointments, setAppointments] = useState<Appointment[]>(seed);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// Insert a new (mock) appointment at the date/time chosen in the dialog.
|
||||
const addAppointment = (appt: NewAppointment) => {
|
||||
@@ -294,6 +297,25 @@ export function AppointmentsView() {
|
||||
}));
|
||||
}, [appointments]);
|
||||
|
||||
const search = query.trim().toLowerCase();
|
||||
|
||||
// While searching, match name/type/provider across every date and group the
|
||||
// hits by date (soonest first) so each section keeps its day header.
|
||||
const results = useMemo(() => {
|
||||
if (!search) return [];
|
||||
const hits = appointments.filter(
|
||||
(a) =>
|
||||
a.name.toLowerCase().includes(search) ||
|
||||
a.type.toLowerCase().includes(search) ||
|
||||
a.provider.toLowerCase().includes(search),
|
||||
);
|
||||
const keys = [...new Set(hits.map((a) => a.date))].sort();
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
items: hits.filter((a) => a.date === key).sort(byTime),
|
||||
}));
|
||||
}, [appointments, search]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-10 px-6 py-10">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
@@ -306,6 +328,15 @@ export function AppointmentsView() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search patient, type, provider"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => setCalendarOpen(true)}
|
||||
@@ -326,27 +357,43 @@ export function AppointmentsView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{kpis.map((k) => (
|
||||
<Kpi key={k.label} {...k} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Section description={formatDayKey(TODAY)} title="Today">
|
||||
{todayItems.length > 0 ? (
|
||||
<ScheduleList items={todayItems} />
|
||||
{search ? (
|
||||
results.length > 0 ? (
|
||||
results.map((group) => (
|
||||
<Section key={group.key} title={formatDayKey(group.key)}>
|
||||
<ScheduleList items={group.items} />
|
||||
</Section>
|
||||
))
|
||||
) : (
|
||||
<p className="rounded-2xl border border-dashed bg-card/20 px-4 py-8 text-center text-muted-foreground text-sm">
|
||||
Nothing scheduled today.
|
||||
No matching appointments.
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{kpis.map((k) => (
|
||||
<Kpi key={k.label} {...k} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{upcoming.map((group) => (
|
||||
<Section key={group.key} title={formatDayKey(group.key)}>
|
||||
<ScheduleList items={group.items} />
|
||||
</Section>
|
||||
))}
|
||||
<Section description={formatDayKey(TODAY)} title="Today">
|
||||
{todayItems.length > 0 ? (
|
||||
<ScheduleList items={todayItems} />
|
||||
) : (
|
||||
<p className="rounded-2xl border border-dashed bg-card/20 px-4 py-8 text-center text-muted-foreground text-sm">
|
||||
Nothing scheduled today.
|
||||
</p>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{upcoming.map((group) => (
|
||||
<Section key={group.key} title={formatDayKey(group.key)}>
|
||||
<ScheduleList items={group.items} />
|
||||
</Section>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<AddAppointmentDialog
|
||||
onAdd={addAppointment}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { NotesEditor } from "@/components/notes/notes-editor";
|
||||
import {
|
||||
Sheet,
|
||||
SheetHeader,
|
||||
SheetPopup,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import type { Note } from "@/lib/notes";
|
||||
|
||||
// Right-side Sheet that holds the rich-text NotesEditor, opened from the Notes
|
||||
// list (mirrors the Patients table → side Sheet pattern). The save/delete logic
|
||||
// stays in NotesView and is passed down. `editorKey` remounts the editor when a
|
||||
// different note (or a fresh draft) is opened.
|
||||
export function NoteDetailSheet({
|
||||
note,
|
||||
editorKey,
|
||||
open,
|
||||
onOpenChange,
|
||||
saving,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: {
|
||||
note: Note | null;
|
||||
editorKey: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
saving: boolean;
|
||||
onSave: (data: { title: string; content: string }) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Sheet onOpenChange={onOpenChange} open={open}>
|
||||
<SheetPopup className="sm:max-w-2xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{note?.id ? "Edit note" : "New note"}</SheetTitle>
|
||||
</SheetHeader>
|
||||
{/* Plain flex container (not SheetPanel) so the editor gets a bounded
|
||||
height and scrolls internally rather than nesting two scroll areas. */}
|
||||
<div className="flex min-h-0 flex-1 flex-col px-6 pt-1 pb-6">
|
||||
{note && (
|
||||
<NotesEditor
|
||||
key={editorKey}
|
||||
note={note}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SheetPopup>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { NotebookPen, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { NotesEditor } from "@/components/notes/notes-editor";
|
||||
import { NoteDetailSheet } from "@/components/notes/note-detail-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Empty,
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
updateNote,
|
||||
} from "@/lib/notes";
|
||||
import { notify } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const newDraft = (): Note => ({
|
||||
id: "",
|
||||
@@ -33,8 +32,9 @@ const newDraft = (): Note => ({
|
||||
|
||||
export function NotesView() {
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
// No auto-selection: with nothing chosen the right pane shows the Empty state.
|
||||
// The note shown in the editor Sheet; null when the Sheet is closed.
|
||||
const [selected, setSelected] = useState<Note | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [draftKey, setDraftKey] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -64,6 +64,12 @@ export function NotesView() {
|
||||
const startNew = () => {
|
||||
setSelected(newDraft());
|
||||
setDraftKey((k) => k + 1);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
const openNote = (note: Note) => {
|
||||
setSelected(note);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
const save = async (data: { title: string; content: string }) => {
|
||||
@@ -92,6 +98,7 @@ export function NotesView() {
|
||||
const list = await listNotes();
|
||||
setNotes(list);
|
||||
setSelected(null);
|
||||
setSheetOpen(false);
|
||||
notify.success("Note deleted");
|
||||
} catch (err) {
|
||||
notify.error(
|
||||
@@ -102,84 +109,76 @@ export function NotesView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4 p-4">
|
||||
{/* Left: note list */}
|
||||
<aside className="flex w-72 shrink-0 flex-col overflow-hidden rounded-2xl border bg-card/30">
|
||||
<div className="flex items-center justify-between gap-2 border-border border-b px-4 py-3">
|
||||
<h1 className="font-semibold text-base tracking-tight">Notes</h1>
|
||||
<Button
|
||||
aria-label="New note"
|
||||
onClick={startNew}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-10">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">Notes</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Clinical notes. Click a note to open it.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2">
|
||||
{loading ? (
|
||||
<p className="px-2 py-1.5 text-muted-foreground text-sm">Loading…</p>
|
||||
) : notes.length === 0 ? (
|
||||
<p className="px-2 py-1.5 text-muted-foreground text-sm">
|
||||
No notes yet.
|
||||
</p>
|
||||
) : (
|
||||
notes.map((n) => (
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full flex-col items-start gap-0.5 rounded-lg px-2 py-2 text-left transition-colors hover:bg-accent",
|
||||
selected?.id === n.id && "bg-accent",
|
||||
)}
|
||||
key={n.id}
|
||||
onClick={() => setSelected(n)}
|
||||
type="button"
|
||||
>
|
||||
<span className="w-full truncate font-medium text-foreground text-sm">
|
||||
{n.title || "Untitled note"}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{new Date(n.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right: editor or empty state */}
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{selected ? (
|
||||
<NotesEditor
|
||||
key={selected.id || `draft-${draftKey}`}
|
||||
note={selected}
|
||||
onDelete={selected.id ? () => remove(selected.id) : undefined}
|
||||
onSave={save}
|
||||
saving={saving}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center rounded-2xl border bg-card/30">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<NotebookPen />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No note selected</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Select a note from the list, or create a new one to start
|
||||
writing.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button onClick={startNew} type="button">
|
||||
<Plus className="size-4" />
|
||||
New note
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
<Button className="rounded-3xl" onClick={startNew} type="button">
|
||||
<Plus className="size-4" />
|
||||
New note
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="rounded-2xl border bg-card/30 px-4 py-10 text-center text-muted-foreground text-sm">
|
||||
Loading…
|
||||
</div>
|
||||
) : notes.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center rounded-2xl border bg-card/30 py-16">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<NotebookPen />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No notes yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Create a note to start writing.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button onClick={startNew} type="button">
|
||||
<Plus className="size-4" />
|
||||
New note
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
|
||||
{notes.map((n) => (
|
||||
<button
|
||||
className="flex w-full flex-col items-start gap-0.5 px-4 py-3 text-left transition-colors hover:bg-accent/50"
|
||||
key={n.id}
|
||||
onClick={() => openNote(n)}
|
||||
type="button"
|
||||
>
|
||||
<span className="w-full truncate font-medium text-foreground text-sm">
|
||||
{n.title || "Untitled note"}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Updated {new Date(n.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NoteDetailSheet
|
||||
editorKey={selected?.id || `draft-${draftKey}`}
|
||||
note={selected}
|
||||
onDelete={selected?.id ? () => remove(selected.id) : undefined}
|
||||
onOpenChange={(o) => {
|
||||
setSheetOpen(o);
|
||||
if (!o) setSelected(null);
|
||||
}}
|
||||
onSave={save}
|
||||
open={sheetOpen}
|
||||
saving={saving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ export function PatientsView() {
|
||||
<Input
|
||||
className="w-full pl-9 sm:w-64"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
// Enter opens the top match's record, like picking it from the table.
|
||||
if (event.key === "Enter" && patients.length > 0) {
|
||||
event.preventDefault();
|
||||
open(patients[0].fileNumber);
|
||||
}
|
||||
}}
|
||||
placeholder="Search name or MRN"
|
||||
value={query}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { listPatients, type Patient } from "@/lib/patients";
|
||||
import { notify } from "@/lib/toast";
|
||||
|
||||
@@ -27,6 +28,7 @@ export type NewPrescription = {
|
||||
dose: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
const FREQUENCIES = [
|
||||
@@ -37,6 +39,20 @@ const FREQUENCIES = [
|
||||
"As needed",
|
||||
];
|
||||
|
||||
// Preset courses for the Duration dropdown; "Other" reveals a free-text input.
|
||||
const DURATIONS = [
|
||||
"3 days",
|
||||
"5 days",
|
||||
"7 days",
|
||||
"10 days",
|
||||
"14 days",
|
||||
"1 month",
|
||||
"3 months",
|
||||
"Ongoing",
|
||||
"Other",
|
||||
];
|
||||
const OTHER_DURATION = "Other";
|
||||
|
||||
const controlClass =
|
||||
"h-9 w-full rounded-3xl border border-transparent bg-input/50 px-3 text-sm text-foreground outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30";
|
||||
|
||||
@@ -112,7 +128,9 @@ export function AddPrescriptionDialog({
|
||||
const [medication, setMedication] = useState("");
|
||||
const [dose, setDose] = useState("");
|
||||
const [frequency, setFrequency] = useState(FREQUENCIES[0]);
|
||||
const [duration, setDuration] = useState("");
|
||||
const [durationChoice, setDurationChoice] = useState(DURATIONS[0]);
|
||||
const [durationCustom, setDurationCustom] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
|
||||
// Load patients lazily when the dialog opens (for the quick search).
|
||||
useEffect(() => {
|
||||
@@ -151,7 +169,9 @@ export function AddPrescriptionDialog({
|
||||
setMedication("");
|
||||
setDose("");
|
||||
setFrequency(FREQUENCIES[0]);
|
||||
setDuration("");
|
||||
setDurationChoice(DURATIONS[0]);
|
||||
setDurationCustom("");
|
||||
setNotes("");
|
||||
};
|
||||
|
||||
const submit = (event: FormEvent) => {
|
||||
@@ -164,6 +184,12 @@ export function AddPrescriptionDialog({
|
||||
notify.error("Add a medication", "Enter the medication name.");
|
||||
return;
|
||||
}
|
||||
const duration =
|
||||
durationChoice === OTHER_DURATION ? durationCustom.trim() : durationChoice;
|
||||
if (durationChoice === OTHER_DURATION && !duration) {
|
||||
notify.error("Add a duration", "Enter the custom duration.");
|
||||
return;
|
||||
}
|
||||
onAdd({
|
||||
fileNumber: selected.fileNumber,
|
||||
name: selected.name,
|
||||
@@ -171,7 +197,8 @@ export function AddPrescriptionDialog({
|
||||
medication: medication.trim(),
|
||||
dose: dose.trim(),
|
||||
frequency,
|
||||
duration: duration.trim(),
|
||||
duration,
|
||||
notes: notes.trim(),
|
||||
});
|
||||
notify.success("Prescription added", `${medication.trim()} for ${selected.name}`);
|
||||
reset();
|
||||
@@ -295,10 +322,33 @@ export function AddPrescriptionDialog({
|
||||
</div>
|
||||
|
||||
<Field label="Duration">
|
||||
<Input
|
||||
onChange={(event) => setDuration(event.target.value)}
|
||||
placeholder="e.g. 7 days"
|
||||
value={duration}
|
||||
<select
|
||||
className={controlClass}
|
||||
onChange={(event) => setDurationChoice(event.target.value)}
|
||||
value={durationChoice}
|
||||
>
|
||||
{DURATIONS.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
{d}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{durationChoice === OTHER_DURATION && (
|
||||
<Input
|
||||
autoFocus
|
||||
onChange={(event) => setDurationCustom(event.target.value)}
|
||||
placeholder="e.g. 21 days"
|
||||
value={durationCustom}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field label="Notes">
|
||||
<Textarea
|
||||
onChange={(event) => setNotes(event.target.value)}
|
||||
placeholder="Instructions, e.g. take with food in the morning"
|
||||
rows={3}
|
||||
value={notes}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Sheet,
|
||||
SheetHeader,
|
||||
SheetPanel,
|
||||
SheetPopup,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import type { Prescription, RxStatus } from "@/components/prescriptions/prescriptions-view";
|
||||
|
||||
const statusVariant: Record<
|
||||
RxStatus,
|
||||
"default" | "secondary" | "destructive" | "outline"
|
||||
> = {
|
||||
active: "default",
|
||||
completed: "outline",
|
||||
expired: "destructive",
|
||||
};
|
||||
|
||||
const statusLabel: Record<RxStatus, string> = {
|
||||
active: "Active",
|
||||
completed: "Completed",
|
||||
expired: "Expired",
|
||||
};
|
||||
|
||||
// Right-side Sheet showing one prescription's full detail, opened by clicking a
|
||||
// row in the Prescriptions list (mirrors the Patients table → side Sheet
|
||||
// pattern). Prescriptions live in local state, so the record is passed in.
|
||||
export function PrescriptionDetailSheet({
|
||||
rx,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
rx: Prescription | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<Sheet onOpenChange={onOpenChange} open={open}>
|
||||
<SheetPopup className="sm:max-w-xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{rx ? `${rx.medication}${rx.dose ? ` · ${rx.dose}` : ""}` : "Prescription"}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetPanel className="min-h-0 flex-1">
|
||||
{rx && (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="size-10">
|
||||
<AvatarFallback>{rx.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium text-foreground text-sm">
|
||||
{rx.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
File #{rx.fileNumber}
|
||||
</span>
|
||||
</div>
|
||||
<Badge className="ms-auto" variant={statusVariant[rx.status]}>
|
||||
{statusLabel[rx.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-[7rem_1fr] gap-x-3 gap-y-2 text-sm">
|
||||
<dt className="text-muted-foreground">Medication</dt>
|
||||
<dd className="text-foreground">{rx.medication}</dd>
|
||||
{rx.dose && (
|
||||
<>
|
||||
<dt className="text-muted-foreground">Dose</dt>
|
||||
<dd className="text-foreground">{rx.dose}</dd>
|
||||
</>
|
||||
)}
|
||||
<dt className="text-muted-foreground">Frequency</dt>
|
||||
<dd className="text-foreground">{rx.frequency}</dd>
|
||||
<dt className="text-muted-foreground">Duration</dt>
|
||||
<dd className="text-foreground">{rx.duration || "—"}</dd>
|
||||
<dt className="text-muted-foreground">Prescriber</dt>
|
||||
<dd className="text-foreground">{rx.prescriber}</dd>
|
||||
<dt className="text-muted-foreground">Date</dt>
|
||||
<dd className="text-foreground">{rx.date}</dd>
|
||||
<dt className="text-muted-foreground">Status</dt>
|
||||
<dd className="text-foreground">{statusLabel[rx.status]}</dd>
|
||||
</dl>
|
||||
|
||||
{rx.notes && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-muted-foreground text-xs">Notes</span>
|
||||
<p className="whitespace-pre-wrap text-foreground text-sm leading-relaxed">
|
||||
{rx.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SheetPanel>
|
||||
</SheetPopup>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AddPrescriptionDialog,
|
||||
type NewPrescription,
|
||||
} from "@/components/prescriptions/add-prescription-dialog";
|
||||
import { PrescriptionDetailSheet } from "@/components/prescriptions/prescription-detail-sheet";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,9 +16,9 @@ import { Card } from "@/components/ui/card";
|
||||
// All figures here are mock/placeholder data — there is no prescriptions backend.
|
||||
// They illustrate the Prescriptions layout.
|
||||
|
||||
type RxStatus = "active" | "completed" | "expired";
|
||||
export type RxStatus = "active" | "completed" | "expired";
|
||||
|
||||
type Prescription = {
|
||||
export type Prescription = {
|
||||
fileNumber: string;
|
||||
name: string;
|
||||
initials: string;
|
||||
@@ -27,6 +28,8 @@ type Prescription = {
|
||||
prescriber: string;
|
||||
date: string;
|
||||
status: RxStatus;
|
||||
duration?: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const statusVariant: Record<
|
||||
@@ -132,9 +135,20 @@ function Kpi({
|
||||
);
|
||||
}
|
||||
|
||||
function RxRow({ rx }: { rx: Prescription }) {
|
||||
function RxRow({ rx, onOpen }: { rx: Prescription; onOpen: () => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-accent/50"
|
||||
onClick={onOpen}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onOpen();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Avatar className="size-8">
|
||||
<AvatarFallback>{rx.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -183,6 +197,13 @@ function Section({
|
||||
export function PrescriptionsView() {
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [list, setList] = useState<Prescription[]>(initial);
|
||||
const [selected, setSelected] = useState<Prescription | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
|
||||
const openRx = (rx: Prescription) => {
|
||||
setSelected(rx);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
// Insert a new (mock) prescription at the top of the list, marked active.
|
||||
const addPrescription = (rx: NewPrescription) => {
|
||||
@@ -194,6 +215,8 @@ export function PrescriptionsView() {
|
||||
medication: rx.medication,
|
||||
dose: rx.dose,
|
||||
frequency: rx.frequency,
|
||||
duration: rx.duration || undefined,
|
||||
notes: rx.notes || undefined,
|
||||
prescriber: "Dr. Okafor",
|
||||
date: new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
@@ -234,7 +257,11 @@ export function PrescriptionsView() {
|
||||
<Section description="Most recent first" title="Recent prescriptions">
|
||||
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
|
||||
{list.map((rx) => (
|
||||
<RxRow key={rx.fileNumber + rx.medication + rx.date} rx={rx} />
|
||||
<RxRow
|
||||
key={rx.fileNumber + rx.medication + rx.date}
|
||||
onOpen={() => openRx(rx)}
|
||||
rx={rx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
@@ -244,6 +271,12 @@ export function PrescriptionsView() {
|
||||
onOpenChange={setAddOpen}
|
||||
open={addOpen}
|
||||
/>
|
||||
|
||||
<PrescriptionDetailSheet
|
||||
onOpenChange={setSheetOpen}
|
||||
open={sheetOpen}
|
||||
rx={selected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetHeader,
|
||||
SheetPanel,
|
||||
SheetPopup,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Priority, Task } from "@/components/tasks/tasks-view";
|
||||
|
||||
const priorityVariant: Record<Priority, "destructive" | "secondary" | "outline"> =
|
||||
{
|
||||
high: "destructive",
|
||||
medium: "secondary",
|
||||
low: "outline",
|
||||
};
|
||||
|
||||
const priorityLabel: Record<Priority, string> = {
|
||||
high: "High",
|
||||
medium: "Medium",
|
||||
low: "Low",
|
||||
};
|
||||
|
||||
// Right-side Sheet showing a single task's full detail, opened from the Tasks
|
||||
// list (mirrors the Patients table → PatientDetailSheet pattern). The task is
|
||||
// passed in directly since tasks live in local state — no async fetch.
|
||||
export function TaskDetailSheet({
|
||||
task,
|
||||
open,
|
||||
onOpenChange,
|
||||
onToggle,
|
||||
}: {
|
||||
task: Task | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onToggle: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Sheet onOpenChange={onOpenChange} open={open}>
|
||||
<SheetPopup className="sm:max-w-xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle
|
||||
className={cn(task?.done && "text-muted-foreground line-through")}
|
||||
>
|
||||
{task?.title ?? "Task"}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetPanel className="min-h-0 flex-1">
|
||||
{task && (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<Badge variant={priorityVariant[task.priority]}>
|
||||
{priorityLabel[task.priority]} priority
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-[6rem_1fr] gap-x-3 gap-y-2 text-sm">
|
||||
<dt className="text-muted-foreground">Status</dt>
|
||||
<dd className="text-foreground">
|
||||
{task.done ? "Completed" : "Open"}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">Assignee</dt>
|
||||
<dd className="text-foreground">{task.assignee}</dd>
|
||||
<dt className="text-muted-foreground">Due</dt>
|
||||
<dd className="text-foreground">{task.due}</dd>
|
||||
{task.patient && (
|
||||
<>
|
||||
<dt className="text-muted-foreground">Patient</dt>
|
||||
<dd className="text-foreground">{task.patient}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{task.notes && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-muted-foreground text-xs">Details</span>
|
||||
<p className="whitespace-pre-wrap text-foreground text-sm leading-relaxed">
|
||||
{task.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => onToggle(task.id)}
|
||||
type="button"
|
||||
variant={task.done ? "outline" : "default"}
|
||||
>
|
||||
{task.done ? "Reopen task" : "Mark complete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SheetPanel>
|
||||
</SheetPopup>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ListTodo, Plus } from "lucide-react";
|
||||
import { Check, Plus } from "lucide-react";
|
||||
import { type FormEvent, type ReactNode, useMemo, useState } from "react";
|
||||
|
||||
import { TaskDetailSheet } from "@/components/tasks/task-detail-sheet";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -15,23 +16,17 @@ import {
|
||||
DialogPopup,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { notify } from "@/lib/toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// All tasks here are mock/placeholder data — there is no tasks backend. They
|
||||
// illustrate a care-team to-do board.
|
||||
|
||||
type Priority = "high" | "medium" | "low";
|
||||
export type Priority = "high" | "medium" | "low";
|
||||
|
||||
type Task = {
|
||||
export type Task = {
|
||||
id: string;
|
||||
title: string;
|
||||
assignee: string;
|
||||
@@ -109,7 +104,7 @@ function CheckButton({
|
||||
aria-label={done ? "Mark as not done" : "Mark as done"}
|
||||
aria-pressed={done}
|
||||
className={cn(
|
||||
"mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-md border transition-colors",
|
||||
"flex size-5 shrink-0 items-center justify-center rounded-md border transition-colors",
|
||||
done
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-input hover:border-ring",
|
||||
@@ -144,12 +139,14 @@ function AddTaskDialog({
|
||||
onAdd: (task: Omit<Task, "id" | "done">) => void;
|
||||
}) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [assignee, setAssignee] = useState("");
|
||||
const [due, setDue] = useState("");
|
||||
const [priority, setPriority] = useState<Priority>("medium");
|
||||
|
||||
const reset = () => {
|
||||
setTitle("");
|
||||
setNotes("");
|
||||
setAssignee("");
|
||||
setDue("");
|
||||
setPriority("medium");
|
||||
@@ -158,11 +155,12 @@ function AddTaskDialog({
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!title.trim()) {
|
||||
notify.error("Add a title", "Describe the task first.");
|
||||
notify.error("Add a subject", "Describe the task first.");
|
||||
return;
|
||||
}
|
||||
onAdd({
|
||||
title: title.trim(),
|
||||
notes: notes.trim() || undefined,
|
||||
assignee: assignee.trim() || "Unassigned",
|
||||
due: due.trim() || "No due date",
|
||||
priority,
|
||||
@@ -180,7 +178,7 @@ function AddTaskDialog({
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<DialogPopup className="sm:max-w-md">
|
||||
<DialogPopup className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New task</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -190,7 +188,7 @@ function AddTaskDialog({
|
||||
|
||||
<form className="contents" onSubmit={submit}>
|
||||
<DialogPanel className="flex flex-col gap-4">
|
||||
<Field label="Title">
|
||||
<Field label="Subject">
|
||||
<Input
|
||||
autoFocus
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
@@ -198,6 +196,14 @@ function AddTaskDialog({
|
||||
value={title}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Details — what needs to be done">
|
||||
<Textarea
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Describe what needs to happen, any context, links…"
|
||||
rows={4}
|
||||
value={notes}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Assignee">
|
||||
<Input
|
||||
@@ -242,6 +248,7 @@ function AddTaskDialog({
|
||||
export function TasksView() {
|
||||
const [tasks, setTasks] = useState<Task[]>(seed);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [filter, setFilter] = useState<Filter>("all");
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
|
||||
@@ -258,6 +265,11 @@ export function TasksView() {
|
||||
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
|
||||
);
|
||||
|
||||
const openTask = (id: string) => {
|
||||
setSelectedId(id);
|
||||
setSheetOpen(true);
|
||||
};
|
||||
|
||||
const addTask = (task: Omit<Task, "id" | "done">) =>
|
||||
setTasks((prev) => [
|
||||
{ ...task, id: `t-${Date.now()}`, done: false },
|
||||
@@ -265,149 +277,83 @@ export function TasksView() {
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full gap-4 p-4">
|
||||
{/* Left: task list */}
|
||||
<aside className="flex w-80 shrink-0 flex-col overflow-hidden rounded-2xl border bg-card/30">
|
||||
<div className="flex items-center justify-between gap-2 border-border border-b px-4 py-3">
|
||||
<h1 className="font-semibold text-base tracking-tight">Tasks</h1>
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-10">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-2xl tracking-tight">Tasks</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Care-team to-dos. Click a task to see its details. Sample data.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => setAddOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
New task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-1 rounded-2xl border bg-card/30 p-1 sm:w-fit">
|
||||
{(["all", "open", "done"] as Filter[]).map((f) => (
|
||||
<Button
|
||||
aria-label="New task"
|
||||
onClick={() => setAddOpen(true)}
|
||||
size="icon-sm"
|
||||
className="flex-1 capitalize sm:flex-none"
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
variant={filter === f ? "secondary" : "ghost"}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
{f}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 border-border border-b px-2 py-2">
|
||||
{(["all", "open", "done"] as Filter[]).map((f) => (
|
||||
<Button
|
||||
className="flex-1 capitalize"
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={filter === f ? "secondary" : "ghost"}
|
||||
>
|
||||
{f}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2">
|
||||
{visible.length === 0 ? (
|
||||
<p className="px-2 py-1.5 text-muted-foreground text-sm">
|
||||
No tasks here.
|
||||
</p>
|
||||
) : (
|
||||
visible.map((t) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-2 rounded-lg px-2 py-2 transition-colors hover:bg-accent/50",
|
||||
selected?.id === t.id && "bg-accent",
|
||||
)}
|
||||
key={t.id}
|
||||
<div className="divide-y divide-border overflow-hidden rounded-2xl border bg-card/30">
|
||||
{visible.length === 0 ? (
|
||||
<p className="px-4 py-10 text-center text-muted-foreground text-sm">
|
||||
No tasks here.
|
||||
</p>
|
||||
) : (
|
||||
visible.map((t) => (
|
||||
<div className="flex items-center gap-3 px-4 py-3" key={t.id}>
|
||||
<CheckButton done={t.done} onClick={() => toggle(t.id)} />
|
||||
<button
|
||||
className="flex min-w-0 flex-1 flex-col text-left"
|
||||
onClick={() => openTask(t.id)}
|
||||
type="button"
|
||||
>
|
||||
<CheckButton done={t.done} onClick={() => toggle(t.id)} />
|
||||
<button
|
||||
className="flex min-w-0 flex-1 flex-col text-left"
|
||||
onClick={() => setSelectedId(t.id)}
|
||||
type="button"
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-sm",
|
||||
t.done
|
||||
? "text-muted-foreground line-through"
|
||||
: "font-medium text-foreground",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-sm",
|
||||
t.done
|
||||
? "text-muted-foreground line-through"
|
||||
: "font-medium text-foreground",
|
||||
)}
|
||||
>
|
||||
{t.title}
|
||||
</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{t.assignee} · {t.due}
|
||||
</span>
|
||||
</button>
|
||||
<Badge className="shrink-0" variant={priorityVariant[t.priority]}>
|
||||
{priorityLabel[t.priority]}
|
||||
</Badge>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right: task detail or empty state */}
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{selected ? (
|
||||
<div className="flex h-full flex-col gap-4 overflow-y-auto rounded-2xl border bg-card/30 p-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h2
|
||||
className={cn(
|
||||
"font-semibold text-foreground text-xl tracking-tight",
|
||||
selected.done && "text-muted-foreground line-through",
|
||||
)}
|
||||
>
|
||||
{selected.title}
|
||||
</h2>
|
||||
<Badge variant={priorityVariant[selected.priority]}>
|
||||
{priorityLabel[selected.priority]}
|
||||
{t.title}
|
||||
</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{t.assignee} · {t.due}
|
||||
</span>
|
||||
</button>
|
||||
<Badge className="shrink-0" variant={priorityVariant[t.priority]}>
|
||||
{priorityLabel[t.priority]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<dl className="grid grid-cols-[6rem_1fr] gap-x-3 gap-y-2 text-sm">
|
||||
<dt className="text-muted-foreground">Status</dt>
|
||||
<dd className="text-foreground">
|
||||
{selected.done ? "Completed" : "Open"}
|
||||
</dd>
|
||||
<dt className="text-muted-foreground">Assignee</dt>
|
||||
<dd className="text-foreground">{selected.assignee}</dd>
|
||||
<dt className="text-muted-foreground">Due</dt>
|
||||
<dd className="text-foreground">{selected.due}</dd>
|
||||
{selected.patient && (
|
||||
<>
|
||||
<dt className="text-muted-foreground">Patient</dt>
|
||||
<dd className="text-foreground">{selected.patient}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
{selected.notes && (
|
||||
<p className="text-foreground text-sm leading-relaxed">
|
||||
{selected.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => toggle(selected.id)}
|
||||
type="button"
|
||||
variant={selected.done ? "outline" : "default"}
|
||||
>
|
||||
{selected.done ? "Reopen task" : "Mark complete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center rounded-2xl border bg-card/30">
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<ListTodo />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No task selected</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Select a task to see its details, or create a new one.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AddTaskDialog onAdd={addTask} onOpenChange={setAddOpen} open={addOpen} />
|
||||
|
||||
<TaskDetailSheet
|
||||
onOpenChange={setSheetOpen}
|
||||
onToggle={toggle}
|
||||
open={sheetOpen}
|
||||
task={selected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user