From 741883465ce6e8e59f155a68ba25dcf7edc89a3d Mon Sep 17 00:00:00 2001 From: Citali Date: Thu, 12 Feb 2026 22:59:53 +0100 Subject: [PATCH] feat(commissions): add editable assignment and artwork linkage --- TODO.md | 9 +- apps/admin/src/app/commissions/page.tsx | 203 +++++++++++++++++- packages/content/src/commissions.ts | 17 +- .../migration.sql | 2 + packages/db/prisma/schema.prisma | 1 + packages/db/src/commissions.ts | 11 + packages/db/src/index.ts | 1 + 7 files changed, 235 insertions(+), 9 deletions(-) create mode 100644 packages/db/prisma/migrations/20260213012000_commission_linked_artworks/migration.sql diff --git a/TODO.md b/TODO.md index 87a3b26..a36f80f 100644 --- a/TODO.md +++ b/TODO.md @@ -148,10 +148,10 @@ This file is the single source of truth for roadmap and delivery progress. - [x] [P1] Users management (invite, roles, status) - [x] [P1] Disable/ban user function and enforcement in auth/session checks - [x] [P1] Owner/support protection rules in user management actions (cannot delete/demote) -- [~] [P1] Commissions management (request intake, owner, due date, notes, linked customer, linked artworks) -- [~] [P1] Customer records (contact profile, notes, consent flags, recurrence marker) -- [~] [P1] Customer-to-commission linkage and reuse workflow (no re-entry for recurring customers) -- [~] [P1] Kanban workflow for commissions (new, scoped, in-progress, review, done) +- [x] [P1] Commissions management (request intake, owner, due date, notes, linked customer, linked artworks) +- [x] [P1] Customer records (contact profile, notes, consent flags, recurrence marker) +- [x] [P1] Customer-to-commission linkage and reuse workflow (no re-entry for recurring customers) +- [x] [P1] Kanban workflow for commissions (new, scoped, in-progress, review, done) - [x] [P1] Header banner management (message, CTA, active window) - [~] [P1] Announcements management (prominent site notices with schedule, priority, and audience targeting) - [~] [P2] News/blog editorial workflow (draft/review/publish, authoring metadata) @@ -368,6 +368,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Page builder reusable blocks completed: admin block editor now supports full field editing + ordering controls for hero/rich-text/gallery/cta/form/price-cards; public renderer includes form-link behavior for `contact`/`commission` keys. - [2026-02-12] Navigation management completed: admin `/navigation` now supports menu update/delete controls, nested item parent selection via menu-local dropdown, and full order/visibility updates across menus and items. - [2026-02-12] Users management baseline completed: admin `/users` now supports managed user creation, role changes (`admin/editor/manager`), status changes (ban/unban), and protected/system guardrails for role-change/delete/ban actions. +- [2026-02-12] Commissions management completed: admin kanban cards now include inline detail editing (assignee/customer/budget/due date/notes), linked-artwork references via `linkedArtworkIds`, and creation/edit flows use assignable users instead of raw ID entry. - [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries. - [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path). - [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`. diff --git a/apps/admin/src/app/commissions/page.tsx b/apps/admin/src/app/commissions/page.tsx index 539322d..351dcab 100644 --- a/apps/admin/src/app/commissions/page.tsx +++ b/apps/admin/src/app/commissions/page.tsx @@ -2,8 +2,11 @@ import { commissionKanbanOrder, createCommission, createCustomer, + db, + listArtworks, listCommissions, listCustomers, + updateCommission, updateCommissionStatus, } from "@cms/db" import { Button } from "@cms/ui/button" @@ -67,6 +70,19 @@ function readNullableDate(formData: FormData, field: string): Date | null { return parsed } +function readUuidList(formData: FormData, field: string): string[] { + const raw = readInputString(formData, field) + + if (!raw) { + return [] + } + + return raw + .split(",") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) +} + function redirectWithState(params: { notice?: string; error?: string }) { const query = new URLSearchParams() @@ -124,6 +140,7 @@ async function createCommissionAction(formData: FormData) { status: readInputString(formData, "status"), customerId: readNullableString(formData, "customerId"), assignedUserId: readNullableString(formData, "assignedUserId"), + linkedArtworkIds: readUuidList(formData, "linkedArtworkIds"), budgetMin: readNullableNumber(formData, "budgetMin"), budgetMax: readNullableNumber(formData, "budgetMax"), dueAt: readNullableDate(formData, "dueAt"), @@ -136,6 +153,35 @@ async function createCommissionAction(formData: FormData) { redirectWithState({ notice: "Commission created." }) } +async function updateCommissionAction(formData: FormData) { + "use server" + + await requirePermissionForRoute({ + nextPath: "/commissions", + permission: "commissions:write", + scope: "own", + }) + + try { + await updateCommission({ + id: readInputString(formData, "id"), + title: readInputString(formData, "title"), + description: readNullableString(formData, "description"), + customerId: readNullableString(formData, "customerId"), + assignedUserId: readNullableString(formData, "assignedUserId"), + linkedArtworkIds: readUuidList(formData, "linkedArtworkIds"), + budgetMin: readNullableNumber(formData, "budgetMin"), + budgetMax: readNullableNumber(formData, "budgetMax"), + dueAt: readNullableDate(formData, "dueAt"), + }) + } catch { + redirectWithState({ error: "Failed to update commission details." }) + } + + revalidatePath("/commissions") + redirectWithState({ notice: "Commission updated." }) +} + async function updateCommissionStatusAction(formData: FormData) { "use server" @@ -166,6 +212,14 @@ function formatDate(value: Date | null) { return value.toLocaleDateString("en-US") } +function formatDateInput(value: Date | null) { + if (!value) { + return "" + } + + return value.toISOString().slice(0, 10) +} + export default async function CommissionsManagementPage({ searchParams, }: { @@ -177,10 +231,22 @@ export default async function CommissionsManagementPage({ scope: "own", }) - const [resolvedSearchParams, customers, commissions] = await Promise.all([ + const [resolvedSearchParams, customers, commissions, assignees, artworks] = await Promise.all([ searchParams, listCustomers(200), listCommissions(300), + db.user.findMany({ + where: { + isBanned: false, + }, + orderBy: [{ createdAt: "asc" }], + select: { + id: true, + name: true, + username: true, + }, + }), + listArtworks(300), ]) const notice = readFirstValue(resolvedSearchParams.notice) @@ -309,11 +375,18 @@ export default async function CommissionsManagementPage({
+