mirror of
https://github.com/temetro/temetro.git
synced 2026-08-08 17:53:15 +00:00
frontend: UI fixes + settings/patients features
- Record history: replace fragile COSS Timeline separator math with a continuous-rail list so every audited change renders in full (RTL-safe). - Prescriptions: new reusable COSS DatePicker (Popover + Calendar) replaces raw <input type="date"> for Start/End date. - Update banner: rebuilt with COSS Alert variant="warning"; pinned physical bottom-right so it stays bottom-right under Arabic RTL. - AI setup notice: thin attached warning banner with clearer copy; now shown above the input in active chats too, not just the empty state. - Settings profile: wire the previously dead Specialty (Select) and Professional links (editable rows) into the persisted preferences. - Patients: add a status filter and broaden search to conditions/allergies. - Sidebar user menu: quick language switch submenu (RTL already wired). - ai-elements: fix a subset of Base UI type drift (22 -> 12; remainder is cmdk->Autocomplete migration drift kept behind ignoreBuildErrors). - i18n: new keys added across all five locales (en/de/fr/ar/so). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,16 +21,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Sparkline } from "@/components/chat/sparkline";
|
||||
import { AttachmentsSection } from "@/components/patients/patient-files";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
Timeline,
|
||||
TimelineContent,
|
||||
TimelineDate,
|
||||
TimelineHeader,
|
||||
TimelineIndicator,
|
||||
TimelineItem,
|
||||
TimelineSeparator,
|
||||
TimelineTitle,
|
||||
} from "@/components/ui/timeline";
|
||||
import {
|
||||
type ActivityEntityType,
|
||||
type ActivityEntry,
|
||||
@@ -182,36 +172,41 @@ function RecordHistory({ fileNumber }: { fileNumber: string }) {
|
||||
{t("patientCard.history.empty")}
|
||||
</p>
|
||||
) : (
|
||||
// Every entry is a past, audited event, so mark them all completed
|
||||
// (filled indicators) by seeding the active step past the last item.
|
||||
<Timeline defaultValue={entries.length}>
|
||||
// A plain vertical rail so EVERY audited change renders in full — the
|
||||
// icon column draws a connector that stretches to the next entry, and
|
||||
// the flex layout mirrors correctly under RTL.
|
||||
<ol className="space-y-0">
|
||||
{entries.map((e, i) => {
|
||||
const Icon = historyIcon[e.entityType] ?? Pencil;
|
||||
const isLast = i === entries.length - 1;
|
||||
return (
|
||||
<TimelineItem
|
||||
className="group-data-[orientation=vertical]/timeline:ms-10"
|
||||
key={e.id}
|
||||
step={i + 1}
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5" />
|
||||
<TimelineTitle className="mt-0.5">
|
||||
{e.actorName}
|
||||
</TimelineTitle>
|
||||
<TimelineIndicator className="group-data-[orientation=vertical]/timeline:-left-7 flex size-6 items-center justify-center border-none bg-primary/10 text-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground">
|
||||
<li className="flex gap-3" key={e.id}>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<Icon size={14} />
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
<TimelineContent>
|
||||
{e.action}
|
||||
<TimelineDate className="mt-1 mb-0">
|
||||
</span>
|
||||
{!isLast && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="my-1 w-px flex-1 bg-primary/15"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`min-w-0 flex-1 ${isLast ? "" : "pb-5"}`}
|
||||
>
|
||||
<p className="font-medium text-foreground text-sm">
|
||||
{e.actorName}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">{e.action}</p>
|
||||
<time className="mt-0.5 block font-medium text-muted-foreground text-xs">
|
||||
{new Date(e.createdAt).toLocaleString()}
|
||||
</TimelineDate>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
</time>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
</ol>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -13,8 +13,23 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ListPagination } from "@/components/ui/list-pagination";
|
||||
import {
|
||||
Select,
|
||||
SelectItem,
|
||||
SelectPopup,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { listPatients, type Patient } from "@/lib/patients";
|
||||
|
||||
type StatusFilter = "all" | Patient["status"];
|
||||
const STATUS_FILTERS: StatusFilter[] = [
|
||||
"all",
|
||||
"active",
|
||||
"inpatient",
|
||||
"discharged",
|
||||
];
|
||||
|
||||
// Rows shown per page on the patients table before paginating.
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@@ -32,6 +47,7 @@ const statusVariant: Record<Patient["status"], BadgeVariant> = {
|
||||
export function PatientsView() {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
// Bumped on open so the create dialog remounts with a fresh file # / form.
|
||||
@@ -69,9 +85,18 @@ export function PatientsView() {
|
||||
}, []);
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const patients = allPatients.filter(
|
||||
(p) => !q || p.name.toLowerCase().includes(q) || p.fileNumber.includes(q)
|
||||
);
|
||||
// Search matches name, MRN, problem/condition labels, and allergy substances;
|
||||
// the status filter narrows to one clinical status.
|
||||
const patients = allPatients.filter((p) => {
|
||||
if (statusFilter !== "all" && p.status !== statusFilter) return false;
|
||||
if (!q) return true;
|
||||
return (
|
||||
p.name.toLowerCase().includes(q) ||
|
||||
p.fileNumber.includes(q) ||
|
||||
p.problems.some((problem) => problem.label.toLowerCase().includes(q)) ||
|
||||
p.allergies.some((a) => a.substance.toLowerCase().includes(q))
|
||||
);
|
||||
});
|
||||
|
||||
// Client-side pagination over the filtered list (10/page). Searching resets to
|
||||
// the first page (done in the search handler); `page` is clamped at render so a
|
||||
@@ -133,6 +158,29 @@ export function PatientsView() {
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
setStatusFilter((value ?? "all") as StatusFilter);
|
||||
setPage(1);
|
||||
}}
|
||||
value={statusFilter}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t("patients.filterStatus")}
|
||||
className="w-full sm:w-40"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectPopup>
|
||||
{STATUS_FILTERS.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value === "all"
|
||||
? t("patients.allStatuses")
|
||||
: t(`patients.status.${value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
onClick={() => setImportOpen(true)}
|
||||
|
||||
Reference in New Issue
Block a user