frontend: clinical UI fixes + chat/composer polish

- chat composer: attach works with text (sr-only file input); lighter
  bordered input box
- messages: defer "+" menu actions past close so file/appointment pickers fire
- tasks: top-level New task button
- chat: condensed appointment card with "View in calendar" deep-link;
  appointments page auto-opens the month calendar from ?calendar=&date=
- lab: work-queue rows expand to show task detail + complete
- inventory: clickable item detail dialog
- pharmacy: Dispense action records a dispense + "Recently dispensed" feed;
  expiring badge uses endDate and only flags <=2 days left
- prescriptions: keyboard nav in patient search, inventory-backed medication
  combobox (free-text fallback), optional start/end dates; thread dates through
- sidebar: whole nav scrolls as one
- i18n keys for all new strings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-16 20:45:30 +03:00
parent f0fe501d62
commit 307cc3cc64
18 changed files with 866 additions and 101 deletions
@@ -9,6 +9,7 @@ import {
Stethoscope,
Users,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -192,8 +193,14 @@ function Section({
export function AppointmentsView() {
const { t } = useTranslation();
// Deep-link from the AI chat appointment card: `?calendar=1&date=YYYY-MM-DD`
// opens the month calendar on that date.
const searchParams = useSearchParams();
const dateParam = searchParams.get("date");
const [addOpen, setAddOpen] = useState(false);
const [calendarOpen, setCalendarOpen] = useState(false);
const [calendarOpen, setCalendarOpen] = useState(
() => searchParams.get("calendar") != null,
);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [query, setQuery] = useState("");
const [selectedAppt, setSelectedAppt] = useState<Appointment | null>(null);
@@ -381,6 +388,7 @@ export function AppointmentsView() {
<CalendarDialog
appointments={appointments}
initialDate={dateParam}
onOpenChange={setCalendarOpen}
open={calendarOpen}
/>
@@ -1,7 +1,7 @@
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
@@ -53,18 +53,32 @@ export function CalendarDialog({
open,
onOpenChange,
appointments,
initialDate,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
appointments: Appointment[];
// Optional deep-link target (YYYY-MM-DD): when set, the calendar opens on this
// date's month with the day selected (e.g. from the AI chat appointment card).
initialDate?: string | null;
}) {
const { t } = useTranslation();
// First-of-month for the displayed month; defaults to TODAY's month.
const startKey =
initialDate && /^\d{4}-\d{2}-\d{2}$/.test(initialDate) ? initialDate : TODAY;
// First-of-month for the displayed month; defaults to the deep-link/TODAY month.
const [viewMonth, setViewMonth] = useState<Date>(() => {
const t = parseKey(TODAY);
return new Date(t.getFullYear(), t.getMonth(), 1);
const d = parseKey(startKey);
return new Date(d.getFullYear(), d.getMonth(), 1);
});
const [selectedKey, setSelectedKey] = useState<string>(TODAY);
const [selectedKey, setSelectedKey] = useState<string>(startKey);
// When opened via a deep-link date, jump the view to that month/day.
useEffect(() => {
if (!open || !initialDate || !/^\d{4}-\d{2}-\d{2}$/.test(initialDate)) return;
const d = parseKey(initialDate);
setViewMonth(new Date(d.getFullYear(), d.getMonth(), 1));
setSelectedKey(initialDate);
}, [open, initialDate]);
const byDate = useMemo(() => {
const map = new Map<string, Appointment[]>();