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

209 lines
7.8 KiB
Markdown

# 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.tsx``actions/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.tsx``actions/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.