tasks: assign a task to a specific person

Backend: add tasks.assignee_user_id (nullable, fk user) + migration; persist
it through create/update and surface tasks to the assigned person in listTasks.

Frontend: New Task dialog gains a three-way Assignee control
(Myself / Department / Person) with a provider picker from /api/staff/providers;
task card + detail show the assignee's name when set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-20 18:40:56 +03:00
parent 1d9b1b1d22
commit bbc6869745
11 changed files with 4131 additions and 20 deletions
+5
View File
@@ -25,6 +25,11 @@ export const tasks = pgTable(
// The department (member role) a task is assigned to, e.g. "reception".
// Null means a personal task belonging to its creator. Drives who sees it.
assigneeRole: text("assignee_role"),
// A specific person the task is assigned to. Null when assigned to a
// department or kept personal. The assignee display name lives in `assignee`.
assigneeUserId: text("assignee_user_id").references(() => user.id, {
onDelete: "set null",
}),
due: text("due").notNull().default("No due date"),
priority: text("priority").$type<TaskPriority>().notNull(),
// Board column: todo | in_progress | done. Kept in sync with `done`
+1
View File
@@ -15,6 +15,7 @@ export const taskInputSchema = z.object({
title: z.string().trim().min(1, "A task subject is required.").max(200),
assignee: z.string().trim().max(200).default("Unassigned"),
assigneeRole: z.enum(TASK_DEPARTMENTS).nullish(),
assigneeUserId: z.string().trim().max(120).nullish(),
due: z.string().trim().max(120).default("No due date"),
priority: z.enum(["high", "medium", "low"]).default("medium"),
status: z.enum(["todo", "in_progress", "done"]).default("todo"),
+9 -1
View File
@@ -24,6 +24,7 @@ function toTask(row: TaskRow): Task {
title: row.title,
assignee: row.assignee,
assigneeRole: row.assigneeRole,
assigneeUserId: row.assigneeUserId,
due: row.due,
priority: row.priority,
status: row.status,
@@ -48,7 +49,11 @@ export async function listTasks(
let where = eq(tasks.organizationId, orgId);
if (!isAdmin) {
const visible = [eq(tasks.createdBy, viewer.userId)];
const visible = [
eq(tasks.createdBy, viewer.userId),
// Tasks assigned to this person specifically.
eq(tasks.assigneeUserId, viewer.userId),
];
if (roles.length) visible.push(inArray(tasks.assigneeRole, roles));
where = and(where, or(...visible))!;
}
@@ -73,6 +78,7 @@ export async function createTask(
title: input.title,
assignee: input.assignee,
assigneeRole: input.assigneeRole ?? null,
assigneeUserId: input.assigneeUserId ?? null,
due: input.due,
priority: input.priority,
status: input.status,
@@ -100,6 +106,8 @@ export async function updateTask(
if (patch.assignee !== undefined) set.assignee = patch.assignee;
if (patch.assigneeRole !== undefined)
set.assigneeRole = patch.assigneeRole ?? null;
if (patch.assigneeUserId !== undefined)
set.assigneeUserId = patch.assigneeUserId ?? null;
if (patch.due !== undefined) set.due = patch.due;
if (patch.priority !== undefined) set.priority = patch.priority;
if (patch.patient !== undefined) set.patient = patch.patient ?? null;
+4
View File
@@ -11,6 +11,10 @@ export type Task = {
assignee: string;
// Department (member role) the task is assigned to; null = personal task.
assigneeRole: string | null;
// A specific person the task is assigned to (user id); null when assigned to
// a department or kept personal. Takes precedence over assigneeRole for who
// sees the task.
assigneeUserId: string | null;
due: string;
priority: TaskPriority;
status: TaskStatus;