Edit Project dialog's Owner/Project Manager field is now a dropdown
of real users on the system instead of a text box — so the value is
a FK to a real User row rather than a hand-typed string that drifts
from the roster.
Schema:
- Project.requestorUserId (nullable FK → User) + relation
"ProjectOwner" on both sides. Migration 20260425000000_project_owner_fk.
- Existing freeform `requestor` column stays put. It's now the
FALLBACK for ingest-sourced rows where the upstream Owner name
doesn't match a user on the system yet; the UI prefers the linked
user's display name when one exists.
Plumbing:
- createProjectSchema accepts requestorUserId.
- project-service listProjects + getProject both include
requestorUser { id, name, email } so the UI doesn't need a second
round-trip.
UI:
- ProjectFormDialog swaps the Input for a Select populated by
useUsers(). "— Unassigned —" at the top clears the link. When a
project has a legacy freeform requestor and no linked user, the
dialog shows it as a hint ("Currently unlinked (from intake):
<name> — pick a user from the list to replace") so admins can
see what came in from the XLSX/webhook and bind it.
- Project detail view + list view both prefer
requestorUser.name ?? requestorUser.email ?? requestor for the
"Owner" column. Sort key on the list uses the same chain.
981 lines
31 KiB
Text
981 lines
31 KiB
Text
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ─── Enums ──────────────────────────────────────────────
|
|
|
|
enum Role {
|
|
ADMIN
|
|
PRODUCER
|
|
ARTIST
|
|
CLIENT_VIEWER
|
|
}
|
|
|
|
enum ProjectStatus {
|
|
PIPELINE
|
|
ACTIVE
|
|
ON_HOLD
|
|
COMPLETED
|
|
CANCELED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum BriefStatus {
|
|
PENDING // just arrived
|
|
REVIEWING // producer is triaging
|
|
ACCEPTED // approved but not yet a project
|
|
REJECTED // declined
|
|
CONVERTED // promoted to a Project (see convertedProjectId)
|
|
ARCHIVED
|
|
}
|
|
|
|
enum Priority {
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
URGENT
|
|
}
|
|
|
|
enum DeliverableStatus {
|
|
NOT_STARTED
|
|
IN_PROGRESS
|
|
IN_REVIEW
|
|
APPROVED
|
|
ON_HOLD
|
|
}
|
|
|
|
enum StageStatus {
|
|
BLOCKED
|
|
NOT_STARTED
|
|
IN_PROGRESS
|
|
IN_REVIEW
|
|
CHANGES_REQUESTED
|
|
APPROVED
|
|
DELIVERED
|
|
SKIPPED
|
|
}
|
|
|
|
enum RevisionStatus {
|
|
SUBMITTED
|
|
IN_REVIEW
|
|
CHANGES_REQUESTED
|
|
APPROVED
|
|
}
|
|
|
|
enum NotificationType {
|
|
ASSIGNMENT
|
|
STATUS_CHANGE
|
|
REVISION_SUBMITTED
|
|
REVISION_FEEDBACK
|
|
COMMENT
|
|
DEADLINE_APPROACHING
|
|
DEADLINE_OVERDUE
|
|
STAGE_UNBLOCKED
|
|
}
|
|
|
|
enum AssignmentRole {
|
|
LEAD
|
|
SUPPORT
|
|
}
|
|
|
|
enum SkillLevel {
|
|
JUNIOR
|
|
INTERMEDIATE
|
|
SENIOR
|
|
LEAD
|
|
}
|
|
|
|
// ─── RBAC ──────────────────────────────────────────────
|
|
|
|
model OrgRolePermission {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
role Role
|
|
permission Permission
|
|
|
|
@@unique([organizationId, role, permission])
|
|
@@index([organizationId])
|
|
@@map("org_role_permissions")
|
|
}
|
|
|
|
// ─── Organization ───────────────────────────────────────
|
|
|
|
model Organization {
|
|
id String @id @default(cuid())
|
|
name String
|
|
domain String @unique
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
users User[]
|
|
projects Project[]
|
|
automationRules AutomationRule[]
|
|
rolePermissions OrgRolePermission[]
|
|
pipelineTemplates PipelineTemplate[]
|
|
deliverables Deliverable[]
|
|
deliverableStages DeliverableStage[]
|
|
invitations Invitation[]
|
|
customFieldDefs CustomFieldDefinition[]
|
|
notificationRules NotificationRule[]
|
|
clientTeams ClientTeam[]
|
|
pods Pod[]
|
|
resourceBookings ResourceBooking[]
|
|
briefs Brief[]
|
|
|
|
@@map("organizations")
|
|
}
|
|
|
|
// ─── Auth.js models ─────────────────────────────────────
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
role Role @default(ARTIST)
|
|
department String?
|
|
maxCapacity Int @default(5)
|
|
|
|
// Local auth (MVP) — Entra SSO coexists via nullable passwordHash
|
|
passwordHash String?
|
|
passwordResetToken String? @unique
|
|
passwordResetExpires DateTime?
|
|
mustChangePassword Boolean @default(true)
|
|
lastLoginAt DateTime?
|
|
isExternal Boolean @default(false) // fast filter for CLIENT_VIEWER users
|
|
|
|
organizationId String?
|
|
organization Organization? @relation(fields: [organizationId], references: [id])
|
|
|
|
homePodId String?
|
|
homePod Pod? @relation("PodHome", fields: [homePodId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
accounts Account[]
|
|
sessions Session[]
|
|
assignments StageAssignment[]
|
|
comments Comment[]
|
|
notifications Notification[]
|
|
skills UserSkill[]
|
|
searchLogs SearchLog[]
|
|
automationRules AutomationRule[] @relation("AutomationCreator")
|
|
chatMessages ChatMessage[]
|
|
invitationsSent Invitation[] @relation("InvitedBy")
|
|
annotations Annotation[]
|
|
clientTeams ClientTeamMembership[]
|
|
podsLed Pod[] @relation("PodLead")
|
|
bookings ResourceBooking[] @relation("BookingResource")
|
|
bookingsCreated ResourceBooking[] @relation("BookingCreator")
|
|
attachmentsCreated DeliverableAttachment[]
|
|
projectsOwned Project[] @relation("ProjectOwner")
|
|
|
|
@@index([homePodId])
|
|
@@index([isExternal])
|
|
@@map("users")
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
@@map("accounts")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("sessions")
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
@@map("verification_tokens")
|
|
}
|
|
|
|
// ─── Pipeline Templates (seed data) ────────────────────
|
|
|
|
model PipelineStageTemplate {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
slug String @unique
|
|
order Int @unique
|
|
isCriticalGate Boolean @default(false)
|
|
isOptional Boolean @default(false)
|
|
description String?
|
|
estimatedDays Float?
|
|
|
|
dependsOn PipelineStageDependency[] @relation("DependsOnStage")
|
|
dependedBy PipelineStageDependency[] @relation("PrerequisiteStage")
|
|
|
|
deliverableStages DeliverableStage[]
|
|
skillRequirements StageSkillRequirement[]
|
|
|
|
@@map("pipeline_stage_templates")
|
|
}
|
|
|
|
model PipelineStageDependency {
|
|
id String @id @default(cuid())
|
|
stageId String
|
|
prerequisiteId String
|
|
|
|
stage PipelineStageTemplate @relation("DependsOnStage", fields: [stageId], references: [id])
|
|
prerequisite PipelineStageTemplate @relation("PrerequisiteStage", fields: [prerequisiteId], references: [id])
|
|
|
|
@@unique([stageId, prerequisiteId])
|
|
@@map("pipeline_stage_dependencies")
|
|
}
|
|
|
|
// ─── Dynamic Pipeline Templates (org-scoped) ───────────
|
|
|
|
model PipelineTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String?
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
isArchived Boolean @default(false)
|
|
isDefault Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
stages PipelineStageDefinition[]
|
|
projects Project[]
|
|
|
|
@@unique([organizationId, name])
|
|
@@index([organizationId])
|
|
@@map("pipeline_templates")
|
|
}
|
|
|
|
model PipelineStageDefinition {
|
|
id String @id @default(cuid())
|
|
pipelineId String
|
|
pipeline PipelineTemplate @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
|
|
name String
|
|
slug String
|
|
order Int
|
|
isCriticalGate Boolean @default(false)
|
|
isOptional Boolean @default(false)
|
|
description String?
|
|
estimatedDays Float?
|
|
color String?
|
|
customStatuses Json?
|
|
|
|
dependsOn PipelineStageDependencyV2[] @relation("DependsOnStageV2")
|
|
dependedBy PipelineStageDependencyV2[] @relation("PrerequisiteStageV2")
|
|
|
|
// Declared "rework" / pushback paths — explicit opt-in backward
|
|
// transitions for rejection handling. Example: from Final Approval you
|
|
// can push back to In Progress Creative when something fails approval.
|
|
// Without an entry here a stage can ONLY move forward through the
|
|
// pipeline (along with the existing dependsOn graph).
|
|
reworkFrom PipelineStageRework[] @relation("ReworkFromStage")
|
|
reworkTo PipelineStageRework[] @relation("ReworkToStage")
|
|
|
|
deliverableStages DeliverableStage[]
|
|
deliverableAttachments DeliverableAttachment[]
|
|
|
|
@@unique([pipelineId, slug])
|
|
@@unique([pipelineId, order])
|
|
@@map("pipeline_stage_definitions")
|
|
}
|
|
|
|
model PipelineStageDependencyV2 {
|
|
id String @id @default(cuid())
|
|
stageId String
|
|
prerequisiteId String
|
|
|
|
stage PipelineStageDefinition @relation("DependsOnStageV2", fields: [stageId], references: [id], onDelete: Cascade)
|
|
prerequisite PipelineStageDefinition @relation("PrerequisiteStageV2", fields: [prerequisiteId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([stageId, prerequisiteId])
|
|
@@map("pipeline_stage_dependencies_v2")
|
|
}
|
|
|
|
// Explicit backward "rework" transitions on a pipeline template.
|
|
// Distinct from dependsOn (which captures forward prerequisites): this
|
|
// table captures "from stage X you are allowed to push a deliverable
|
|
// back to stage Y". Board drag validates drops against this list.
|
|
model PipelineStageRework {
|
|
id String @id @default(cuid())
|
|
fromStageId String
|
|
toStageId String
|
|
|
|
fromStage PipelineStageDefinition @relation("ReworkFromStage", fields: [fromStageId], references: [id], onDelete: Cascade)
|
|
toStage PipelineStageDefinition @relation("ReworkToStage", fields: [toStageId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([fromStageId, toStageId])
|
|
@@index([fromStageId])
|
|
@@map("pipeline_stage_reworks")
|
|
}
|
|
|
|
// ─── Project ────────────────────────────────────────────
|
|
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
projectCode String @unique
|
|
name String
|
|
description String?
|
|
status ProjectStatus @default(ACTIVE)
|
|
priority Priority @default(MEDIUM)
|
|
startDate DateTime?
|
|
dueDate DateTime?
|
|
businessUnit String?
|
|
formFactor String?
|
|
codeName String?
|
|
npiOrRefresh String?
|
|
quarter String?
|
|
// Freeform owner name from XLSX/webhook intake. Used as a fallback
|
|
// label when no matching user exists on the system (e.g. the upstream
|
|
// Owner column contains a client-side name). Once we can link it to
|
|
// a real user, populate requestorUserId and display that instead.
|
|
requestor String?
|
|
requestorUserId String?
|
|
workfrontId String?
|
|
omgCode String?
|
|
bmtId String?
|
|
estimatedCost Float?
|
|
actualCost Float?
|
|
agency String?
|
|
|
|
// Dow-specific: upstream OMG job number (canonical key for XLSX + webhook ingest)
|
|
omgJobNumber String? @unique
|
|
|
|
// pgvector embedding for semantic search (raw SQL — Prisma can't query this directly)
|
|
embedding Unsupported("vector(768)")?
|
|
customFields Json?
|
|
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id])
|
|
|
|
clientTeamId String?
|
|
clientTeam ClientTeam? @relation(fields: [clientTeamId], references: [id])
|
|
|
|
requestorUser User? @relation("ProjectOwner", fields: [requestorUserId], references: [id])
|
|
|
|
pipelineTemplateId String?
|
|
pipelineTemplate PipelineTemplate? @relation(fields: [pipelineTemplateId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
deliverables Deliverable[]
|
|
convertedFrom Brief[] @relation("BriefConvertedProject")
|
|
|
|
@@index([organizationId])
|
|
@@index([clientTeamId])
|
|
@@index([pipelineTemplateId])
|
|
@@index([status])
|
|
@@map("projects")
|
|
}
|
|
|
|
// ─── Deliverable ────────────────────────────────────────
|
|
|
|
model Deliverable {
|
|
id String @id @default(cuid())
|
|
name String
|
|
status DeliverableStatus @default(NOT_STARTED)
|
|
priority Priority @default(MEDIUM)
|
|
dueDate DateTime?
|
|
notes String?
|
|
cmfSku String?
|
|
assetCount Int?
|
|
requestedDueDate DateTime?
|
|
plannedDeliveryDate DateTime?
|
|
actualDeliveryDate DateTime?
|
|
wfInputDate DateTime?
|
|
|
|
// pgvector embedding for semantic search (raw SQL — Prisma can't query this directly)
|
|
embedding Unsupported("vector(768)")?
|
|
customFields Json?
|
|
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
organizationId String?
|
|
organization Organization? @relation(fields: [organizationId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
stages DeliverableStage[]
|
|
attachments DeliverableAttachment[]
|
|
|
|
@@index([projectId])
|
|
@@index([organizationId])
|
|
@@index([status])
|
|
@@map("deliverables")
|
|
}
|
|
|
|
// ─── Deliverable Attachments (files + external links) ───
|
|
//
|
|
// Producers upload PDFs, design exports, and other asset files; they
|
|
// also paste Figma / Drive / Dropbox links that should live alongside.
|
|
// Two kinds share one row: file (uploaded to /data/uploads/deliverables)
|
|
// vs link (external URL). An optional stageDefinitionId tags an asset
|
|
// to a specific pipeline stage; null means it applies to the whole
|
|
// deliverable.
|
|
model DeliverableAttachment {
|
|
id String @id @default(cuid())
|
|
deliverableId String
|
|
deliverable Deliverable @relation(fields: [deliverableId], references: [id], onDelete: Cascade)
|
|
|
|
kind String // "file" | "link"
|
|
title String
|
|
url String // local /api/uploads/... or external https://...
|
|
|
|
mimeType String?
|
|
fileSize Int?
|
|
thumbnailUrl String?
|
|
|
|
stageDefinitionId String?
|
|
stageDefinition PipelineStageDefinition? @relation(fields: [stageDefinitionId], references: [id])
|
|
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id])
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([deliverableId])
|
|
@@index([stageDefinitionId])
|
|
@@map("deliverable_attachments")
|
|
}
|
|
|
|
// ─── Deliverable Stage (instance per deliverable) ───────
|
|
|
|
model DeliverableStage {
|
|
id String @id @default(cuid())
|
|
status StageStatus @default(BLOCKED)
|
|
revisionRound Int @default(0)
|
|
startDate DateTime?
|
|
completedDate DateTime?
|
|
dueDate DateTime?
|
|
notes String?
|
|
subStatus String?
|
|
manualSchedule Boolean @default(false)
|
|
scheduleConflict Boolean @default(false)
|
|
scheduleDelta Int?
|
|
|
|
deliverableId String
|
|
deliverable Deliverable @relation(fields: [deliverableId], references: [id], onDelete: Cascade)
|
|
|
|
templateId String
|
|
template PipelineStageTemplate @relation(fields: [templateId], references: [id])
|
|
|
|
stageDefinitionId String?
|
|
stageDefinition PipelineStageDefinition? @relation(fields: [stageDefinitionId], references: [id])
|
|
|
|
organizationId String?
|
|
organization Organization? @relation(fields: [organizationId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
assignments StageAssignment[]
|
|
revisions Revision[]
|
|
comments Comment[]
|
|
|
|
@@unique([deliverableId, stageDefinitionId])
|
|
@@index([deliverableId])
|
|
@@index([stageDefinitionId])
|
|
@@index([organizationId])
|
|
@@index([status])
|
|
@@map("deliverable_stages")
|
|
}
|
|
|
|
// ─── Stage Assignment ───────────────────────────────────
|
|
|
|
model StageAssignment {
|
|
id String @id @default(cuid())
|
|
role AssignmentRole? @default(LEAD)
|
|
|
|
deliverableStageId String
|
|
deliverableStage DeliverableStage @relation(fields: [deliverableStageId], references: [id], onDelete: Cascade)
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([deliverableStageId, userId])
|
|
@@index([userId])
|
|
@@map("stage_assignments")
|
|
}
|
|
|
|
// ─── Revision ───────────────────────────────────────────
|
|
|
|
model Revision {
|
|
id String @id @default(cuid())
|
|
roundNumber Int
|
|
status RevisionStatus @default(SUBMITTED)
|
|
feedbackNotes String?
|
|
internalNotes String?
|
|
attachments Json?
|
|
|
|
deliverableStageId String
|
|
deliverableStage DeliverableStage @relation(fields: [deliverableStageId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
annotations Annotation[]
|
|
|
|
@@index([deliverableStageId])
|
|
@@map("revisions")
|
|
}
|
|
|
|
// ─── Comment ────────────────────────────────────────────
|
|
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
content String @db.Text
|
|
|
|
deliverableStageId String
|
|
deliverableStage DeliverableStage @relation(fields: [deliverableStageId], references: [id], onDelete: Cascade)
|
|
|
|
authorId String
|
|
author User @relation(fields: [authorId], references: [id])
|
|
|
|
parentId String?
|
|
parent Comment? @relation("CommentThread", fields: [parentId], references: [id])
|
|
replies Comment[] @relation("CommentThread")
|
|
|
|
annotations Annotation[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([deliverableStageId])
|
|
@@index([parentId])
|
|
@@map("comments")
|
|
}
|
|
|
|
// ─── Notification ───────────────────────────────────────
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
type NotificationType
|
|
title String
|
|
message String
|
|
link String?
|
|
isRead Boolean @default(false)
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId, isRead])
|
|
@@map("notifications")
|
|
}
|
|
|
|
// ─── Skills & Capacity (Phase 6) ────────────────────────
|
|
|
|
model Skill {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
createdAt DateTime @default(now())
|
|
|
|
users UserSkill[]
|
|
stageRequirements StageSkillRequirement[]
|
|
|
|
@@map("skills")
|
|
}
|
|
|
|
model UserSkill {
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
skillId String
|
|
skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade)
|
|
level SkillLevel @default(INTERMEDIATE)
|
|
|
|
@@id([userId, skillId])
|
|
@@map("user_skills")
|
|
}
|
|
|
|
model StageSkillRequirement {
|
|
stageTemplateId String
|
|
stageTemplate PipelineStageTemplate @relation(fields: [stageTemplateId], references: [id], onDelete: Cascade)
|
|
skillId String
|
|
skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade)
|
|
importance Int @default(1) // 1=nice-to-have, 2=important, 3=required
|
|
|
|
@@id([stageTemplateId, skillId])
|
|
@@map("stage_skill_requirements")
|
|
}
|
|
|
|
// ─── Automation Engine (Phase 7.1) ──────────────────────
|
|
|
|
model AutomationRule {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String?
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id])
|
|
isEnabled Boolean @default(true)
|
|
trigger Json // { event, conditions[] }
|
|
actions Json // [{ type, params }]
|
|
createdById String
|
|
createdBy User @relation("AutomationCreator", fields: [createdById], references: [id])
|
|
executions AutomationExecution[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([organizationId])
|
|
@@index([isEnabled])
|
|
@@map("automation_rules")
|
|
}
|
|
|
|
model AutomationExecution {
|
|
id String @id @default(cuid())
|
|
ruleId String
|
|
rule AutomationRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
|
|
triggeredBy Json // the event payload that triggered execution
|
|
result Json // what actions were taken + outcomes
|
|
status ExecutionStatus
|
|
error String?
|
|
executedAt DateTime @default(now())
|
|
|
|
@@index([ruleId])
|
|
@@index([executedAt])
|
|
@@map("automation_executions")
|
|
}
|
|
|
|
enum ExecutionStatus {
|
|
SUCCESS
|
|
PARTIAL_FAILURE
|
|
FAILURE
|
|
}
|
|
|
|
enum Permission {
|
|
PROJECT_CREATE
|
|
PROJECT_UPDATE
|
|
PROJECT_DELETE
|
|
PROJECT_VIEW
|
|
DELIVERABLE_VIEW
|
|
DELIVERABLE_CREATE
|
|
DELIVERABLE_UPDATE
|
|
DELIVERABLE_DELETE
|
|
STAGE_VIEW
|
|
STAGE_UPDATE
|
|
STAGE_UPDATE_STATUS
|
|
STAGE_ASSIGN
|
|
STAGE_SCHEDULE
|
|
REVISION_CREATE
|
|
REVISION_UPDATE
|
|
REVISION_REVIEW
|
|
COMMENT_CREATE
|
|
COMMENT_DELETE
|
|
COMMENT_DELETE_ANY
|
|
PIPELINE_MANAGE
|
|
USER_MANAGE
|
|
ROLE_MANAGE
|
|
ORG_SETTINGS
|
|
AUTOMATION_MANAGE
|
|
FIELD_CUSTOMIZE
|
|
CLIENT_TEAM_MANAGE
|
|
POD_MANAGE
|
|
}
|
|
|
|
// ─── Chat History (CLI Anything) ────────────────────────
|
|
|
|
model ChatMessage {
|
|
id String @id @default(cuid())
|
|
sessionId String
|
|
role String // "user" | "assistant" | "system"
|
|
content String @db.Text
|
|
toolCalls Json? // tool calls made by assistant
|
|
toolResults Json? // results of tool execution
|
|
metadata Json? // context: active project, etc.
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
organizationId String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([sessionId])
|
|
@@index([userId])
|
|
@@map("chat_messages")
|
|
}
|
|
|
|
// ─── Custom Fields ──────────────────────────────────────
|
|
|
|
model CustomFieldDefinition {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
entityType String // "PROJECT" | "DELIVERABLE"
|
|
fieldName String
|
|
fieldType String // "TEXT" | "NUMBER" | "DATE" | "SELECT" | "BOOLEAN"
|
|
fieldOptions Json? // For SELECT type: { options: string[] }
|
|
isRequired Boolean @default(false)
|
|
order Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([organizationId, entityType, fieldName])
|
|
@@index([organizationId])
|
|
@@map("custom_field_definitions")
|
|
}
|
|
|
|
// ─── Notification Rules ─────────────────────────────────
|
|
|
|
model NotificationRule {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
name String
|
|
isEnabled Boolean @default(true)
|
|
event String // e.g. "STAGE_STATUS_CHANGE", "DEADLINE_APPROACHING", "REVISION_SUBMITTED"
|
|
conditions Json? // { field: string, operator: string, value: any }[]
|
|
channels Json // ["IN_APP", "EMAIL"]
|
|
recipientRoles Json // ["ADMIN", "PRODUCER"] or ["ASSIGNEE"]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([organizationId])
|
|
@@index([event])
|
|
@@map("notification_rules")
|
|
}
|
|
|
|
// ─── Invitations ────────────────────────────────────────
|
|
|
|
model Invitation {
|
|
id String @id @default(cuid())
|
|
email String
|
|
role Role @default(ARTIST)
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
invitedById String
|
|
invitedBy User @relation("InvitedBy", fields: [invitedById], references: [id])
|
|
token String @unique @default(cuid())
|
|
expiresAt DateTime
|
|
acceptedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([email, organizationId])
|
|
@@index([organizationId])
|
|
@@index([token])
|
|
@@map("invitations")
|
|
}
|
|
|
|
// ─── Semantic Search (Phase 8.4) ────────────────────────
|
|
|
|
model SearchLog {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
query String
|
|
resultCount Int @default(0)
|
|
clickedId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@map("search_logs")
|
|
}
|
|
|
|
// ─── Annotations (Visual Review) ────────────────────────
|
|
|
|
enum AnnotationType {
|
|
RECTANGLE
|
|
ELLIPSE
|
|
ARROW
|
|
FREEHAND
|
|
TEXT
|
|
PIN
|
|
SCREENSHOT
|
|
}
|
|
|
|
// ─── Annotation ─────────────────────────────────────────
|
|
|
|
model Annotation {
|
|
id String @id @default(cuid())
|
|
commentId String
|
|
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
revisionId String
|
|
revision Revision @relation(fields: [revisionId], references: [id], onDelete: Cascade)
|
|
type AnnotationType
|
|
data Json
|
|
imageX Float
|
|
imageY Float
|
|
timestampSeconds Float? // null for image annotations, set for video annotations
|
|
frameThumbnailUrl String? // cached frame thumbnail for the annotation's moment
|
|
createdById String
|
|
createdBy User @relation(fields: [createdById], references: [id])
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([commentId])
|
|
@@index([revisionId])
|
|
@@index([revisionId, timestampSeconds])
|
|
@@map("annotations")
|
|
}
|
|
|
|
// ─── Dow: Client Teams (visibility grouping) ───────────
|
|
|
|
model ClientTeam {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
name String
|
|
slug String
|
|
createdAt DateTime @default(now())
|
|
|
|
userMemberships ClientTeamMembership[]
|
|
projects Project[]
|
|
briefs Brief[]
|
|
|
|
@@unique([organizationId, slug])
|
|
@@index([organizationId])
|
|
@@map("client_teams")
|
|
}
|
|
|
|
model ClientTeamMembership {
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
clientTeamId String
|
|
clientTeam ClientTeam @relation(fields: [clientTeamId], references: [id], onDelete: Cascade)
|
|
isPrimary Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([userId, clientTeamId])
|
|
@@index([clientTeamId])
|
|
@@map("client_team_memberships")
|
|
}
|
|
|
|
// ─── Dow: Incoming Briefs (pre-project intake) ──────────
|
|
//
|
|
// A Brief is a request that has arrived but hasn't yet been triaged into
|
|
// a Project. Three intake paths all land here: manual create (producer
|
|
// types it in), REST API (`POST /api/briefs`), or webhook
|
|
// (`POST /api/webhooks/briefs`). Webhook payloads are idempotent on the
|
|
// `externalId` column so replays from upstream systems are safe.
|
|
//
|
|
// Once a brief is approved and matched to work, it gets "promoted" to a
|
|
// Project — the status flips to CONVERTED and `convertedProjectId` links
|
|
// back to the new Project. That keeps the audit trail without requiring
|
|
// the Brief row to disappear.
|
|
|
|
model Brief {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
|
|
title String
|
|
description String?
|
|
|
|
// Source tracking — "manual" | "api" | "webhook:<system>" so we can
|
|
// report on where briefs come from without needing a separate enum.
|
|
source String @default("manual")
|
|
|
|
// External idempotency key (from webhook/API). Unique per org so the
|
|
// same upstream system can't create duplicates on replay.
|
|
externalId String?
|
|
|
|
// Requestor info — client-side contact. Freeform because intake
|
|
// systems can't be relied on to have user rows.
|
|
requestorName String?
|
|
requestorEmail String?
|
|
|
|
status BriefStatus @default(PENDING)
|
|
priority Priority @default(MEDIUM)
|
|
|
|
requestedDueDate DateTime?
|
|
|
|
clientTeamId String?
|
|
clientTeam ClientTeam? @relation(fields: [clientTeamId], references: [id])
|
|
|
|
// When promoted to a Project, fill in convertedProjectId (CONVERTED status).
|
|
convertedProjectId String?
|
|
convertedProject Project? @relation("BriefConvertedProject", fields: [convertedProjectId], references: [id])
|
|
|
|
// Pass-through field for anything in the incoming payload we didn't map.
|
|
rawPayload Json?
|
|
|
|
receivedAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([organizationId, externalId])
|
|
@@index([organizationId])
|
|
@@index([status])
|
|
@@index([clientTeamId])
|
|
@@map("briefs")
|
|
}
|
|
|
|
// ─── Dow: Production Pods (capacity grouping) ──────────
|
|
|
|
model Pod {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
name String
|
|
slug String
|
|
leadUserId String?
|
|
leadUser User? @relation("PodLead", fields: [leadUserId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
|
|
members User[] @relation("PodHome")
|
|
|
|
@@unique([organizationId, slug])
|
|
@@index([organizationId])
|
|
@@map("pods")
|
|
}
|
|
|
|
// ─── Dow: Resource bookings (daily capacity grid) ──────
|
|
// One row per (user, date, jobNumber) — matches the Resources.html
|
|
// prototype's model: a producer assigns N hours of a given job to a
|
|
// person on a specific day. Multiple rows per (user, date) are expected
|
|
// (split days across jobs). jobNumber is a freeform string so it can
|
|
// hold Project.omgJobNumber, Project.projectCode, or an ad-hoc scratch
|
|
// label — the capacity planner doesn't care what project the hours are
|
|
// against, just that the total for the day/week is tracked.
|
|
model ResourceBooking {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
userId String
|
|
user User @relation("BookingResource", fields: [userId], references: [id], onDelete: Cascade)
|
|
date DateTime // date-only — we store 00:00:00 UTC
|
|
jobNumber String
|
|
hours Float
|
|
note String?
|
|
createdById String
|
|
createdBy User @relation("BookingCreator", fields: [createdById], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([organizationId, date])
|
|
@@index([userId, date])
|
|
@@map("resource_bookings")
|
|
}
|