842a8fe036
Deploy / deploy (push) Successful in 3m34s
"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>
330 lines
9.0 KiB
TypeScript
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;
|
|
}
|
|
}
|