dow-prod-tracker/src/hooks/use-projects.ts
DJP 9ff0f03a4d Board drag-and-drop with forward/rework pipeline rules
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.
2026-04-21 12:45:17 -04:00

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"] });
},
});
}