dow-prod-tracker/prisma/schema.prisma
DJP 18ae429924 Dow-ify project form + seed placeholder roster + Resource Manager + docs
You flagged three concrete gaps after the deploy went live — all
addressed in this commit, plus the API + how-to docs you asked for.

A) Create Project dialog was still HP-centric
   Placeholders like "HP Envy x360 Renders" / "HP-2026-001" / "NPI /
   Refresh" / "Form Factor" etc. bore no relation to Dow's actual XLSX
   columns and the form had no ClientTeam selector — so any
   admin-created project was orphaned from the visibility layer.
   - src/lib/validators/project.ts: added clientTeamId + omgJobNumber;
     status enum now includes PIPELINE and CANCELED
   - src/components/projects/project-form-dialog.tsx: rewritten around
     the Dow XLSX schema. Three tabs (Details / Dates / References)
     instead of four. Placeholders reference real Dow values
     (Celena / Yzabella etc. for Owner, 2337959 for OMG #, Brand / Events
     etc. for Team, Copywriting/Display/... for Category). ClientTeam
     selector populated from /api/client-teams with a "no teams — add
     one in Settings" fallback. Category is a typed enum dropdown with
     the 8 XLSX values. Risk/Priority wording mirrors the XLSX labels
     (Priority = URGENT). Dropped HP-only fields from the UI
     (formFactor, codeName, npiOrRefresh, businessUnit placeholder,
     agency, Financial tab, Workfront ID placeholder). Legacy fields
     are still in the Zod schema for back-compat but not rendered.

B) Users invisible because only the admin was seeded
   The plan flagged "real Dow/Oliver roster — open question" and we
   never got the list, so the seed only created admin@dowjones.com.
   prisma/seed-dow.ts now also creates the 9 placeholder resources
   from the Resources.html prototype (Alice Chen, Ben Marsh, Cara Wu,
   Dan Koch, Eva Stone, Frank Osei, Grace Lee, Hiro Tanaka, Isla Reeve),
   distributed round-robin across the three placeholder pods. Each has
   role + department + maxCapacity set but no passwordHash, so they
   show up in the UI immediately but can't log in until an admin
   invites them via Settings → Team (which issues a reset link).
   Swap for the real roster whenever Zia delivers it — the emails are
   @example.com so they're safe to delete.

C) Resource Manager page (matching Resources.html)
   New capacity planner UI — daily hours-per-job grid.
   - Schema: new ResourceBooking model { userId, date, jobNumber,
     hours, note, organizationId, createdById }. Migration at
     prisma/migrations/20260421000000_resource_bookings.
   - Validator (src/lib/validators/booking.ts): create + list schemas
     with date-only coercion.
   - Service (src/lib/services/booking-service.ts): week window
     helpers, create/list/delete + known-job-numbers lookup for the
     popover autocomplete.
   - API: GET/POST /api/resources/bookings, DELETE
     /api/resources/bookings/[id], GET /api/resources/job-numbers.
     Writes gated to ADMIN + PRODUCER; reads open to any signed-in
     member of the org (capacity view is a shared studio-level thing,
     not per-team visibility).
   - Hook (src/hooks/use-bookings.ts) with TanStack Query wiring +
     week-scoped cache keys.
   - Page (src/app/(app)/resources/page.tsx) ports the Resources.html
     design to the app's Tailwind + shadcn primitives: Resource × Day
     grid grouped by department, week navigator, click-to-assign
     popover with job-number autocomplete + hour chips (1/2/3/4/6/8 +
     custom), capacity bar per cell, week total column with over-cap
     warning, collapsible role bands. Matches the prototype's
     color-hashed job chips so the same job number gets a consistent
     color across the grid.
   - Sidebar nav: added "Resources" entry next to Workload.

D) Docs — full README + API reference + how-to
   - API.md: complete REST + webhook reference. Three auth modes
     documented (session cookie / X-API-Key / OMG HMAC). XLSX upload
     header map with the Dow XLSX column correspondences. OMG webhook
     has the speculative payload shape + a working bash example that
     signs + sends a request. Common flows at the bottom: bootstrap
     from zero, OMG publishes a status change, update a job from an
     external script.
   - HOWTO.md: end-to-end runbook. Mental model, local dev, prod
     deploy pointers, first-login ritual, add-users flow (UI + API),
     client teams + pods config, XLSX ingest (UI + curl + idempotency
     notes), OMG webhook wiring (secret gen through verification),
     producer daily workflow, client-viewer experience, resource
     planning walk-through, RBAC matrix, common-problems table, and
     "change the model" pointer map for future edits.
   - README.md: top intro now points at API.md / HOWTO.md / DEPLOY.md.

Verified: npx tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:31:19 -04:00

840 lines
25 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 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[]
@@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")
@@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")
deliverableStages DeliverableStage[]
@@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")
}
// ─── 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?
requestor 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])
pipelineTemplateId String?
pipelineTemplate PipelineTemplate? @relation(fields: [pipelineTemplateId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deliverables Deliverable[]
@@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[]
@@index([projectId])
@@index([organizationId])
@@index([status])
@@map("deliverables")
}
// ─── 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[]
@@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: 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")
}