From beef7cc42a5652f4f5a3165f07f883d6f55af4aa Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 23 Jul 2026 10:12:40 +0000 Subject: [PATCH] feat(ara-projects): project settings / setup hub at /settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the page behind two links that previously 404'd — the per-project nav's Settings item and the import screen's "finish setup first" CTA. ADR-0019 made work-order import setup-gated, which turned setup from a one-shot wizard into something a project accretes over time. This is the return path: setup status, the four /setup/* steps as revisitable links, and the editable project details, in that order — the CTA's sender needs the readiness answer first, so it is the first band on the page. The setup-status panel renders `loadImportReadiness` verbatim. It restates no part of the gate: `readiness.ready` is the import screen's own verdict, and `setupSteps.ts` only rearranges that value for display, so "ready to import" cannot mean two different things in two places. A test walks the readiness space and asserts every gated step is done exactly when the helper says ready. Evidence requirements are badged Optional, never outstanding — ADR-0019 leaves them out of the gate. Settings is a management surface, so it is `canManageProject`, not the layout's view guard: a contractor with legitimate access to the project 404s here rather than seeing a form they may not submit. The four `/setup/*` hrefs live in one exported list. Those routes are #410-413, in flight — the hub is what links into all of them, so a slug that lands differently is one line to correct, not four scattered literals. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/ProjectDetailsForm.tsx | 279 ++++++++++++++++++ .../settings/components/SetupStatusPanel.tsx | 112 +++++++ .../settings/components/SetupStepsList.tsx | 97 ++++++ .../projects/[projectId]/settings/page.tsx | 93 ++++++ .../[projectId]/settings/setupSteps.test.ts | 212 +++++++++++++ .../[projectId]/settings/setupSteps.ts | 150 ++++++++++ 6 files changed, 943 insertions(+) create mode 100644 src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx create mode 100644 src/app/projects/[projectId]/settings/components/SetupStatusPanel.tsx create mode 100644 src/app/projects/[projectId]/settings/components/SetupStepsList.tsx create mode 100644 src/app/projects/[projectId]/settings/page.tsx create mode 100644 src/app/projects/[projectId]/settings/setupSteps.test.ts create mode 100644 src/app/projects/[projectId]/settings/setupSteps.ts diff --git a/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx b/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx new file mode 100644 index 00000000..f503ed16 --- /dev/null +++ b/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { + projectDetailsToFormValues, + updateProjectSchema, + type ProjectDetails, + type UpdateProjectFormValues, +} from "@/lib/projects/updateProject"; +import type { SelectOption } from "@/lib/projects/createProject"; +import { DateInput } from "@/app/components/DateInput"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Checkbox } from "@/app/shadcn_components/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/app/shadcn_components/ui/form"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/app/shadcn_components/ui/select"; + +/** + * The HTTP call, kept outside the component so the component contains no + * `fetch` — TanStack Query owns the request lifecycle. Route handlers answer + * errors as `{ error: string }`, so a failure surfaces the server's own message. + */ +async function updateProject( + projectId: string, + values: UpdateProjectFormValues, +): Promise<{ id: string }> { + const response = await fetch(`/api/projects/${projectId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }); + + const body = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(body?.error ?? "Could not save the project details"); + } + return body; +} + +export interface ProjectDetailsFormProps { + details: ProjectDetails; + projectTypes: SelectOption[]; +} + +/** + * Edit a project's details from the settings hub (issue #444). + * + * The same six fields the create-project modal collects, minus the owning + * organisation — re-homing a project would change who can see it, so it is + * shown read-only rather than offered as an edit (see `updateProjectSchema`). + * The resolver is `updateProjectSchema`, which is derived from + * `createProjectSchema`, so the two forms validate identically. + * + * On success the form resets to the values it just saved — which clears the + * dirty state and makes the Save button correctly go quiet — and + * `router.refresh()` re-renders the server component above it, so the heading + * and the readiness panel reflect the edit without a full reload. + */ +export function ProjectDetailsForm({ + details, + projectTypes, +}: ProjectDetailsFormProps) { + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(updateProjectSchema), + defaultValues: projectDetailsToFormValues(details), + }); + + const mutation = useMutation( + (values: UpdateProjectFormValues) => updateProject(details.id, values), + { + onSuccess: (_data, values) => { + form.reset(values); + router.refresh(); + }, + }, + ); + + return ( +
+

Project details

+

+ What this programme of works is called, who it is for, and when it runs. +

+ +
+ mutation.mutate(values))} + className="mt-4 space-y-4 rounded-xl border border-gray-200 bg-white p-6" + data-testid="project-details-form" + > + ( + + Project name + + + + + + )} + /> + + + Client +

+ {details.organisationName ?? "—"} +

+ + A project cannot be moved to another organisation — that would + change who can see it. + +
+ + ( + + Project type + + + + )} + /> + + ( + + Description + +