Producers can now drag cards on both boards. Three distinct write
paths, each with validation appropriate to what it's moving:
Projects board (status lens only):
- Drop updates Project.status via PATCH /api/projects/:id.
- Stage lens stays read-only — Project.stage is derived from the
dominant deliverable stage, there's no single write target.
Deliverables board (status lens):
- Drop updates Deliverable.status directly.
Deliverables board (stage lens) — the interesting one:
- Drop hits new POST /api/deliverables/:id/transition endpoint,
validated by new stage-transition-service.
- Forward transitions: current → APPROVED, optional intermediates
→ SKIPPED, target → IN_PROGRESS. If a REQUIRED intermediate
isn't already done, the drop is rejected ("walk through it
first") — toast shows the blocking stage name.
- Rework (backward) transitions: only allowed if the pipeline
template declares an explicit rework path from current → target.
Otherwise rejected. No cross-pipeline drops possible because the
target slug is looked up inside the deliverable's own pipeline.
Schema:
- New PipelineStageRework self-join on PipelineStageDefinition —
per-template declaration of "from stage X you can push back to
stage Y". Migration 20260423000000_pipeline_stage_reworks.
- Seed populates Dow's canonical rework paths: Client Review
(Copy) → Copywriter, Internal Review → In Progress Creative,
Client Feedback → In Progress Creative, Final Approval → In
Progress Creative, Completed → In Progress Creative.
Pipeline template editor UI (where producers would add their own
rework paths) is deferred; the seed covers Dow out of the box.
Hooks: added useUpdateProjectById, useUpdateDeliverableById,
useTransitionDeliverable — all take ids at mutation time so the
boards don't need one hook per card.
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { apiUrl } from "@/lib/api-client";
|
|
import type { CreateProjectInput, UpdateProjectInput } from "@/lib/validators/project";
|
|
|
|
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(apiUrl(url), init);
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new Error(body.error || `Request failed: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export function useProjects() {
|
|
return useQuery({
|
|
queryKey: ["projects"],
|
|
queryFn: () => fetchJson("/api/projects"),
|
|
});
|
|
}
|
|
|
|
export function useProject(projectId: string) {
|
|
return useQuery({
|
|
queryKey: ["projects", projectId],
|
|
queryFn: () => fetchJson(`/api/projects/${projectId}`),
|
|
enabled: !!projectId,
|
|
});
|
|
}
|
|
|
|
export function useCreateProject() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: CreateProjectInput) =>
|
|
fetchJson("/api/projects", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateProject(projectId: string) {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: UpdateProjectInput) =>
|
|
fetchJson(`/api/projects/${projectId}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
queryClient.invalidateQueries({ queryKey: ["projects", projectId] });
|
|
},
|
|
});
|
|
}
|
|
|
|
// Generic project update that takes the id at mutation time (not at hook
|
|
// bind time). Handy for list/board views that mutate different projects.
|
|
export function useUpdateProjectById() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: UpdateProjectInput }) =>
|
|
fetchJson(`/api/projects/${id}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteProject() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (projectId: string) =>
|
|
fetchJson(`/api/projects/${projectId}`, { method: "DELETE" }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
},
|
|
});
|
|
}
|