Timezone (all users EST): - lib/utils: hardcode APP_TIME_ZONE (America/New_York) in formatDate/ formatRenewalDate, add formatDateTime and todayInAppTimeZone helpers - Replace every ad-hoc toLocaleDateString/toLocaleString call across the app (task notes, audit log, backups, shape import, renewal groups, client detail) with the shared EST-aware helpers - Fix UTC "today" bug in date-input defaults/min/max (completion date, reminder date) that rolled to the next calendar day after ~7-8pm ET Task provenance: - Task card info icon (now visible to all users, not just privileged) shows full origin: template, level, renewal anchor, due-date math, and who/what generated the task - Include template.daysOffset and creator in task queries Setup N/A: - New setupNaAt/setupNaReason fields on Client; mark/restore UI and API to exclude non-Shape/lost-business clients from the setup queue everywhere it's counted (queue page, API, manager dashboard, metrics gauge) Task generation fixes: - auto-generate: client-level branch now gated on the renewal anchor's (group/policy) createdAt instead of the client's, so pre-go-live clients with new post-go-live groups are no longer skipped forever - New auto-assign-tasks sweep run after every sync to assign advocates to tasks created by paths that don't assign directly (e.g. setup wizard)
582 lines
21 KiB
Text
582 lines
21 KiB
Text
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ============================================
|
|
// User Management & Authentication
|
|
// ============================================
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
entraOid String? @unique @map("entra_oid")
|
|
email String @unique
|
|
displayName String? @map("display_name")
|
|
department String?
|
|
jobTitle String? @map("job_title")
|
|
photoUrl String? @map("photo_url")
|
|
office String?
|
|
isActive Boolean @default(true) @map("is_active")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
userRoles UserRole[]
|
|
createdTasks Task[] @relation("TaskCreatedBy")
|
|
completedTasks Task[] @relation("TaskCompletedBy")
|
|
taskAssignments TaskAssignment[]
|
|
taskNotes TaskNote[] @relation("TaskNoteAuthor")
|
|
createdTemplates TaskTemplate[]
|
|
syncLogs SyncLog[]
|
|
auditLogs AuditLog[]
|
|
notifications Notification[]
|
|
notificationPrefs NotificationPreference[]
|
|
createdPolicyGroups PolicyGroup[]
|
|
advocateClients Client[] @relation("ClientAdvocate")
|
|
clientMemberships ClientMember[]
|
|
shapeImportRuns ShapeImportRun[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Role {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
permissions Json @default("{}")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
userRoles UserRole[]
|
|
entraGroupMappings EntraGroupRoleMapping[]
|
|
|
|
@@map("roles")
|
|
}
|
|
|
|
model UserRole {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
roleId String @map("role_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, roleId])
|
|
@@map("user_roles")
|
|
}
|
|
|
|
model EntraGroupRoleMapping {
|
|
id String @id @default(cuid())
|
|
entraGroupId String @map("entra_group_id")
|
|
roleId String @map("role_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([entraGroupId, roleId])
|
|
@@map("entra_group_role_mappings")
|
|
}
|
|
|
|
// ============================================
|
|
// Client & Policy Management
|
|
// ============================================
|
|
|
|
model Designation {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
color String
|
|
rules String?
|
|
displayOrder Int @map("display_order")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
afwAnotId String? @unique @map("afw_anot_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
clientsDesignation1 Client[] @relation("ClientDesignation1")
|
|
clientsDesignation2 Client[] @relation("ClientDesignation2")
|
|
taskTemplates TaskTemplate[]
|
|
|
|
@@map("designations")
|
|
}
|
|
|
|
model Client {
|
|
id String @id @default(cuid())
|
|
amsCustomerId String @unique @map("ams_customer_id")
|
|
/// AFW's short numeric customer/account number (AFW_Customer.CustNo) — distinct from
|
|
/// amsCustomerId (AFW's internal GUID). This is what ImageRight's FileNumberPart1 expects.
|
|
amsCustomerNumber Int? @map("ams_customer_number")
|
|
name String
|
|
addressLine1 String? @map("address_line1")
|
|
addressLine2 String? @map("address_line2")
|
|
city String?
|
|
state String?
|
|
zipCode String? @map("zip_code")
|
|
phone String?
|
|
email String?
|
|
producerCode String? @map("producer_code")
|
|
amsCreatedAt DateTime? @map("ams_created_at")
|
|
amsModifiedAt DateTime? @map("ams_modified_at")
|
|
designationId String? @map("designation_id")
|
|
designation2Id String? @map("designation2_id")
|
|
claimsAdvocateId String? @map("claims_advocate_id")
|
|
parentClientId String? @map("parent_client_id")
|
|
notes String? @db.Text
|
|
customFields Json @default("{}") @map("custom_fields")
|
|
renewalDate DateTime? @map("renewal_date")
|
|
setupCompletedAt DateTime? @map("setup_completed_at")
|
|
setupNaAt DateTime? @map("setup_na_at")
|
|
setupNaReason String? @map("setup_na_reason")
|
|
lastSyncedAt DateTime? @map("last_synced_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
designation Designation? @relation("ClientDesignation1", fields: [designationId], references: [id])
|
|
designation2 Designation? @relation("ClientDesignation2", fields: [designation2Id], references: [id])
|
|
claimsAdvocate User? @relation("ClientAdvocate", fields: [claimsAdvocateId], references: [id])
|
|
parentClient Client? @relation("ClientParent", fields: [parentClientId], references: [id])
|
|
subsidiaries Client[] @relation("ClientParent")
|
|
policies Policy[]
|
|
tasks Task[]
|
|
policyGroups PolicyGroup[]
|
|
members ClientMember[]
|
|
contacts ClientContact[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@index([name])
|
|
@@index([designationId])
|
|
@@index([designation2Id])
|
|
@@index([claimsAdvocateId])
|
|
@@index([parentClientId])
|
|
@@map("clients")
|
|
}
|
|
|
|
model ClientContact {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
label String
|
|
name String
|
|
title String?
|
|
phone String?
|
|
mobilePhone String? @map("mobile_phone")
|
|
email String?
|
|
notes String? @db.Text
|
|
source String @default("manual")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([clientId, name, label, source], name: "client_contact_dedup")
|
|
@@index([clientId])
|
|
@@map("client_contacts")
|
|
}
|
|
|
|
model ClientMember {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
userId String @map("user_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([clientId, userId])
|
|
@@index([clientId])
|
|
@@index([userId])
|
|
@@map("client_members")
|
|
}
|
|
|
|
model PolicyGroup {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
name String
|
|
renewalDate DateTime @map("renewal_date")
|
|
notes String? @db.Text
|
|
createdBy String? @map("created_by")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
creator User? @relation(fields: [createdBy], references: [id])
|
|
policies Policy[]
|
|
tasks Task[]
|
|
|
|
@@index([clientId])
|
|
@@index([renewalDate])
|
|
@@map("policy_groups")
|
|
}
|
|
|
|
model Policy {
|
|
id String @id @default(cuid())
|
|
amsPolicyId String @unique @map("ams_policy_id")
|
|
clientId String @map("client_id")
|
|
policyGroupId String? @map("policy_group_id")
|
|
policyNumber String? @map("policy_number")
|
|
policyType String? @map("policy_type")
|
|
effectiveDate DateTime? @map("effective_date")
|
|
expirationDate DateTime @map("expiration_date")
|
|
carrierName String? @map("carrier_name")
|
|
writingCompanyName String? @map("writing_company_name")
|
|
premiumAmount Decimal? @map("premium_amount") @db.Decimal(12, 2)
|
|
status String?
|
|
billMethod String? @map("bill_method")
|
|
businessType String? @map("business_type")
|
|
department String?
|
|
executiveName String? @map("executive_name")
|
|
csrName String? @map("csr_name")
|
|
additionalRep1 String? @map("additional_rep1")
|
|
additionalRep2 String? @map("additional_rep2")
|
|
additionalExec1 String? @map("additional_exec1")
|
|
additionalExec2 String? @map("additional_exec2")
|
|
amsCreatedAt DateTime? @map("ams_created_at")
|
|
amsModifiedAt DateTime? @map("ams_modified_at")
|
|
lastSyncedAt DateTime? @map("last_synced_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
policyGroup PolicyGroup? @relation(fields: [policyGroupId], references: [id])
|
|
tasks Task[]
|
|
|
|
@@index([clientId])
|
|
@@index([policyGroupId])
|
|
@@index([expirationDate])
|
|
@@index([department])
|
|
@@map("policies")
|
|
}
|
|
|
|
// ============================================
|
|
// Task Management
|
|
// ============================================
|
|
|
|
enum TaskStatus {
|
|
NOT_STARTED
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
BLOCKED
|
|
NA
|
|
CANCELLED
|
|
}
|
|
|
|
enum TaskPriority {
|
|
LOW
|
|
MEDIUM
|
|
HIGH
|
|
URGENT
|
|
}
|
|
|
|
enum TaskTiming {
|
|
PRE_RENEWAL
|
|
POST_RENEWAL
|
|
}
|
|
|
|
enum TaskLevel {
|
|
POLICY
|
|
RENEWAL_GROUP
|
|
BOTH
|
|
CLIENT
|
|
}
|
|
|
|
enum DepartmentType {
|
|
PERSONAL_LINES
|
|
COMMERCIAL_LINES
|
|
CLAIMS
|
|
BENEFITS
|
|
OTHER
|
|
}
|
|
|
|
/// Two-pass audit classification, per imageright_claim_review_audit_methodology.md.
|
|
/// Separates substantive filing compliance from filename/naming-convention compliance.
|
|
enum TaskAuditClassification {
|
|
PASS
|
|
PASS_WITH_NAMING_EXCEPTION
|
|
MANUAL_REVIEW
|
|
FAIL_MISSING
|
|
FAIL_LATE
|
|
FAIL_WRONG_ARTIFACT
|
|
NOT_APPLICABLE
|
|
}
|
|
|
|
model TaskTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String? @db.Text
|
|
department DepartmentType
|
|
timing TaskTiming
|
|
daysOffset Int @map("days_offset")
|
|
defaultPriority TaskPriority @map("default_priority")
|
|
taskGroup String? @map("task_group")
|
|
isActive Boolean @default(true) @map("is_active")
|
|
displayOrder Int? @map("display_order")
|
|
level TaskLevel @default(BOTH)
|
|
designationId String? @map("designation_id")
|
|
policyTypeFilter String? @map("policy_type_filter")
|
|
createdBy String? @map("created_by")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
designation Designation? @relation(fields: [designationId], references: [id])
|
|
creator User? @relation(fields: [createdBy], references: [id])
|
|
tasks Task[]
|
|
|
|
@@index([designationId])
|
|
@@map("task_templates")
|
|
}
|
|
|
|
model Task {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
department DepartmentType
|
|
timing TaskTiming
|
|
daysOffset Int @map("days_offset")
|
|
dueDate DateTime @map("due_date")
|
|
status TaskStatus @default(NOT_STARTED)
|
|
priority TaskPriority
|
|
clientId String @map("client_id")
|
|
policyId String? @map("policy_id")
|
|
policyGroupId String? @map("policy_group_id")
|
|
templateId String? @map("template_id")
|
|
createdBy String? @map("created_by")
|
|
completedAt DateTime? @map("completed_at")
|
|
completedBy String? @map("completed_by")
|
|
notes String? @db.Text
|
|
isAdHoc Boolean @default(false) @map("is_ad_hoc")
|
|
naReason String? @map("na_reason") @db.Text
|
|
cancelledReason String? @map("cancelled_reason") @db.Text
|
|
taskGroup String? @map("task_group")
|
|
imageRightFiled Boolean? @map("image_right_filed")
|
|
reminderDate DateTime? @map("reminder_date")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
policy Policy? @relation(fields: [policyId], references: [id])
|
|
policyGroup PolicyGroup? @relation(fields: [policyGroupId], references: [id])
|
|
template TaskTemplate? @relation(fields: [templateId], references: [id])
|
|
creator User? @relation("TaskCreatedBy", fields: [createdBy], references: [id])
|
|
completer User? @relation("TaskCompletedBy", fields: [completedBy], references: [id])
|
|
assignments TaskAssignment[]
|
|
taskNotes TaskNote[]
|
|
taskAudits TaskAudit[]
|
|
|
|
@@index([clientId])
|
|
@@index([policyId])
|
|
@@index([policyGroupId])
|
|
@@index([status])
|
|
@@index([dueDate])
|
|
@@index([department])
|
|
@@map("tasks")
|
|
}
|
|
|
|
model TaskAssignment {
|
|
id String @id @default(cuid())
|
|
taskId String @map("task_id")
|
|
userId String @map("user_id")
|
|
assignedAt DateTime @default(now()) @map("assigned_at")
|
|
|
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([taskId, userId])
|
|
@@map("task_assignments")
|
|
}
|
|
|
|
/// Result of checking one SHAPE_IR_Filing_Audit_Spec.md checklist item against
|
|
/// ImageRight for a client. Each audit run inserts new rows (history preserved);
|
|
/// the latest row per (clientId, specItemKey) is the current status. Optionally
|
|
/// linked to an existing generated Task, but not required — a spec item with no
|
|
/// matching Task is still auditable and surfaces as a gap.
|
|
model TaskAudit {
|
|
id String @id @default(cuid())
|
|
clientId String @map("client_id")
|
|
taskId String? @map("task_id")
|
|
specItemKey String @map("spec_item_key")
|
|
targetDate DateTime @map("target_date")
|
|
classification TaskAuditClassification
|
|
/// 0-100 metadata confidence score (audit-scoring.ts); null when short-circuited (e.g. no file found).
|
|
confidenceScore Int? @map("confidence_score")
|
|
/// "high" | "medium" | "low" | "none" — bucketed from confidenceScore.
|
|
evidenceConfidence String? @map("evidence_confidence")
|
|
/// Did the matched artifact's own filename/type match the spec's expected pattern exactly?
|
|
filenamePatternCompliant Boolean? @map("filename_pattern_compliant")
|
|
/// True when classification is PASS_WITH_NAMING_EXCEPTION.
|
|
namingException Boolean? @map("naming_exception")
|
|
manualReviewRequired Boolean? @map("manual_review_required")
|
|
matchedDocId String? @map("matched_doc_id")
|
|
matchedDocName String? @map("matched_doc_name")
|
|
matchedDocDate DateTime? @map("matched_doc_date")
|
|
/// Subject of the .msg, when the matched artifact (or its container) was an email.
|
|
msgSubject String? @map("msg_subject")
|
|
msgSentDate DateTime? @map("msg_sent_date")
|
|
/// Filenames of attachments found inside a matched .msg container.
|
|
attachmentFilenames String[] @map("attachment_filenames")
|
|
/// Human-readable summary of what content evidence was found and why (for the audit report).
|
|
contentEvidenceSummary String? @map("content_evidence_summary") @db.Text
|
|
/// Ordered list of {signal, score, detail} scoring reasons, for the audit trail.
|
|
matchedSignals Json? @map("matched_signals")
|
|
folderChecked String? @map("folder_checked")
|
|
errorMessage String? @map("error_message") @db.Text
|
|
runBy String? @map("run_by")
|
|
runAt DateTime @default(now()) @map("run_at")
|
|
|
|
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
|
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
|
runByUser User? @relation(fields: [runBy], references: [id])
|
|
|
|
@@index([clientId])
|
|
@@index([specItemKey])
|
|
@@index([clientId, specItemKey, runAt])
|
|
@@map("task_audits")
|
|
}
|
|
|
|
model TaskNote {
|
|
id String @id @default(cuid())
|
|
taskId String @map("task_id")
|
|
userId String @map("user_id")
|
|
content String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
|
user User @relation("TaskNoteAuthor", fields: [userId], references: [id])
|
|
|
|
@@index([taskId])
|
|
@@map("task_notes")
|
|
}
|
|
|
|
// ============================================
|
|
// Sync & System Management
|
|
// ============================================
|
|
|
|
model SyncLog {
|
|
id String @id @default(cuid())
|
|
syncType String @map("sync_type")
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
status String
|
|
rowsProcessed Int @default(0) @map("rows_processed")
|
|
rowsInserted Int @default(0) @map("rows_inserted")
|
|
rowsUpdated Int @default(0) @map("rows_updated")
|
|
errorMessage String? @map("error_message") @db.Text
|
|
triggeredBy String? @map("triggered_by")
|
|
|
|
user User? @relation(fields: [triggeredBy], references: [id])
|
|
|
|
@@index([syncType])
|
|
@@index([startedAt])
|
|
@@map("sync_logs")
|
|
}
|
|
|
|
model SyncConfig {
|
|
id String @id @default(cuid())
|
|
key String @unique
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("sync_config")
|
|
}
|
|
|
|
model AppSetting {
|
|
key String @id
|
|
value String @db.Text
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("app_settings")
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
userId String? @map("user_id")
|
|
action String
|
|
entityType String @map("entity_type")
|
|
entityId String? @map("entity_id")
|
|
oldValues Json? @map("old_values")
|
|
newValues Json? @map("new_values")
|
|
ipAddress String? @map("ip_address")
|
|
userAgent String? @map("user_agent") @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User? @relation(fields: [userId], references: [id])
|
|
|
|
@@index([userId])
|
|
@@index([entityType])
|
|
@@index([createdAt])
|
|
@@map("audit_logs")
|
|
}
|
|
|
|
// ============================================
|
|
// Notifications
|
|
// ============================================
|
|
|
|
model Notification {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
type String
|
|
title String
|
|
message String @db.Text
|
|
relatedEntityType String? @map("related_entity_type")
|
|
relatedEntityId String? @map("related_entity_id")
|
|
isRead Boolean @default(false) @map("is_read")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, isRead])
|
|
@@index([createdAt])
|
|
@@map("notifications")
|
|
}
|
|
|
|
model ShapeImportRun {
|
|
id String @id @default(cuid())
|
|
startedAt DateTime @default(now()) @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
status String // pending | running | completed | failed
|
|
dryRun Boolean @default(true) @map("dry_run")
|
|
driveId String @map("drive_id")
|
|
folderPath String @default("Claims/SHAPE Accounts") @map("folder_path")
|
|
triggeredBy String? @map("triggered_by")
|
|
stats Json?
|
|
|
|
user User? @relation(fields: [triggeredBy], references: [id])
|
|
logLines ShapeImportLog[]
|
|
|
|
@@index([startedAt])
|
|
@@map("shape_import_runs")
|
|
}
|
|
|
|
model ShapeImportLog {
|
|
id String @id @default(cuid())
|
|
runId String @map("run_id")
|
|
seq Int
|
|
line String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
run ShapeImportRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([runId, seq])
|
|
@@map("shape_import_logs")
|
|
}
|
|
|
|
model NotificationPreference {
|
|
id String @id @default(cuid())
|
|
userId String @map("user_id")
|
|
notificationType String @map("notification_type")
|
|
inApp Boolean @default(true) @map("in_app")
|
|
email Boolean @default(false)
|
|
teams Boolean @default(false)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, notificationType])
|
|
@@map("notification_preferences")
|
|
}
|