Files
vfxreview/types/index.ts
T
twotalesanimation 842a8fe036
Deploy / deploy (push) Successful in 3m34s
feat(board): paste client feedback from WhatsApp
"Paste feedback" on each board card takes a client's message as it arrived
and splits it into rounds ("FEEDBACK 113 DUE Oct 7 12:00 PM") and per-shot
notes, tolerating WhatsApp copy prefixes, bold markers, wrapped lines,
several codes per note and codes without the show prefix.

A review step shows each note's shot, the version the client was sent,
the player frame and a "move task to Changes" tick (on by default).
Saving creates a FeedbackRound, a player comment per note tagged
source=WHATSAPP (shown as "Client feedback") and a board line under the
round's due time; ticking the line off resolves the comment.

Quoted frames are read as comp frames: shot frameStart or the house 1001,
plus the slate (recorded, else 1 for pipeline previews). Version.duration
is never stored on live, so the length-based slate inference can't be used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:31:34 +02:00

330 lines
9.0 KiB
TypeScript

import { Role, ApprovalStatus, ReviewStatus, ShotStatus, ShotApprovalStatus, ShotPriority, ProjectStatus, TaskStatus, TaskType } from "@prisma/client";
// Re-export Prisma enums for convenience
export { Role, ApprovalStatus, ReviewStatus, ShotStatus, ShotApprovalStatus, ShotPriority, ProjectStatus, TaskStatus, TaskType };
// ── Annotation Types ─────────────────────────────────────────────────────────
export type AnnotationTool = "freehand" | "arrow" | "rectangle" | "circle";
export interface AnnotationPoint {
x: number; // normalized 0-1 — v2.0: of the picture; v1.0: of the player box
y: number; // normalized 0-1 — v2.0: of the picture; v1.0: of the player box
}
export interface AnnotationShape {
id: string;
tool: AnnotationTool;
/** For freehand: all path points. For others: [startPoint, endPoint] */
points: AnnotationPoint[];
color: string;
strokeWidth: number;
frameNumber: number;
}
export interface AnnotationDrawingData {
shapes: AnnotationShape[];
canvasWidth: number;
canvasHeight: number;
/**
* "2.0": points are relative to the displayed picture, so they stay locked to
* the image at any player size. "1.0" (legacy): relative to the whole player
* box (picture + letterbox bars) that was canvasWidth x canvasHeight.
*/
version: "1.0" | "2.0";
}
// ── Review Player State ──────────────────────────────────────────────────────
export interface ReviewPlayerState {
isPlaying: boolean;
currentFrame: number;
currentTime: number;
duration: number;
playbackRate: number;
volume: number;
isMuted: boolean;
isFullscreen: boolean;
isAnnotating: boolean;
selectedTool: AnnotationTool;
selectedColor: string;
strokeWidth: number;
showAnnotations: boolean;
fps: number;
totalFrames: number;
}
// ── Comment Types ────────────────────────────────────────────────────────────
export interface CommentWithReplies {
id: string;
versionId: string;
authorId: string;
frameNumber: number;
timestamp: number;
text: string;
isResolved: boolean;
/** "WHATSAPP" = client feedback pasted from a message; author is who logged it */
source?: string | null;
createdAt: Date;
updatedAt: Date;
author: {
id: string;
name: string | null;
email: string;
image: string | null;
role: Role;
} | null;
replies: CommentReplyWithAuthor[];
annotations: AnnotationSummary[];
}
export interface CommentReplyWithAuthor {
id: string;
commentId: string;
authorId: string;
text: string;
createdAt: Date;
author: {
id: string;
name: string | null;
email: string;
image: string | null;
role: Role;
} | null;
}
export interface AnnotationSummary {
id: string;
frameNumber: number;
drawingData: unknown;
color: string;
isVisible: boolean;
authorId: string;
}
// ── Version Types ────────────────────────────────────────────────────────────
export interface VersionWithDetails {
id: string;
versionNumber: number;
shotId: string | null;
taskId: string | null;
artistId: string | null;
isClientVisible: boolean;
sharedAt: Date | null;
sharedById: string | null;
fileUrl: string;
fileName: string;
fileSize: bigint | null;
mimeType: string | null;
thumbnailUrl: string | null;
posterUrl: string | null;
proxyUrl: string | null;
fps: number;
duration: number | null;
frameCount: number | null;
width: number | null;
height: number | null;
notes: string | null;
approvalStatus: ApprovalStatus;
reviewStatus: ReviewStatus;
isLatest: boolean;
createdAt: Date;
updatedAt: Date;
artist: {
id: string;
name: string | null;
email: string;
image: string | null;
} | null;
_count?: {
comments: number;
};
approvals?: {
id: string;
status: string;
notes: string | null;
createdAt: Date;
user: { id: string; name: string | null; image: string | null };
}[];
}
// ── Shot Types ───────────────────────────────────────────────────────────────
export interface ShotWithDetails {
id: string;
shotCode: string;
/** Client-supplied naming alias — slates/overlays/delivery only, never internal paths. */
clientCode: string | null;
scene: string;
episode: string | null;
shotNumber: number;
sequence: string | null;
description: string | null;
notes: string | null;
status: ShotStatus;
priority: ShotPriority;
artistId: string | null;
projectId: string;
fps: number;
frameStart: number | null;
frameEnd: number | null;
dueDate: Date | null;
thumbnailUrl: string | null;
generatedThumbnailUrl: string | null;
generatedThumbnailFrom: string | null;
originalFootageUrl: string | null;
originalFootageKey: string | null;
shotGroupId: string | null;
shotApprovalStatus: ShotApprovalStatus;
sharedWithClient: boolean;
// EDL / pull CSV fields
sourceClip: string | null;
timecodeStart: string | null;
timecodeEnd: string | null;
clipDuration: string | null;
exrOutput: string | null;
// Sequence / picture lock timecodes
seqTimecodeStart: string | null;
seqTimecodeEnd: string | null;
// High-res deliverable
highResFilename: string | null;
// Shot-level version tracking
shotVersion: string;
// Netflix slate fields
slateType: string | null;
slateDescription: string | null;
slateVfxScope: string | null;
slateSubmissionNote: string | null;
shotGroup: { id: string; name: string } | null;
footagePlates: {
id: string;
label: string;
fileUrl: string;
fileKey: string;
fileName: string;
fileSize: bigint | string | null;
sortOrder: number;
createdAt: Date;
}[];
createdAt: Date;
updatedAt: Date;
artist: {
id: string;
name: string | null;
email: string;
image: string | null;
} | null;
versions: VersionWithDetails[];
_count?: {
versions: number;
};
}
// ── Task Types ───────────────────────────────────────────────────────────────
export interface TaskWithDetails {
id: string;
title: string;
description: string | null;
type: TaskType;
status: TaskStatus;
priority: ShotPriority;
dueDate: Date | null;
estimatedHours: number | null;
sortOrder: number;
shotId: string | null;
assetId: string | null;
assignedArtistId: string | null;
createdById: string;
projectId: string;
createdAt: Date;
updatedAt: Date;
assignedArtist: {
id: string;
name: string | null;
email: string;
image: string | null;
} | null;
createdBy: {
id: string;
name: string | null;
email: string;
};
_count?: {
versions: number;
};
}
export interface AssetWithDetails {
id: string;
projectId: string;
assetCode: string;
name: string;
description: string | null;
status: ShotStatus;
priority: ShotPriority;
leadId: string | null;
dueDate: Date | null;
createdAt: Date;
updatedAt: Date;
lead: {
id: string;
name: string | null;
email: string;
image: string | null;
} | null;
tasks: TaskWithDetails[];
_count?: {
tasks: number;
};
}
// ── Dashboard Types ──────────────────────────────────────────────────────────
/**
* Dashboard headline numbers. Every one is derived from pipeline state and
* scoped to live productions — see lib/dashboard/ops.ts. The previous shape
* carried task due/overdue counts, which read a column nothing writes.
*/
export interface DashboardStats {
blocked: number;
inQc: number;
readyToDeliver: number;
awaitingClientReview: number;
activeProjects: number;
}
// ── Notification Badge ────────────────────────────────────────────────────────
export interface NotificationItem {
id: string;
type: string;
title: string;
message: string;
data: Record<string, unknown> | null;
isRead: boolean;
createdAt: Date;
}
// ── Next-Auth session extension ──────────────────────────────────────────────
declare module "next-auth" {
interface Session {
user: {
id: string;
role: Role;
mustChangePassword: boolean;
name?: string | null;
email?: string | null;
image?: string | null;
};
}
interface User {
role: Role;
mustChangePassword?: boolean;
}
}