fix: clinic creation gating, analysis line charts, username + task assignee, delete confirm

- Hide "Create clinic" (sidebar footer) for non-admins; only owner/admin can
  spin up additional clinics. Onboarding for brand-new users is unaffected.
- Analysis: drop the bar charts; show line charts (Sparkline) inside KPI cards
  that open a detail dialog with the full chart + per-point breakdown.
- Add Team Member: validate the username client-side (no spaces; letters,
  numbers, dots, underscores) with a clear warning + field hint.
- Tasks: New Task now has an Assignee selector (Myself / Other → department).
  Tasks are visible to the department they're assigned to (or the creator), and
  show who created them. Backend adds assignee_role + created_by_name with
  visibility filtering in listTasks; owners/admins see all.
- Care team: removing a member now asks for confirmation first (dialog) and
  surfaces success/failure + refreshes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-08 19:52:12 +03:00
parent 8ab0552cf8
commit 4f8793c765
19 changed files with 2878 additions and 144 deletions
@@ -11,6 +11,7 @@ import {
SheetPopup,
SheetTitle,
} from "@/components/ui/sheet";
import { ROLE_LABELS } from "@/lib/access";
import { cn } from "@/lib/utils";
import type { Priority, Task } from "@/components/tasks/tasks-view";
@@ -21,6 +22,10 @@ const priorityVariant: Record<Priority, "destructive" | "secondary" | "outline">
low: "outline",
};
function deptLabel(role: string): string {
return (ROLE_LABELS as Record<string, string>)[role] ?? role;
}
// 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 from the page.
@@ -67,9 +72,19 @@ export function TaskDetailSheet({
: t("tasks.detail.open")}
</dd>
<dt className="text-muted-foreground">
{t("tasks.detail.assignee")}
{t("tasks.detail.assignedTo")}
</dt>
<dd className="text-foreground">{task.assignee}</dd>
<dd className="text-foreground">
{task.assigneeRole
? t("tasks.detail.deptTeam", {
dept: deptLabel(task.assigneeRole),
})
: t("tasks.detail.personal")}
</dd>
<dt className="text-muted-foreground">
{t("tasks.detail.createdBy")}
</dt>
<dd className="text-foreground">{task.createdByName ?? "—"}</dd>
<dt className="text-muted-foreground">
{t("tasks.detail.due")}
</dt>
+73 -22
View File
@@ -25,6 +25,8 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ROLE_LABELS } from "@/lib/access";
import { DEPARTMENTS } from "@/lib/roles";
import {
type Priority,
type Task,
@@ -36,6 +38,12 @@ import {
import { notify } from "@/lib/toast";
import { cn } from "@/lib/utils";
type AssigneeMode = "self" | "other";
function deptLabel(role: string): string {
return (ROLE_LABELS as Record<string, string>)[role] ?? role;
}
export type { Priority, Task } from "@/lib/tasks";
type Filter = "all" | "open" | "done";
@@ -98,14 +106,16 @@ function AddTaskDialog({
const { t } = useTranslation();
const [title, setTitle] = useState("");
const [notes, setNotes] = useState("");
const [assignee, setAssignee] = useState("");
const [assigneeMode, setAssigneeMode] = useState<AssigneeMode>("self");
const [department, setDepartment] = useState<string>("reception");
const [due, setDue] = useState("");
const [priority, setPriority] = useState<Priority>("medium");
const reset = () => {
setTitle("");
setNotes("");
setAssignee("");
setAssigneeMode("self");
setDepartment("reception");
setDue("");
setPriority("medium");
};
@@ -122,7 +132,8 @@ function AddTaskDialog({
onAdd({
title: title.trim(),
notes: notes.trim() || undefined,
assignee: assignee.trim() || "Unassigned",
// "Myself" → personal task (no department); "Other" → a department.
assigneeRole: assigneeMode === "self" ? null : department,
due: due.trim() || "No due date",
priority,
});
@@ -163,14 +174,47 @@ function AddTaskDialog({
value={notes}
/>
</Field>
<div className="grid grid-cols-2 gap-3">
<Field label={t("tasks.dialog.assignee")}>
<Input
onChange={(e) => setAssignee(e.target.value)}
placeholder={t("tasks.dialog.assigneePlaceholder")}
value={assignee}
/>
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs">
{t("tasks.dialog.assignee")}
</span>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={() => setAssigneeMode("self")}
size="sm"
type="button"
variant={assigneeMode === "self" ? "secondary" : "outline"}
>
{t("tasks.dialog.assigneeSelf")}
</Button>
<Button
className="flex-1"
onClick={() => setAssigneeMode("other")}
size="sm"
type="button"
variant={assigneeMode === "other" ? "secondary" : "outline"}
>
{t("tasks.dialog.assigneeOther")}
</Button>
</div>
</div>
{assigneeMode === "other" && (
<Field label={t("tasks.dialog.department")}>
<select
className={controlClass}
onChange={(e) => setDepartment(e.target.value)}
value={department}
>
{DEPARTMENTS.map((d) => (
<option key={d} value={d}>
{ROLE_LABELS[d]}
</option>
))}
</select>
</Field>
)}
<div className="grid grid-cols-2 gap-3">
<Field label={t("tasks.dialog.due")}>
<Input
onChange={(e) => setDue(e.target.value)}
@@ -178,18 +222,18 @@ function AddTaskDialog({
value={due}
/>
</Field>
<Field label={t("tasks.dialog.priorityLabel")}>
<select
className={controlClass}
onChange={(e) => setPriority(e.target.value as Priority)}
value={priority}
>
<option value="high">{t("tasks.priority.high")}</option>
<option value="medium">{t("tasks.priority.medium")}</option>
<option value="low">{t("tasks.priority.low")}</option>
</select>
</Field>
</div>
<Field label={t("tasks.dialog.priorityLabel")}>
<select
className={controlClass}
onChange={(e) => setPriority(e.target.value as Priority)}
value={priority}
>
<option value="high">{t("tasks.priority.high")}</option>
<option value="medium">{t("tasks.priority.medium")}</option>
<option value="low">{t("tasks.priority.low")}</option>
</select>
</Field>
</DialogPanel>
<DialogFooter>
@@ -337,7 +381,14 @@ export function TasksView() {
{task.title}
</span>
<span className="truncate text-muted-foreground text-xs">
{task.assignee} · {task.due}
{task.assigneeRole
? t("tasks.list.forDept", {
dept: deptLabel(task.assigneeRole),
})
: t("tasks.list.personal")}
{task.createdByName
? ` · ${t("tasks.list.byCreator", { name: task.createdByName })}`
: ""}
</span>
</button>
<Badge