680 lines
24 KiB
Plaintext
680 lines
24 KiB
Plaintext
// P17 HR-Hub core (AK-P17a) — employee master data, locations, personnel
|
|
// documents and the encrypted team password safe.
|
|
//
|
|
// AD-21: one self-contained schema file per module. `tenantId` / `userId` /
|
|
// `*UserId` / `fileId` / `locationId` are intentionally BARE String scalars (no
|
|
// `@relation`) so this file has zero merge-conflict surface against
|
|
// `core.prisma`/`accounts.prisma` (no back-relation fields on Tenant/User
|
|
// needed). Referential integrity is carried by app-level scoping (the hr*Service
|
|
// helpers) plus the RLS policies from the merge migration (see
|
|
// prisma/rls/hr-isolation.sql). Documents reference File from files.prisma
|
|
// (AK-P4a/b) by bare `fileId` for the same reason.
|
|
//
|
|
// Scope v1 (AK-P17a ticket doc, strict): employee master data + locations +
|
|
// personnel documents + password safe ONLY. Announcements, leave/sick reports,
|
|
// payslips, requests, surveys and upload reminders belong to AK-P17b / follow-up
|
|
// and are intentionally NOT modelled here.
|
|
//
|
|
// ENCRYPTION (AD-8, deferred): sensitive values — the password-safe entry
|
|
// secrets and the employee emergency contact — are stored as ENVELOPE CIPHERTEXT
|
|
// strings (the `*Ciphertext` columns). Per the AK-P17a build decision the
|
|
// per-tenant DEK / KMS envelope layer is a separate infrastructure ticket; the
|
|
// service layer routes every read/write of these columns through the
|
|
// `hrFieldCrypto` seam (apps/web/app/utils/integrations/hrFieldCrypto.server.ts),
|
|
// whose production binding currently defers to that follow-up ticket. The schema
|
|
// is final; only the crypto binding is pending.
|
|
|
|
/// Visibility of an HR personnel document to the employee it belongs to.
|
|
/// `employee` = the employee can see (and, if self-uploaded, manage) it;
|
|
/// `hr_only` = visible to HR admins only.
|
|
enum HrDocumentVisibility {
|
|
employee
|
|
hr_only
|
|
}
|
|
|
|
/// Coarse kind of a personnel document (for filtering + upload reminders).
|
|
enum HrDocumentKind {
|
|
contract
|
|
certificate
|
|
identification
|
|
qualification
|
|
other
|
|
}
|
|
|
|
/// A linked HR resource / quick link kind.
|
|
enum HrResourceKind {
|
|
link
|
|
file
|
|
}
|
|
|
|
/// Access level a user holds on a password-safe vault. `read` may reveal/copy
|
|
/// values; `write` may also create/edit entries; `admin` may also manage access
|
|
/// and archive/delete the vault.
|
|
enum PasswordSafePermissionLevel {
|
|
read
|
|
write
|
|
admin
|
|
}
|
|
|
|
/// Per-tenant employee master record (1:1 with a global User). Self-maintainable
|
|
/// fields (e.g. position display, team) are edited by the employee; HR-managed
|
|
/// fields by HR admins (enforced in the service layer). The emergency contact is
|
|
/// sensitive and stored ENVELOPE-ENCRYPTED (`emergencyContactCiphertext`, holds
|
|
/// the name/phone/relation triple as encrypted JSON) — never returned without an
|
|
/// explicit, permission-checked reveal.
|
|
model HrEmployeeProfile {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
userId String
|
|
personalNumber String?
|
|
hireDate DateTime?
|
|
position String?
|
|
team String?
|
|
locationId String?
|
|
managerUserId String?
|
|
// Sensitive: AES-256-GCM envelope ciphertext (name/phone/relation as JSON).
|
|
emergencyContactCiphertext String?
|
|
isDemoData Boolean @default(false)
|
|
|
|
@@unique([tenantId, userId])
|
|
@@index([tenantId, locationId])
|
|
@@index([tenantId, managerUserId])
|
|
@@index([tenantId, team])
|
|
}
|
|
|
|
/// A physical company location, scoped per tenant. Soft-delete via `archivedAt`.
|
|
/// Exactly one location may be the tenant default (`isDefault`); enforced in the
|
|
/// service layer (set-default clears the previous default).
|
|
model HrLocation {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
name String
|
|
addressLine1 String?
|
|
addressLine2 String?
|
|
postalCode String?
|
|
city String?
|
|
country String?
|
|
phone String?
|
|
latitude Float?
|
|
longitude Float?
|
|
imageFileId String?
|
|
description String?
|
|
isDefault Boolean @default(false)
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
@@index([tenantId, archivedAt, name])
|
|
@@index([tenantId, isDefault])
|
|
}
|
|
|
|
/// A personnel document for one employee. The bytes live in a File (files.prisma,
|
|
/// referenced by bare `fileId`). `visibility` gates whether the employee sees it;
|
|
/// `uploadedByOffice` marks office-uploaded documents as READ-ONLY for the
|
|
/// employee (they may view/download but not edit or delete). Soft-delete via
|
|
/// `archivedAt`.
|
|
model HrEmployeeDocument {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
employeeUserId String
|
|
uploadedByUserId String
|
|
title String
|
|
documentKind HrDocumentKind @default(other)
|
|
fileId String
|
|
visibility HrDocumentVisibility @default(employee)
|
|
// True when uploaded by HR/office on behalf of the employee → read-only for
|
|
// the employee in the self-service "My documents" view.
|
|
uploadedByOffice Boolean @default(false)
|
|
validUntil DateTime?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
@@index([tenantId, employeeUserId, archivedAt])
|
|
@@index([tenantId, documentKind])
|
|
@@index([tenantId, validUntil])
|
|
}
|
|
|
|
/// A shared HR resource (a link or a file) surfaced on the HR hub. Soft-delete
|
|
/// via `archivedAt`. `audience` is a coarse audience marker (e.g. "all",
|
|
/// "team:<id>"); fine-grained targeting is follow-up.
|
|
model HrResource {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
title String
|
|
kind HrResourceKind @default(link)
|
|
url String?
|
|
fileId String?
|
|
category String?
|
|
audience String @default("all")
|
|
sortOrder Int @default(0)
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
@@index([tenantId, archivedAt, category, sortOrder])
|
|
}
|
|
|
|
/// A quick link tile on the HR hub. Soft-delete via `archivedAt`.
|
|
model HrQuickLink {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
title String
|
|
url String
|
|
icon String?
|
|
audience String @default("all")
|
|
sortOrder Int @default(0)
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
@@index([tenantId, archivedAt, sortOrder])
|
|
}
|
|
|
|
/// A password-safe vault, scoped per tenant. Access is NOT implied by tenant
|
|
/// membership — a user only sees a vault they hold an EmployeePasswordSafeAccess
|
|
/// row for (or via the tenant-wide admin view, which never grants content
|
|
/// access). Soft-delete via `archivedAt`.
|
|
model EmployeePasswordSafeVault {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
name String
|
|
description String?
|
|
createdByUserId String
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
entries EmployeePasswordSafeEntry[]
|
|
access EmployeePasswordSafeAccess[]
|
|
|
|
@@index([tenantId, archivedAt, name])
|
|
}
|
|
|
|
/// A single secret in a vault. The secret values are stored ENVELOPE-ENCRYPTED
|
|
/// (`*Ciphertext`) and are NEVER selected into a list view — only an explicit,
|
|
/// permission-checked + audited reveal decrypts a single value on demand.
|
|
model EmployeePasswordSafeEntry {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
vaultId String
|
|
title String
|
|
tags String[] @default([])
|
|
url String?
|
|
// Sensitive: AES-256-GCM envelope ciphertext. Never returned in list queries.
|
|
usernameCiphertext String?
|
|
passwordCiphertext String?
|
|
notesCiphertext String?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
vault EmployeePasswordSafeVault @relation(fields: [vaultId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId, vaultId, archivedAt])
|
|
}
|
|
|
|
/// A user's access grant on a vault (M:N user↔vault with a permission level).
|
|
model EmployeePasswordSafeAccess {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
vaultId String
|
|
userId String
|
|
permissionLevel PasswordSafePermissionLevel @default(read)
|
|
grantedByUserId String
|
|
|
|
vault EmployeePasswordSafeVault @relation(fields: [vaultId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([vaultId, userId])
|
|
@@index([tenantId, userId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// AK-P17b · P17 HR-Hub workflows — leave/sick, announcements, payslips, upload
|
|
// reminders, requests and surveys. Additive to this file (AD-21); same bare
|
|
// String-scalar convention (no `@relation` to core/User/File) so the merge
|
|
// surface against core.prisma/accounts.prisma stays zero. CalendarEvent (P16,
|
|
// M25) and the M05 FormSubmission (AK-P16c) are referenced by NULLABLE bare
|
|
// scalars only — both land after P17, so the actual calendar sync / form-render
|
|
// reuse is a deferred Dev-Nachzug; the columns are final.
|
|
//
|
|
// ENCRYPTION (AD-8): the sick-leave certificate ("AU-Schein") is sensitive. We
|
|
// envelope-encrypt the certificate File REFERENCE (`certificateRefCiphertext`)
|
|
// through the hrFieldCrypto seam (mirrors HrEmployeeProfile.emergencyContact):
|
|
// the column never reveals which File is the certificate without an explicit,
|
|
// permission-checked + audited reveal. (At-rest encryption of the File BYTES is
|
|
// a File-platform concern, tracked separately.)
|
|
// =============================================================================
|
|
|
|
/// Kind of a vacation/leave request.
|
|
enum HrVacationRequestKind {
|
|
vacation
|
|
special_leave
|
|
unpaid_leave
|
|
overtime_compensation
|
|
}
|
|
|
|
/// Approval state of a leave request. `clarification` = approver asked the
|
|
/// requester a question (Rückfrage) and awaits a reply before deciding.
|
|
enum HrLeaveRequestStatus {
|
|
submitted
|
|
clarification
|
|
approved
|
|
rejected
|
|
withdrawn
|
|
}
|
|
|
|
/// State of a sick-leave report.
|
|
enum HrSickLeaveStatus {
|
|
reported
|
|
closed
|
|
cancelled
|
|
}
|
|
|
|
/// Coarse audience marker for announcements / surveys / upload reminders.
|
|
/// `team`/`location` pair with `audienceRef` (the team or location id).
|
|
enum HrAudienceKind {
|
|
all
|
|
team
|
|
location
|
|
}
|
|
|
|
/// Kind of a generic HR request (template-driven).
|
|
enum HrRequestKind {
|
|
expense
|
|
hardware
|
|
training
|
|
reference
|
|
}
|
|
|
|
/// Approval state of an HR request (mirrors leave).
|
|
enum HrRequestStatus {
|
|
submitted
|
|
clarification
|
|
approved
|
|
rejected
|
|
withdrawn
|
|
}
|
|
|
|
/// Cadence of an upload reminder. `every_n_months` reads `intervalMonths`;
|
|
/// `on_expiry` derives the due date from a document's `validUntil`.
|
|
enum HrReminderCadenceKind {
|
|
once
|
|
yearly
|
|
every_n_months
|
|
on_expiry
|
|
}
|
|
|
|
/// Lifecycle of an upload-reminder definition.
|
|
enum HrReminderStatus {
|
|
active
|
|
paused
|
|
completed
|
|
}
|
|
|
|
/// State of a single dispatched reminder to one employee.
|
|
enum HrReminderDispatchStatus {
|
|
pending
|
|
sent
|
|
escalated
|
|
acknowledged
|
|
}
|
|
|
|
/// Survey item answer type.
|
|
enum HrSurveyItemType {
|
|
text
|
|
rating_5
|
|
nps
|
|
choice
|
|
boolean
|
|
date
|
|
}
|
|
|
|
/// Who may see a survey's aggregated results.
|
|
enum HrSurveyResultVisibility {
|
|
admins
|
|
participants
|
|
everyone
|
|
}
|
|
|
|
/// Lifecycle of a survey.
|
|
enum HrSurveyStatus {
|
|
draft
|
|
open
|
|
closed
|
|
}
|
|
|
|
/// A multi-step leave/vacation request. State machine: submitted →
|
|
/// {clarification ↔ submitted} → approved | rejected, or → withdrawn by the
|
|
/// requester. Only `approved` requests feed the absence calendar / plantafel
|
|
/// conflict feed (R-P9b). `calendarEventId` stays null until the P16 calendar
|
|
/// sync lands (deferred).
|
|
model EmployeeVacationRequest {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
employeeUserId String
|
|
requestKind HrVacationRequestKind @default(vacation)
|
|
startDate DateTime
|
|
endDate DateTime
|
|
note String?
|
|
status HrLeaveRequestStatus @default(submitted)
|
|
assignedApproverUserId String?
|
|
clarificationNote String?
|
|
decidedByUserId String?
|
|
decidedAt DateTime?
|
|
decisionNote String?
|
|
withdrawnAt DateTime?
|
|
// Deferred: CalendarEvent sync lands with P16 (M25). Bare nullable scalar.
|
|
calendarEventId String?
|
|
isDemoData Boolean @default(false)
|
|
|
|
@@index([tenantId, employeeUserId, status])
|
|
@@index([tenantId, assignedApproverUserId, status])
|
|
@@index([tenantId, status, startDate, endDate])
|
|
}
|
|
|
|
/// A sick-leave report, filed by the employee or by HR/office on their behalf
|
|
/// (`reportedByUserId`). The AU certificate reference is stored ENVELOPE-
|
|
/// ENCRYPTED (`certificateRefCiphertext`) and never returned without a
|
|
/// permission-checked + audited reveal.
|
|
model EmployeeSickLeaveReport {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
employeeUserId String
|
|
reportedByUserId String
|
|
startDate DateTime
|
|
expectedReturnDate DateTime?
|
|
returnDate DateTime?
|
|
// Sensitive: AES-256-GCM envelope ciphertext of the certificate File id.
|
|
certificateRefCiphertext String?
|
|
status HrSickLeaveStatus @default(reported)
|
|
note String?
|
|
// Deferred: CalendarEvent sync lands with P16 (M25).
|
|
calendarEventId String?
|
|
isDemoData Boolean @default(false)
|
|
|
|
@@index([tenantId, employeeUserId, status])
|
|
@@index([tenantId, status, startDate])
|
|
}
|
|
|
|
/// A company announcement. Markdown `body`, audience targeting, optional
|
|
/// acknowledgement requirement (read-receipt), scheduling (`publishAt`) and
|
|
/// expiry. Soft-delete via `archivedAt`.
|
|
model HrAnnouncement {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
authorUserId String
|
|
title String
|
|
body String
|
|
audienceKind HrAudienceKind @default(all)
|
|
audienceRef String?
|
|
requiresAcknowledgement Boolean @default(false)
|
|
pinnedAt DateTime?
|
|
publishAt DateTime?
|
|
expiresAt DateTime?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
reads HrAnnouncementRead[]
|
|
reactions HrAnnouncementReaction[]
|
|
comments HrAnnouncementComment[]
|
|
|
|
@@index([tenantId, archivedAt, pinnedAt])
|
|
@@index([tenantId, publishAt])
|
|
}
|
|
|
|
/// Per-user read / acknowledgement receipt for an announcement.
|
|
/// `acknowledgedAt` is set only when the announcement required acknowledgement
|
|
/// and the user clicked confirm.
|
|
model HrAnnouncementRead {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
tenantId String
|
|
announcementId String
|
|
userId String
|
|
acknowledgedAt DateTime?
|
|
|
|
announcement HrAnnouncement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([announcementId, userId])
|
|
@@index([tenantId, userId])
|
|
}
|
|
|
|
/// A single emoji reaction by a user on an announcement.
|
|
model HrAnnouncementReaction {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
tenantId String
|
|
announcementId String
|
|
userId String
|
|
reaction String
|
|
|
|
announcement HrAnnouncement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([announcementId, userId, reaction])
|
|
@@index([tenantId, announcementId])
|
|
}
|
|
|
|
/// A single-level comment on an announcement. Soft-delete via `archivedAt`.
|
|
model HrAnnouncementComment {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
announcementId String
|
|
userId String
|
|
body String
|
|
archivedAt DateTime?
|
|
|
|
announcement HrAnnouncement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId, announcementId, archivedAt])
|
|
}
|
|
|
|
/// A payslip PDF for one employee for one period. The bytes live in a File
|
|
/// (referenced by bare `fileId`). Opening the payslip marks it read
|
|
/// (`isRead`/`readAt`) and writes an audit event per read. Soft-delete via
|
|
/// `archivedAt`.
|
|
model EmployeePayslip {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
employeeUserId String
|
|
uploadedByUserId String
|
|
periodYear Int
|
|
periodMonth Int
|
|
fileId String
|
|
isRead Boolean @default(false)
|
|
readAt DateTime?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
@@unique([tenantId, employeeUserId, periodYear, periodMonth])
|
|
@@index([tenantId, employeeUserId, archivedAt])
|
|
}
|
|
|
|
/// An upload-reminder definition: ask a targeted audience to upload a document
|
|
/// of a given kind on a cadence, optionally mandatory and escalating to the
|
|
/// manager when overdue. `linkedFormId` (M05) stays null until AK-P16c.
|
|
/// Soft-delete via `archivedAt`.
|
|
model EmployeeUploadReminder {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
title String
|
|
documentKind HrDocumentKind @default(other)
|
|
cadenceKind HrReminderCadenceKind @default(once)
|
|
intervalMonths Int?
|
|
audienceKind HrAudienceKind @default(all)
|
|
audienceRef String?
|
|
mandatory Boolean @default(false)
|
|
escalateToManager Boolean @default(false)
|
|
// M05 form template reference; null until AK-P16c renderer lands (deferred).
|
|
linkedFormId String?
|
|
dueAt DateTime?
|
|
status HrReminderStatus @default(active)
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
dispatches EmployeeUploadReminderDispatch[]
|
|
|
|
@@index([tenantId, status, dueAt])
|
|
}
|
|
|
|
/// One reminder dispatched to one employee for one due date. Idempotent per
|
|
/// (reminder, employee, dueAt) so the cadence worker never double-sends.
|
|
model EmployeeUploadReminderDispatch {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
reminderId String
|
|
employeeUserId String
|
|
dueAt DateTime
|
|
status HrReminderDispatchStatus @default(pending)
|
|
sentAt DateTime?
|
|
acknowledgedAt DateTime?
|
|
escalatedAt DateTime?
|
|
|
|
reminder EmployeeUploadReminder @relation(fields: [reminderId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([reminderId, employeeUserId, dueAt])
|
|
@@index([tenantId, employeeUserId, status])
|
|
}
|
|
|
|
/// A reusable HR request template (expense/hardware/training/reference) with an
|
|
/// optional default approver and linked M05 form (`linkedFormId`, null until
|
|
/// AK-P16c). Soft-delete via `archivedAt`.
|
|
model HrRequestTemplate {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
title String
|
|
requestKind HrRequestKind
|
|
description String?
|
|
defaultApproverUserId String?
|
|
linkedFormId String?
|
|
isActive Boolean @default(true)
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
requests HrRequest[]
|
|
|
|
@@index([tenantId, archivedAt, requestKind])
|
|
}
|
|
|
|
/// A generic HR request submitted by an employee, optionally from a template.
|
|
/// Until the M05 renderer (AK-P16c) lands, the payload is captured as a JSON
|
|
/// string in `details` (transitional form); `formSubmissionId` is wired in
|
|
/// later. Approval mirrors the leave state machine. Soft-delete via `archivedAt`.
|
|
model HrRequest {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
employeeUserId String
|
|
templateId String?
|
|
requestKind HrRequestKind
|
|
title String
|
|
// Transitional form payload (JSON string) until M05 submission is wired.
|
|
details String?
|
|
formSubmissionId String?
|
|
status HrRequestStatus @default(submitted)
|
|
assignedApproverUserId String?
|
|
clarificationNote String?
|
|
decidedByUserId String?
|
|
decidedAt DateTime?
|
|
decisionNote String?
|
|
withdrawnAt DateTime?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
template HrRequestTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([tenantId, employeeUserId, status])
|
|
@@index([tenantId, assignedApproverUserId, status])
|
|
}
|
|
|
|
/// A survey. May be anonymous (`isAnonymous`); aggregated results are gated by
|
|
/// `resultVisibility`. Soft-delete via `archivedAt`.
|
|
model HrSurvey {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
authorUserId String
|
|
title String
|
|
description String?
|
|
isAnonymous Boolean @default(false)
|
|
resultVisibility HrSurveyResultVisibility @default(admins)
|
|
audienceKind HrAudienceKind @default(all)
|
|
audienceRef String?
|
|
status HrSurveyStatus @default(draft)
|
|
opensAt DateTime?
|
|
closesAt DateTime?
|
|
isDemoData Boolean @default(false)
|
|
archivedAt DateTime?
|
|
|
|
items HrSurveyItem[]
|
|
submissions HrSurveySubmission[]
|
|
|
|
@@index([tenantId, status, archivedAt])
|
|
}
|
|
|
|
/// One question in a survey. `options` holds the choices for `choice` items.
|
|
model HrSurveyItem {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
tenantId String
|
|
surveyId String
|
|
itemType HrSurveyItemType
|
|
prompt String
|
|
required Boolean @default(false)
|
|
options String[] @default([])
|
|
sortOrder Int @default(0)
|
|
|
|
survey HrSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([tenantId, surveyId])
|
|
}
|
|
|
|
/// One submission to a survey. For identified surveys `submitterUserId` is set
|
|
/// and unique per survey; for anonymous surveys it is null and `submitterHash`
|
|
/// (a non-reversible per-survey hash of the user) enforces one-submission
|
|
/// without re-identification. `answers` maps item id → answer value.
|
|
model HrSurveySubmission {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
tenantId String
|
|
surveyId String
|
|
submitterUserId String?
|
|
submitterHash String?
|
|
answers Json
|
|
|
|
survey HrSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([surveyId, submitterUserId])
|
|
@@unique([surveyId, submitterHash])
|
|
@@index([tenantId, surveyId])
|
|
}
|