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
+46
View File
@@ -0,0 +1,46 @@
import { apiFetch } from "@/lib/api-client";
// A dispensing event — the record of a medication handed to a patient at the
// pharmacy. Mirrors the backend `src/types/dispense.ts`. Scoped to the active
// clinic; patient + dispenser fields are denormalized for display.
export type Dispense = {
id: string;
fileNumber: string;
name: string;
initials: string;
medication: string;
dose: string;
quantity: number;
unit: string;
prescriptionId: string | null;
dispensedBy: string | null;
dispensedByName: string;
dispensedAt: string; // ISO timestamp
notes: string | null;
createdAt: string;
};
// The fields the dispense action collects; the backend fills the dispenser
// identity (from the signed-in user) and the timestamp.
export type DispenseInput = {
fileNumber: string;
name: string;
initials?: string;
medication: string;
dose?: string;
quantity?: number;
unit?: string;
prescriptionId?: string | null;
notes?: string | null;
};
export function listDispenses(): Promise<Dispense[]> {
return apiFetch<Dispense[]>("/api/dispenses");
}
export function createDispense(input: DispenseInput): Promise<Dispense> {
return apiFetch<Dispense>("/api/dispenses", {
method: "POST",
body: JSON.stringify(input),
});
}
+40 -7
View File
@@ -385,7 +385,8 @@
"prescriber": "Prescriber",
"date": "Date",
"status": "Status",
"notes": "Notes"
"notes": "Notes",
"courseDates": "Course"
},
"dialog": {
"title": "New prescription",
@@ -413,7 +414,16 @@
"needMedBody": "Enter the medication name.",
"needDurationTitle": "Add a duration",
"needDurationBody": "Enter the custom duration.",
"addedTitle": "Prescription added"
"addedTitle": "Prescription added",
"badDatesTitle": "Check the dates",
"badDatesBody": "The end date can't be before the start date.",
"outOfStock": "Out of stock",
"inStock": "{{count}} in stock",
"courseDates": "Course dates",
"removeDates": "Remove",
"startDate": "Start date",
"endDate": "End date",
"addDates": "Add start/end dates"
}
},
"pharmacy": {
@@ -422,7 +432,7 @@
"searchPlaceholder": "Search prescriptions",
"kpi": {
"active": "Active prescriptions",
"expiring": "Expiring within 7 days",
"expiring": "Expiring soon",
"patients": "Patients on medication"
},
"queue": {
@@ -436,7 +446,15 @@
"completedTitle": "Prescription completed",
"completedBody": "{{medication}} for {{name}} marked as completed.",
"completeFailedTitle": "Couldn't update prescription",
"completeFailedBody": "Please try again."
"completeFailedBody": "Please try again.",
"dispense": "Dispense",
"dispensedTitle": "Dispensed",
"dispensedBody": "{{medication}} dispensed to {{name}}.",
"dispensed": {
"title": "Recently dispensed",
"description": "Medications handed to patients at the pharmacy.",
"empty": "Nothing dispensed yet."
}
},
"inventory": {
"title": "Inventory",
@@ -485,6 +503,11 @@
"addedTitle": "Item added",
"failedTitle": "Could not add item",
"failedBody": "Something went wrong. Please try again."
},
"detail": {
"description": "Stock item",
"notes": "Notes",
"close": "Close"
}
},
"lab": {
@@ -493,7 +516,14 @@
"queue": {
"title": "Work queue",
"description": "Tasks assigned to the Lab department.",
"empty": "No lab tasks right now."
"empty": "No lab tasks right now.",
"noNotes": "No details provided.",
"meta": {
"status": "Status",
"patient": "Patient",
"assignee": "Assignee",
"createdBy": "Created by"
}
},
"addResult": {
"button": "Add result",
@@ -550,7 +580,8 @@
"board": {
"addTask": "Add task",
"moveTo": "Move to",
"emptyColumn": "No tasks."
"emptyColumn": "No tasks.",
"newTask": "New task"
},
"filters": {
"all": "All",
@@ -884,7 +915,9 @@
"noTasks": "No tasks.",
"noPrescriptions": "No prescriptions.",
"inventory": "Inventory",
"noInventory": "No inventory items."
"noInventory": "No inventory items.",
"moreAppointments": "+{{count}} more",
"viewInCalendar": "View in calendar"
},
"clinicCard": {
"title": "Clinic",
+4
View File
@@ -14,6 +14,8 @@ export type Prescription = {
frequency: string;
prescriber: string;
prescribedAt: string; // YYYY-MM-DD
startDate: string | null; // YYYY-MM-DD, optional course start
endDate: string | null; // YYYY-MM-DD, optional course end (drives expiry)
status: RxStatus;
duration: string | null;
notes: string | null;
@@ -35,6 +37,8 @@ export type PrescriptionInput = {
notes?: string | null;
prescriber?: string;
prescribedAt?: string;
startDate?: string | null;
endDate?: string | null;
status?: RxStatus;
source?: "manual" | "ai";
};