Files
vfxreview/Shot task workflow.md
T
twotalesanimation b640fa472c
Deploy / deploy (push) Successful in 2m37s
added unapprove
2026-06-11 15:19:12 +02:00

7.8 KiB

Shot / Task / Version & Approval Status Workflow

Core Entities & Status Fields

Entity Status Field Enum
Shot status (derived) ShotStatus
Shot shotApprovalStatus ShotApprovalStatus
Task status TaskStatus
Version approvalStatus ApprovalStatus
Version reviewStatus ReviewStatus (lightly used)

Data Model & Enums

Enum States Purpose
ShotStatus WAITING, IN_PROGRESS, INTERNAL_REVIEW, READY_FOR_CLIENT, CLIENT_REVIEW, REVISIONS, COMPLETE Derived shot state (read-only, calculated)
ShotApprovalStatus PENDING, INTERNALLY_APPROVED, CLIENT_APPROVED Shot-level approval stage (manually set)
TaskStatus TODO, IN_PROGRESS, INTERNAL_REVIEW, CLIENT_REVIEW, CHANGES, DONE Task execution state
ApprovalStatus PENDING_REVIEW, APPROVED, REJECTED, NEEDS_CHANGES Version review decision
ReviewStatus PENDING, INTERNAL_APPROVED, CLIENT_APPROVED, NEEDS_CHANGES, FINAL_APPROVED Version review progression (legacy/parallel)

Core Relationships

Project → Shot (1:N)
   ↓
Shot → Task (1:N)
   ↓
Task → Version (1:N)
   ↓
Version → Approval (1:N, one per reviewer)

Shot Status is Derived, Not Set Directly

ShotStatus is computed by deriveShotStatus() in lib/shot-status.ts from task states and approval fields. It is recalculated via recalcShotStatus() whenever a task, version, or approval changes.

Priority rules (highest wins):

REVISIONS         ← any task is CHANGES
COMPLETE          ← shotApprovalStatus === CLIENT_APPROVED
CLIENT_REVIEW     ← INTERNALLY_APPROVED + sharedWithClient = true
READY_FOR_CLIENT  ← INTERNALLY_APPROVED + sharedWithClient = false
IN_PROGRESS       ← any task is TODO or IN_PROGRESS
INTERNAL_REVIEW   ← tasks exist, none blocking (approval still PENDING)
WAITING           ← no tasks

recalcShotStatus() is automatically triggered on:

  • Task creation (added to shot)
  • Task status update
  • Approval submission

Version → Approval → Task Pipeline

Artist uploads Version
    → all prior versions: isLatest = false
    → new Version: approvalStatus = PENDING_REVIEW, isLatest = true
    → Task auto-moves → INTERNAL_REVIEW
    → Shot recalculated → INTERNAL_REVIEW
    → Notifications sent to supervisors/producers
         ↓
Reviewer submits Approval (creates Approval record)
    → Version.approvalStatus updated
    → APPROVED             → Task → DONE
    → REJECTED             → Task → CHANGES
    → NEEDS_CHANGES        → Task → CHANGES
    → Shot recalculated
    → Notifications sent (artist, Slack webhook)

Approval Status Mapping

Version.approvalStatus Task.status Shot.status (if isolated)
PENDING_REVIEW INTERNAL_REVIEW INTERNAL_REVIEW
APPROVED DONE depends on other tasks
REJECTED CHANGES REVISIONS
NEEDS_CHANGES CHANGES REVISIONS

Role-Based Access for Approvals

  • Only ADMIN, PRODUCER, SUPERVISOR, CLIENT can approve
  • Client can only approve if shot is shared (sharedWithClient = true)
  • Approval records are append-only — each review creates a new Approval row; the latest one wins

Task Status Transitions

TODO
  ↓ (artist starts work / uploads version)
IN_PROGRESS / INTERNAL_REVIEW
  ↓ (approval submitted)
  ├─→ DONE        (if APPROVED)
  └─→ CHANGES     (if REJECTED or NEEDS_CHANGES)
         ↓
CLIENT_REVIEW     (when shot is shared with client)
Action Trigger Effect
createTask() Producer creates task Creates in TODO; calls recalcShotStatus()
updateTaskStatus() Manual status change Updates task; triggers notifications + shot recalc
Task → INTERNAL_REVIEW Version uploaded Auto-triggered in actions/versions.ts
Task → CLIENT_REVIEW Shot shared with client Auto-triggered in actions/shots.ts
Task → DONE Version approved Auto-triggered in actions/approvals.ts
Task → CHANGES Version rejected/needs changes Auto-triggered in actions/approvals.ts

Permissions: ADMIN, PRODUCER, SUPERVISOR can create/update tasks.


Shot-Level Approval Lifecycle

PENDING (initial)
    ↓ internallyApproveShot()  [ADMIN / PRODUCER / SUPERVISOR]
INTERNALLY_APPROVED  →  Shot status: READY_FOR_CLIENT
    ↓ shareWithClient()
sharedWithClient = true  →  Shot status: CLIENT_REVIEW
    latest versions: isClientVisible = true, sharedAt = now
         ↓                               ↓
clientApproveShot()           clientRequestsChanges()
shotApprovalStatus =          shotApprovalStatus = PENDING
CLIENT_APPROVED               sharedWithClient = false
Shot status: COMPLETE         isClientVisible = false (all task versions)
                              Shot status: REVISIONS (if tasks have CHANGES)

State Details

State shotApprovalStatus sharedWithClient Shot.status
Initial PENDING false WAITING / IN_PROGRESS / INTERNAL_REVIEW
Internally approved INTERNALLY_APPROVED false READY_FOR_CLIENT
Shared with client INTERNALLY_APPROVED true CLIENT_REVIEW
Client approved CLIENT_APPROVED true COMPLETE
Revisions requested PENDING false REVISIONS

Key Implementation Files

Concern File
Status derivation engine lib/shot-status.ts
Shot actions (share/approve/revise) actions/shots.ts
Approval creation + cascades actions/approvals.ts
Version upload + task auto-transition actions/versions.ts
Task creation / status updates actions/tasks.ts
Shot status UI badges components/shots/ShotCard.tsx
Task status UI badges components/tasks/TaskCard.tsx

Notable Design Choices

  • ShotStatus is never written directly — always a computed projection of task + approval state, preventing inconsistency.
  • ReviewStatus on Version is largely redundant with approvalStatus; it appears to be a legacy/parallel field not driving much logic currently.
  • Approval records are append-only — each review creates a new Approval row; the latest one wins for updating Version.approvalStatus.
  • Client visibility is gated at two levels: Shot.sharedWithClient and Version.isClientVisible, both toggled atomically when sharing.
  • shareWithClient() is atomic: all task versions are updated in a single transaction.

Unapprove / Undo Approval

Version-Level Unapprove (unapproveVersion)

Available to: ADMIN, PRODUCER, SUPERVISOR
Shown as: "Undo Approval" in the version dropdown (only visible when approvalStatus === APPROVED)
Location: VersionList.tsxactions/approvals.ts

Version.approvalStatus === APPROVED
    ↓ unapproveVersion()
Version.approvalStatus → PENDING_REVIEW
Task.status (if DONE) → INTERNAL_REVIEW
Audit: new Approval record created with status = PENDING_REVIEW, notes = "Approval undone"
Shot recalculated

Shot-Level Unapprove (unapproveShot)

Available to: ADMIN, PRODUCER, SUPERVISOR
Shown as: "Undo Client Approval" button on the shot detail page (only when shotApprovalStatus === CLIENT_APPROVED)
Location: shots/[shotId]/page.tsxactions/shots.ts

shotApprovalStatus === CLIENT_APPROVED  (Shot status: COMPLETE)
    ↓ unapproveShot()
shotApprovalStatus → INTERNALLY_APPROVED
Shot.status → CLIENT_REVIEW  (if still sharedWithClient)
           → READY_FOR_CLIENT (if sharedWithClient = false)

Neither unapprove action modifies the sharedWithClient flag or version visibility — only the approval state is rolled back.