feat: add Assignment Prisma model + migration
Links Employee to exactly one of AssetItem/License (XOR enforced at the Zod layer, not built yet) via nullable FKs, plus assignedAt/returnedAt/ signedAt timestamps. See docs/asset-license-management.md's "Assignment" section for the full spec.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "assignments" (
|
||||
"id" UUID NOT NULL,
|
||||
"employee_id" UUID NOT NULL,
|
||||
"asset_item_id" UUID,
|
||||
"license_id" UUID,
|
||||
"assigned_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"returned_at" TIMESTAMPTZ(6),
|
||||
"signed_at" TIMESTAMPTZ(6),
|
||||
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "assignments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "assignments" ADD CONSTRAINT "assignments_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "assignments" ADD CONSTRAINT "assignments_asset_item_id_fkey" FOREIGN KEY ("asset_item_id") REFERENCES "asset_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "assignments" ADD CONSTRAINT "assignments_license_id_fkey" FOREIGN KEY ("license_id") REFERENCES "licenses"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+59
-3
@@ -70,6 +70,10 @@ model Employee {
|
||||
// Back-relation for PurchaseOrder.receivedById — an Employee can be the
|
||||
// receiver on multiple POs over time.
|
||||
purchaseOrdersReceived PurchaseOrder[]
|
||||
// Back-relation for Assignment.employeeId — an Employee can hold multiple
|
||||
// Assignments (assets/licenses) over time, including several active ones
|
||||
// at once (e.g. a laptop + a license seat simultaneously).
|
||||
assignments Assignment[]
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
@@map("employees")
|
||||
@@ -117,6 +121,10 @@ model AssetItem {
|
||||
purchaseOrderId String? @map("purchase_order_id") @db.Uuid
|
||||
purchaseOrder PurchaseOrder? @relation(fields: [purchaseOrderId], references: [id])
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
// Back-relation for Assignment.assetItemId — an AssetItem can have
|
||||
// multiple Assignment rows over its lifetime (historical + at most one
|
||||
// active, see Assignment model's comment), not built until this task.
|
||||
assignments Assignment[]
|
||||
|
||||
@@map("asset_items")
|
||||
}
|
||||
@@ -158,6 +166,10 @@ model License {
|
||||
purchaseOrderId String? @map("purchase_order_id") @db.Uuid
|
||||
purchaseOrder PurchaseOrder? @relation(fields: [purchaseOrderId], references: [id])
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
// Back-relation for Assignment.licenseId — a License can have multiple
|
||||
// Assignment rows (one per seat handed out, plus historical returns), not
|
||||
// built until this task.
|
||||
assignments Assignment[]
|
||||
|
||||
@@map("licenses")
|
||||
}
|
||||
@@ -247,10 +259,10 @@ enum AssetCategoryValueGroup {
|
||||
// (e.g. "Laptop", "Cáp HDMI") freely; only `valueGroup` is a fixed 2-value
|
||||
// set.
|
||||
model AssetCategory {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
name String
|
||||
valueGroup AssetCategoryValueGroup @map("value_group")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
valueGroup AssetCategoryValueGroup @map("value_group")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
attributeDefs AssetCategoryAttributeDef[]
|
||||
// Back-relation for AssetModel.categoryId — see "Cập nhật 2026-08-02 (2)"
|
||||
// in docs/asset-license-management.md. Only categories with
|
||||
@@ -344,6 +356,50 @@ model AssetModel {
|
||||
@@map("asset_models")
|
||||
}
|
||||
|
||||
// Assigns an AssetItem (high-value hardware) or a License seat to a specific
|
||||
// Employee — see docs/asset-license-management.md's "## Assignment (chốt
|
||||
// 2026-08-02)" section and the `Assignment` row in the "Data model (sơ bộ)"
|
||||
// table. Exactly one of assetItemId/licenseId must be set (XOR) — enforced
|
||||
// at the Zod layer (src/lib/assignments.ts's `.refine()`), NOT a DB
|
||||
// constraint, per the same convention as PurchaseOrder's
|
||||
// receivedAt/receivedById co-nullability check: Prisma schema can't express
|
||||
// an XOR between two nullable FKs natively. Both FKs stay plain nullable
|
||||
// columns here for that reason. `assignedAt` defaults to now() but is a
|
||||
// real stored column (not derived from createdAt) because a future
|
||||
// "transfer" operation (out of scope for this task, see doc) will create a
|
||||
// new Assignment row whose assignedAt should reflect the transfer moment,
|
||||
// not necessarily row-insert time. `returnedAt` null means still held by
|
||||
// the employee — this is also what "active assignment" means throughout
|
||||
// the codebase (see the uniqueness rule below). `signedAt` is deliberately
|
||||
// just a nullable timestamp marking "handover paperwork signed" — NO file
|
||||
// upload, stakeholder explicitly decided against storing a scanned file at
|
||||
// this stage (see doc). No `@@unique` on assetItemId here for "only one
|
||||
// active assignment at a time" — a plain unique constraint would also
|
||||
// forbid a *second, already-returned* historical Assignment for the same
|
||||
// AssetItem (e.g. item reassigned after being returned), which is valid and
|
||||
// expected. The "at most one ACTIVE (returnedAt IS NULL) assignment per
|
||||
// AssetItem" invariant is instead enforced in application code
|
||||
// (src/lib/assignments.ts's createAssignment(), inside the same transaction
|
||||
// that mutates AssetItem.status/License.seatsUsed) — a partial unique index
|
||||
// (`WHERE returned_at IS NULL`) would express this at the DB level too, but
|
||||
// that's raw SQL outside a Prisma-expressible `@@unique`, so deliberately
|
||||
// not added here; see feature-implementer report for this trade-off.
|
||||
model Assignment {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
employeeId String @map("employee_id") @db.Uuid
|
||||
employee Employee @relation(fields: [employeeId], references: [id])
|
||||
assetItemId String? @map("asset_item_id") @db.Uuid
|
||||
assetItem AssetItem? @relation(fields: [assetItemId], references: [id])
|
||||
licenseId String? @map("license_id") @db.Uuid
|
||||
license License? @relation(fields: [licenseId], references: [id])
|
||||
assignedAt DateTime @default(now()) @map("assigned_at") @db.Timestamptz(6)
|
||||
returnedAt DateTime? @map("returned_at") @db.Timestamptz(6)
|
||||
signedAt DateTime? @map("signed_at") @db.Timestamptz(6)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
@@map("assignments")
|
||||
}
|
||||
|
||||
// Minimal internal auth accounts — NOT company SSO (deliberately ruled out,
|
||||
// see CLAUDE.md / auth decision notes). A handful of trusted IT staff
|
||||
// accounts, created by hand via scripts/create-user.ts, no self-registration.
|
||||
|
||||
Reference in New Issue
Block a user