feat(board): daily board linked to shots

A /board page that replaces the shared doc used as the day-to-day tracker.
One card per active/on-hold project with a status line, dated milestones
and to-do lines typed or pasted in the doc's own format:

  DH2_VFX_027_090 finalise (CHRIS)
  DH2_VFX_005_010 gfx update @michelle wait: client GFX ADH_GFX_005_020

Shot codes link to the shot (live status + latest version), (NAME)/@name
assigns, wait: marks a blocker, headings group lines. Codes that match no
shot are flagged, with a relink when there is an obvious near miss.

Board lines are their own model, not Tasks: tasks drive shot status and
notifications, and a note like "finalise" must not touch either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-10-07 17:30:40 +02:00
parent 27d9618995
commit 34af26dfbf
8 changed files with 1331 additions and 0 deletions
@@ -0,0 +1,44 @@
-- Daily board: the day-to-day tracker that used to live in a shared doc,
-- now linked to shots. Board items are notes, not tasks, so nothing here
-- feeds shot status.
ALTER TABLE "projects" ADD COLUMN "boardStatus" TEXT;
ALTER TABLE "projects" ADD COLUMN "boardStatusAt" TIMESTAMP(3);
CREATE TABLE "board_items" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"shotId" TEXT,
"code" TEXT,
"text" TEXT NOT NULL,
"group" TEXT NOT NULL DEFAULT '',
"assigneeId" TEXT,
"waitingOn" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"doneAt" TIMESTAMP(3),
"doneById" TEXT,
"createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "board_items_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "project_milestones" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"doneAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "project_milestones_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "board_items_projectId_doneAt_idx" ON "board_items"("projectId", "doneAt");
CREATE INDEX "board_items_shotId_idx" ON "board_items"("shotId");
CREATE INDEX "project_milestones_projectId_date_idx" ON "project_milestones"("projectId", "date");
ALTER TABLE "board_items" ADD CONSTRAINT "board_items_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "board_items" ADD CONSTRAINT "board_items_shotId_fkey" FOREIGN KEY ("shotId") REFERENCES "shots"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "board_items" ADD CONSTRAINT "board_items_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "project_milestones" ADD CONSTRAINT "project_milestones_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+61
View File
@@ -262,6 +262,7 @@ model User {
createdShootDays ShootDay[] @relation("ShootDayCreator")
createdTakes Take[] @relation("TakeCreator")
uploadedTakeAttachments TakeAttachment[] @relation("TakeAttachmentUploader")
assignedBoardItems BoardItem[] @relation("BoardItemAssignee")
@@map("users")
}
@@ -382,6 +383,14 @@ model Project {
deliveryPackages DeliveryPackage[]
deliveryRequests DeliveryRequest[]
/// The one-line "where are we" on the daily board ("Official bid sent",
/// "Awaiting feedback on pick-ups"). Free text on purpose: it is what the
/// producer would have typed at the top of the project's section in the doc.
boardStatus String? @db.Text
boardStatusAt DateTime?
boardItems BoardItem[]
milestones ProjectMilestone[]
@@map("projects")
}
@@ -487,6 +496,7 @@ model Shot {
loggedTakes Take[] @relation("TakeToShot")
exports Export[]
deliveryRequestLines DeliveryRequestLine[]
boardItems BoardItem[]
@@unique([projectId, shotCode])
@@map("shots")
@@ -1255,6 +1265,57 @@ model DeliveryRequestLine {
@@map("delivery_request_lines")
}
/// A line on the daily board — the day-to-day to-do the team used to keep in
/// a shared doc ("DH2_VFX_027_090 finalise (CHRIS)"). Deliberately not a Task:
/// tasks are the pipeline's one-per-shot work items whose state drives shot
/// status, and a note like "finalise" or "awaiting gfx" must not move a shot
/// back to IN_PROGRESS or notify anyone.
///
/// `code` is the shot code as typed. When it matches a shot, `shotId` links it
/// and the board shows the shot's live status; when it does not, the line is a
/// flag ("not on our tracker?") until the shot exists or the code is fixed.
model BoardItem {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
shotId String?
shot Shot? @relation(fields: [shotId], references: [id], onDelete: SetNull)
code String?
text String @db.Text
/// Heading the line sits under on the board ("WATER DROPS"); "" = none
group String @default("")
assigneeId String?
assignee User? @relation("BoardItemAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
/// What the line is blocked on ("client GFX ADH_GFX_013_010"); null = not blocked
waitingOn String?
sortOrder Int @default(0)
doneAt DateTime?
doneById String?
createdById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId, doneAt])
@@index([shotId])
@@map("board_items")
}
/// A dated project milestone — batch deliveries, picture locks, shoot start,
/// colour & mix review. Episode due dates stay in EpisodeDueDate because the
/// shot-status board keys off them; these are the dates around the work.
model ProjectMilestone {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
label String
date DateTime
doneAt DateTime?
createdAt DateTime @default(now())
@@index([projectId, date])
@@map("project_milestones")
}
model SketchTemplate {
id String @id @default(cuid())
name String