mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user