ThreadCount Community edition

Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 5470a36 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
ThreadCount
2026-09-13 11:35:40 +10:00
commit 46a1911c1c
275 changed files with 43437 additions and 0 deletions
@@ -0,0 +1,412 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'ISSUER');
-- CreateTable
CREATE TABLE "Facility" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"location" TEXT NOT NULL DEFAULT 'Linen Room',
"coordinator" TEXT NOT NULL DEFAULT '',
"defaultEntitlement" INTEGER NOT NULL DEFAULT 8,
"defaultReorder" INTEGER NOT NULL DEFAULT 3,
"suppliers" TEXT[] DEFAULT ARRAY[]::TEXT[],
"staffGroups" TEXT[] DEFAULT ARRAY[]::TEXT[],
"orderSeq" INTEGER NOT NULL DEFAULT 0,
"catalogSeq" INTEGER NOT NULL DEFAULT 0,
"slipCollectionFooter" TEXT NOT NULL DEFAULT 'Collect from the Linen Room during opening hours. Enquiries: see coordinator.',
"slipDeliveryFooter" TEXT NOT NULL DEFAULT 'After hours deliveries are left with the ward NUM or team leader.',
"slipOrg" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Facility_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"first" TEXT NOT NULL,
"last" TEXT NOT NULL,
"title" TEXT NOT NULL DEFAULT '',
"role" "Role" NOT NULL DEFAULT 'ISSUER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CatalogItem" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"sort" INTEGER NOT NULL,
"item" TEXT NOT NULL,
"gender" TEXT NOT NULL DEFAULT 'Unisex',
"sku" TEXT NOT NULL DEFAULT '',
"supplier" TEXT NOT NULL DEFAULT '',
"cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
"group" TEXT NOT NULL DEFAULT 'All',
"notes" TEXT NOT NULL DEFAULT '',
"sizes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"archived" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CatalogItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Barcode" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"source" TEXT NOT NULL DEFAULT 'bound',
CONSTRAINT "Barcode_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StockLevel" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"opening" INTEGER NOT NULL DEFAULT 0,
"adj" INTEGER NOT NULL DEFAULT 0,
"reorder" INTEGER,
CONSTRAINT "StockLevel_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StockMove" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"type" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"qty" INTEGER NOT NULL,
"reason" TEXT NOT NULL DEFAULT '',
"byName" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "StockMove_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Department" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"cc" TEXT NOT NULL DEFAULT '',
"sort" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Department_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Staff" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"num" TEXT NOT NULL,
"first" TEXT NOT NULL,
"last" TEXT NOT NULL,
"phone" TEXT NOT NULL DEFAULT '',
"group" TEXT NOT NULL DEFAULT '',
"dept" TEXT NOT NULL DEFAULT '',
"top" TEXT NOT NULL DEFAULT '',
"pants" TEXT NOT NULL DEFAULT '',
"shoe" TEXT NOT NULL DEFAULT '',
"ent" INTEGER,
"start" TEXT NOT NULL DEFAULT '',
"notes" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Staff_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Issue" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"qty" INTEGER NOT NULL,
"cond" TEXT NOT NULL DEFAULT 'New',
"orderCode" TEXT NOT NULL DEFAULT '',
"receipt" BOOLEAN NOT NULL DEFAULT false,
"returnedDate" TEXT,
"returnedCond" TEXT,
"override" BOOLEAN NOT NULL DEFAULT false,
"direct" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Issue_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"date" TEXT NOT NULL,
"source" TEXT NOT NULL DEFAULT 'Supplier Order',
"orderFor" TEXT NOT NULL DEFAULT 'Stock',
"staffId" TEXT,
"supplier" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'Draft',
"ref" TEXT NOT NULL DEFAULT '',
"invoice" TEXT NOT NULL DEFAULT '',
"tracking" TEXT NOT NULL DEFAULT '',
"expected" TEXT NOT NULL DEFAULT '',
"received" TEXT NOT NULL DEFAULT '',
"cc" TEXT NOT NULL DEFAULT '',
"notes" TEXT NOT NULL DEFAULT '',
"replenish" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderLine" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"size" TEXT NOT NULL,
"qty" INTEGER NOT NULL,
"sort" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "OrderLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Receipt" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"invoice" TEXT NOT NULL DEFAULT '',
"note" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Receipt_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ReceiptLine" (
"id" TEXT NOT NULL,
"receiptId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"size" TEXT NOT NULL,
"qty" INTEGER NOT NULL,
"dest" TEXT NOT NULL DEFAULT 'shelf',
"cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
CONSTRAINT "ReceiptLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pickup" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"received" TEXT NOT NULL,
"contacted" BOOLEAN NOT NULL DEFAULT false,
"pickedUp" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Pickup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PickupLine" (
"id" TEXT NOT NULL,
"pickupId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"size" TEXT NOT NULL,
"qty" INTEGER NOT NULL,
CONSTRAINT "PickupLine_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Stocktake" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"byName" TEXT NOT NULL,
"counted" INTEGER NOT NULL,
"variances" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Stocktake_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StocktakeLine" (
"id" TEXT NOT NULL,
"stocktakeId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"sys" INTEGER NOT NULL,
"counted" INTEGER NOT NULL,
CONSTRAINT "StocktakeLine_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE INDEX "User_facilityId_idx" ON "User"("facilityId");
-- CreateIndex
CREATE INDEX "CatalogItem_facilityId_idx" ON "CatalogItem"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "CatalogItem_facilityId_sort_key" ON "CatalogItem"("facilityId", "sort");
-- CreateIndex
CREATE INDEX "Barcode_itemId_idx" ON "Barcode"("itemId");
-- CreateIndex
CREATE UNIQUE INDEX "Barcode_facilityId_code_key" ON "Barcode"("facilityId", "code");
-- CreateIndex
CREATE INDEX "StockLevel_facilityId_idx" ON "StockLevel"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "StockLevel_itemId_sizeIndex_key" ON "StockLevel"("itemId", "sizeIndex");
-- CreateIndex
CREATE INDEX "StockMove_facilityId_idx" ON "StockMove"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "Department_facilityId_name_key" ON "Department"("facilityId", "name");
-- CreateIndex
CREATE UNIQUE INDEX "Staff_facilityId_num_key" ON "Staff"("facilityId", "num");
-- CreateIndex
CREATE INDEX "Issue_facilityId_date_idx" ON "Issue"("facilityId", "date");
-- CreateIndex
CREATE INDEX "Issue_staffId_idx" ON "Issue"("staffId");
-- CreateIndex
CREATE INDEX "Order_facilityId_idx" ON "Order"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "Order_facilityId_code_key" ON "Order"("facilityId", "code");
-- CreateIndex
CREATE INDEX "OrderLine_orderId_idx" ON "OrderLine"("orderId");
-- CreateIndex
CREATE INDEX "Receipt_orderId_idx" ON "Receipt"("orderId");
-- CreateIndex
CREATE INDEX "ReceiptLine_receiptId_idx" ON "ReceiptLine"("receiptId");
-- CreateIndex
CREATE INDEX "Pickup_facilityId_idx" ON "Pickup"("facilityId");
-- CreateIndex
CREATE INDEX "PickupLine_pickupId_idx" ON "PickupLine"("pickupId");
-- CreateIndex
CREATE INDEX "Stocktake_facilityId_idx" ON "Stocktake"("facilityId");
-- CreateIndex
CREATE INDEX "StocktakeLine_stocktakeId_idx" ON "StocktakeLine"("stocktakeId");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CatalogItem" ADD CONSTRAINT "CatalogItem_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Barcode" ADD CONSTRAINT "Barcode_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Barcode" ADD CONSTRAINT "Barcode_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockLevel" ADD CONSTRAINT "StockLevel_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockLevel" ADD CONSTRAINT "StockLevel_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockMove" ADD CONSTRAINT "StockMove_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StockMove" ADD CONSTRAINT "StockMove_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Department" ADD CONSTRAINT "Department_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Staff" ADD CONSTRAINT "Staff_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Issue" ADD CONSTRAINT "Issue_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderLine" ADD CONSTRAINT "OrderLine_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderLine" ADD CONSTRAINT "OrderLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReceiptLine" ADD CONSTRAINT "ReceiptLine_receiptId_fkey" FOREIGN KEY ("receiptId") REFERENCES "Receipt"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReceiptLine" ADD CONSTRAINT "ReceiptLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pickup" ADD CONSTRAINT "Pickup_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pickup" ADD CONSTRAINT "Pickup_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pickup" ADD CONSTRAINT "Pickup_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PickupLine" ADD CONSTRAINT "PickupLine_pickupId_fkey" FOREIGN KEY ("pickupId") REFERENCES "Pickup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PickupLine" ADD CONSTRAINT "PickupLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Stocktake" ADD CONSTRAINT "Stocktake_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StocktakeLine" ADD CONSTRAINT "StocktakeLine_stocktakeId_fkey" FOREIGN KEY ("stocktakeId") REFERENCES "Stocktake"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StocktakeLine" ADD CONSTRAINT "StocktakeLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,48 @@
-- Facility: new settings, drop suppliers[] after copying into Supplier rows
ALTER TABLE "Facility" ADD COLUMN "exceptionHigh" INTEGER NOT NULL DEFAULT 10,
ADD COLUMN "glAccount" TEXT NOT NULL DEFAULT '',
ADD COLUMN "journalDesc" TEXT NOT NULL DEFAULT 'Uniform issues',
ADD COLUMN "lastBackup" TEXT NOT NULL DEFAULT '',
ADD COLUMN "logoData" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Facility" ALTER COLUMN "defaultEntitlement" SET DEFAULT 5;
CREATE TABLE "Supplier" (
"id" TEXT NOT NULL, "facilityId" TEXT NOT NULL, "name" TEXT NOT NULL,
"contact" TEXT NOT NULL DEFAULT '', "phone" TEXT NOT NULL DEFAULT '', "account" TEXT NOT NULL DEFAULT '',
"lead" INTEGER, "sort" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Supplier_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Supplier_facilityId_name_key" ON "Supplier"("facilityId", "name");
ALTER TABLE "Supplier" ADD CONSTRAINT "Supplier_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO "Supplier" ("id", "facilityId", "name", "sort")
SELECT md5(random()::text || f.id || s.name || s.ord::text), f.id, s.name, s.ord - 1
FROM "Facility" f, LATERAL unnest(f."suppliers") WITH ORDINALITY AS s(name, ord);
ALTER TABLE "Facility" DROP COLUMN "suppliers";
-- Staff
ALTER TABLE "Staff" ADD COLUMN "ccOverride" TEXT NOT NULL DEFAULT '', ADD COLUMN "inactive" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Staff" DROP COLUMN "shoe";
-- Approvals
CREATE TABLE "Approval" (
"id" TEXT NOT NULL, "facilityId" TEXT NOT NULL, "staffId" TEXT NOT NULL, "date" TEXT NOT NULL,
"byName" TEXT NOT NULL DEFAULT '', "sets" INTEGER NOT NULL, "fte" TEXT NOT NULL DEFAULT '', "notes" TEXT NOT NULL DEFAULT '',
"used" INTEGER NOT NULL DEFAULT 0, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Approval_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Approval_staffId_idx" ON "Approval"("staffId");
ALTER TABLE "Approval" ADD CONSTRAINT "Approval_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Approval" ADD CONSTRAINT "Approval_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Alterations
CREATE TABLE "Alteration" (
"id" TEXT NOT NULL, "facilityId" TEXT NOT NULL, "staffId" TEXT NOT NULL, "date" TEXT NOT NULL,
"garment" TEXT NOT NULL, "desc" TEXT NOT NULL DEFAULT '', "status" TEXT NOT NULL DEFAULT 'Requested',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Alteration_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Alteration_staffId_idx" ON "Alteration"("staffId");
ALTER TABLE "Alteration" ADD CONSTRAINT "Alteration_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Alteration" ADD CONSTRAINT "Alteration_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,13 @@
-- Cost at time of issue (backfilled from the current catalogue price for existing rows)
ALTER TABLE "Issue" ADD COLUMN "cost" DOUBLE PRECISION NOT NULL DEFAULT 0;
UPDATE "Issue" i SET "cost" = c."cost" FROM "CatalogItem" c WHERE c."id" = i."itemId";
-- Back orders link to their parent order
ALTER TABLE "Order" ADD COLUMN "parentId" TEXT;
ALTER TABLE "Order" ADD CONSTRAINT "Order_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
UPDATE "Order" b SET "parentId" = p."id"
FROM "Order" p
WHERE b."parentId" IS NULL AND b."status" IN ('Back Order','Received','Cancelled','Shipped','Ordered')
AND b."notes" LIKE 'Back order — short on ORD-%'
AND p."facilityId" = b."facilityId"
AND p."code" = substring(b."notes" from 'short on (ORD-[0-9]+-[0-9]+)');
@@ -0,0 +1,4 @@
-- Demo facility flag + reset stamp; soft-deactivated users
ALTER TABLE "Facility" ADD COLUMN "isDemo" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Facility" ADD COLUMN "demoResetAt" TIMESTAMP(3);
ALTER TABLE "User" ADD COLUMN "inactive" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,34 @@
-- Pre-loved uniform pool + hand-ins
ALTER TABLE "StockLevel" ADD COLUMN "preloved" INTEGER NOT NULL DEFAULT 0;
ALTER TABLE "Issue" ADD COLUMN "preloved" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "Issue" ADD COLUMN "handedIn" TEXT;
ALTER TABLE "Stocktake" ADD COLUMN "mode" TEXT NOT NULL DEFAULT 'shelf';
CREATE TABLE "HandIn" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"date" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"byName" TEXT NOT NULL,
"credit" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "HandIn_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "HandIn_facilityId_idx" ON "HandIn"("facilityId");
CREATE INDEX "HandIn_staffId_idx" ON "HandIn"("staffId");
ALTER TABLE "HandIn" ADD CONSTRAINT "HandIn_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HandIn" ADD CONSTRAINT "HandIn_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "HandInLine" (
"id" TEXT NOT NULL,
"handInId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"qty" INTEGER NOT NULL,
"cond" TEXT NOT NULL DEFAULT 'Good',
"laundered" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "HandInLine_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "HandInLine_handInId_idx" ON "HandInLine"("handInId");
ALTER TABLE "HandInLine" ADD CONSTRAINT "HandInLine_handInId_fkey" FOREIGN KEY ("handInId") REFERENCES "HandIn"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "HandInLine" ADD CONSTRAINT "HandInLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,18 @@
-- Mobile pack: photo attachments + delivery-round proof
CREATE TABLE "Photo" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"data" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Photo_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Photo_facilityId_idx" ON "Photo"("facilityId");
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Approval" ADD COLUMN "photoId" TEXT;
ALTER TABLE "Receipt" ADD COLUMN "photoId" TEXT;
ALTER TABLE "Issue" ADD COLUMN "returnPhotoId" TEXT;
ALTER TABLE "Pickup" ADD COLUMN "deliveredTo" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Pickup" ADD COLUMN "sigId" TEXT;
ALTER TABLE "Pickup" ADD COLUMN "proofId" TEXT;
ALTER TABLE "Pickup" ADD COLUMN "deliveredRound" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,3 @@
-- Allowance credit is only for garments that were actually issued from the shelf
ALTER TABLE "HandInLine" ADD COLUMN "credited" INTEGER NOT NULL DEFAULT 0;
UPDATE "HandInLine" l SET "credited" = l."qty" FROM "HandIn" h WHERE h."id" = l."handInId" AND h."credit" = true AND l."cond" = 'Good';
@@ -0,0 +1,3 @@
-- Opt-in public barcode lookup (off by default: garment barcode numbers only leave the server
-- once an admin turns this on).
ALTER TABLE "Facility" ADD COLUMN "barcodeLookup" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,3 @@
-- Garment type (Shirt, Pants, Jacket…). Empty for existing rows, which keeps the old
-- name-regex classification in place for them until someone sets a type.
ALTER TABLE "CatalogItem" ADD COLUMN "type" TEXT NOT NULL DEFAULT '';
@@ -0,0 +1,20 @@
-- Public contact form submissions. Deliberately not linked to a Facility: whoever writes in
-- usually doesn't have an account yet.
CREATE TABLE "ContactMessage" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT '',
"facility" TEXT NOT NULL DEFAULT '',
"email" TEXT NOT NULL,
"topic" TEXT NOT NULL DEFAULT '',
"slot" TEXT NOT NULL DEFAULT '',
"message" TEXT NOT NULL,
"ip" TEXT NOT NULL DEFAULT '',
"emailed" BOOLEAN NOT NULL DEFAULT false,
"handled" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ContactMessage_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ContactMessage_createdAt_idx" ON "ContactMessage"("createdAt");
@@ -0,0 +1,32 @@
-- Where a garment lives. A tree of rooms, shelves and bays, plus laundry / external.
CREATE TABLE "Location" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"kind" TEXT NOT NULL DEFAULT 'Shelf',
"parentId" TEXT,
"sort" INTEGER NOT NULL DEFAULT 0,
"archived" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Location_facilityId_name_key" ON "Location"("facilityId", "name");
CREATE INDEX "Location_facilityId_idx" ON "Location"("facilityId");
CREATE INDEX "Location_parentId_idx" ON "Location"("parentId");
ALTER TABLE "Location" ADD CONSTRAINT "Location_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Location" ADD CONSTRAINT "Location_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- A variant's home location.
ALTER TABLE "StockLevel" ADD COLUMN "locationId" TEXT;
CREATE INDEX "StockLevel_locationId_idx" ON "StockLevel"("locationId");
ALTER TABLE "StockLevel" ADD CONSTRAINT "StockLevel_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- The location a stocktake was scoped to; NULL means the whole linen room.
ALTER TABLE "Stocktake" ADD COLUMN "locationId" TEXT;
ALTER TABLE "Stocktake" ADD CONSTRAINT "Stocktake_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Why a counted line didn't match, and the gap at which a reason becomes compulsory.
ALTER TABLE "StocktakeLine" ADD COLUMN "reason" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Facility" ADD COLUMN "varianceReason" INTEGER NOT NULL DEFAULT 5;
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "PasswordReset" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"usedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"requestIp" TEXT NOT NULL DEFAULT '',
CONSTRAINT "PasswordReset_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PasswordReset_tokenHash_key" ON "PasswordReset"("tokenHash");
-- CreateIndex
CREATE INDEX "PasswordReset_userId_idx" ON "PasswordReset"("userId");
-- CreateIndex
CREATE INDEX "PasswordReset_expiresAt_idx" ON "PasswordReset"("expiresAt");
-- AddForeignKey
ALTER TABLE "PasswordReset" ADD CONSTRAINT "PasswordReset_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "AuditEvent" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"userName" TEXT NOT NULL,
"op" TEXT NOT NULL,
"target" TEXT NOT NULL DEFAULT '',
"ip" TEXT NOT NULL DEFAULT '',
CONSTRAINT "AuditEvent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AuditEvent_facilityId_at_idx" ON "AuditEvent"("facilityId", "at");
-- AddForeignKey
ALTER TABLE "AuditEvent" ADD CONSTRAINT "AuditEvent_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "CostChange" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"cost" DOUBLE PRECISION NOT NULL,
"previous" DOUBLE PRECISION,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"byName" TEXT NOT NULL DEFAULT '',
CONSTRAINT "CostChange_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "CostChange_facilityId_at_idx" ON "CostChange"("facilityId", "at");
-- CreateIndex
CREATE INDEX "CostChange_itemId_at_idx" ON "CostChange"("itemId", "at");
-- AddForeignKey
ALTER TABLE "CostChange" ADD CONSTRAINT "CostChange_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CostChange" ADD CONSTRAINT "CostChange_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "Photo" ADD COLUMN "bytes" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "mime" TEXT NOT NULL DEFAULT '',
ADD COLUMN "path" TEXT NOT NULL DEFAULT '',
ALTER COLUMN "data" SET DEFAULT '';
@@ -0,0 +1,21 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totpEnabledAt" TIMESTAMP(3),
ADD COLUMN "totpSecret" TEXT NOT NULL DEFAULT '';
-- CreateTable
CREATE TABLE "RecoveryCode" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"codeHash" TEXT NOT NULL,
"usedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RecoveryCode_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "RecoveryCode_userId_idx" ON "RecoveryCode"("userId");
-- AddForeignKey
ALTER TABLE "RecoveryCode" ADD CONSTRAINT "RecoveryCode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,34 @@
-- AlterTable
ALTER TABLE "Staff" ADD COLUMN "activateCode" TEXT;
-- CreateTable
CREATE TABLE "StaffAccount" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" TIMESTAMP(3),
CONSTRAINT "StaffAccount_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "StaffAccount_staffId_key" ON "StaffAccount"("staffId");
-- CreateIndex
CREATE UNIQUE INDEX "StaffAccount_email_key" ON "StaffAccount"("email");
-- CreateIndex
CREATE INDEX "StaffAccount_facilityId_idx" ON "StaffAccount"("facilityId");
-- CreateIndex
CREATE UNIQUE INDEX "Staff_activateCode_key" ON "Staff"("activateCode");
-- AddForeignKey
ALTER TABLE "StaffAccount" ADD CONSTRAINT "StaffAccount_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StaffAccount" ADD CONSTRAINT "StaffAccount_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,228 @@
-- AlterTable
ALTER TABLE "Staff" ADD COLUMN "managerId" TEXT,
ADD COLUMN "wardDesk" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "Request" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"subjectId" TEXT NOT NULL,
"raisedByStaffId" TEXT,
"raisedByUserId" TEXT,
"raisedByName" TEXT NOT NULL DEFAULT '',
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"qty" INTEGER NOT NULL DEFAULT 1,
"reason" TEXT NOT NULL DEFAULT '',
"note" TEXT NOT NULL DEFAULT '',
"status" TEXT NOT NULL DEFAULT 'awaiting',
"managerId" TEXT,
"managerName" TEXT NOT NULL DEFAULT '',
"decidedAt" TIMESTAMP(3),
"declineReason" TEXT,
"route" TEXT,
"collectCode" TEXT,
"holdUntil" TEXT NOT NULL DEFAULT '',
"signerName" TEXT,
"signerRole" TEXT,
"signedAt" TIMESTAMP(3),
"claimedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Request_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RequestEvent" (
"id" TEXT NOT NULL,
"requestId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"meta" TEXT NOT NULL DEFAULT '',
"actorName" TEXT NOT NULL DEFAULT '',
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RequestEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RequestMessage" (
"id" TEXT NOT NULL,
"requestId" TEXT NOT NULL,
"fromStaff" BOOLEAN NOT NULL,
"authorName" TEXT NOT NULL DEFAULT '',
"body" TEXT NOT NULL,
"readAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RequestMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WaitlistEntry" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"offeredAt" TIMESTAMP(3),
"acceptedAt" TIMESTAMP(3),
"leftAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WaitlistEntry_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KitCheck" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"dueBy" TEXT NOT NULL,
"openedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"closedAt" TIMESTAMP(3),
"openedBy" TEXT NOT NULL DEFAULT '',
CONSTRAINT "KitCheck_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KitCheckAnswer" (
"id" TEXT NOT NULL,
"kitCheckId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"onRecord" INTEGER NOT NULL,
"confirmed" INTEGER NOT NULL,
"answeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "KitCheckAnswer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DamageReport" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"issueId" TEXT,
"kind" TEXT NOT NULL,
"note" TEXT NOT NULL DEFAULT '',
"photoId" TEXT,
"requestId" TEXT,
"handedInAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DamageReport_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LinenNotice" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"body" TEXT NOT NULL,
"startsAt" TEXT NOT NULL DEFAULT '',
"endsAt" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LinenNotice_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Request_facilityId_status_idx" ON "Request"("facilityId", "status");
-- CreateIndex
CREATE INDEX "Request_subjectId_idx" ON "Request"("subjectId");
-- CreateIndex
CREATE INDEX "Request_managerId_idx" ON "Request"("managerId");
-- CreateIndex
CREATE UNIQUE INDEX "Request_facilityId_code_key" ON "Request"("facilityId", "code");
-- CreateIndex
CREATE INDEX "RequestEvent_requestId_idx" ON "RequestEvent"("requestId");
-- CreateIndex
CREATE INDEX "RequestMessage_requestId_idx" ON "RequestMessage"("requestId");
-- CreateIndex
CREATE INDEX "WaitlistEntry_facilityId_itemId_sizeIndex_idx" ON "WaitlistEntry"("facilityId", "itemId", "sizeIndex");
-- CreateIndex
CREATE UNIQUE INDEX "WaitlistEntry_staffId_itemId_sizeIndex_key" ON "WaitlistEntry"("staffId", "itemId", "sizeIndex");
-- CreateIndex
CREATE INDEX "KitCheck_facilityId_idx" ON "KitCheck"("facilityId");
-- CreateIndex
CREATE INDEX "KitCheckAnswer_kitCheckId_staffId_idx" ON "KitCheckAnswer"("kitCheckId", "staffId");
-- CreateIndex
CREATE UNIQUE INDEX "KitCheckAnswer_kitCheckId_staffId_itemId_sizeIndex_key" ON "KitCheckAnswer"("kitCheckId", "staffId", "itemId", "sizeIndex");
-- CreateIndex
CREATE INDEX "DamageReport_facilityId_idx" ON "DamageReport"("facilityId");
-- CreateIndex
CREATE INDEX "DamageReport_staffId_idx" ON "DamageReport"("staffId");
-- CreateIndex
CREATE INDEX "LinenNotice_facilityId_idx" ON "LinenNotice"("facilityId");
-- AddForeignKey
ALTER TABLE "Request" ADD CONSTRAINT "Request_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Request" ADD CONSTRAINT "Request_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Request" ADD CONSTRAINT "Request_raisedByStaffId_fkey" FOREIGN KEY ("raisedByStaffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Request" ADD CONSTRAINT "Request_raisedByUserId_fkey" FOREIGN KEY ("raisedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Request" ADD CONSTRAINT "Request_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RequestEvent" ADD CONSTRAINT "RequestEvent_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "Request"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RequestMessage" ADD CONSTRAINT "RequestMessage_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "Request"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WaitlistEntry" ADD CONSTRAINT "WaitlistEntry_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WaitlistEntry" ADD CONSTRAINT "WaitlistEntry_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WaitlistEntry" ADD CONSTRAINT "WaitlistEntry_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KitCheck" ADD CONSTRAINT "KitCheck_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KitCheckAnswer" ADD CONSTRAINT "KitCheckAnswer_kitCheckId_fkey" FOREIGN KEY ("kitCheckId") REFERENCES "KitCheck"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KitCheckAnswer" ADD CONSTRAINT "KitCheckAnswer_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KitCheckAnswer" ADD CONSTRAINT "KitCheckAnswer_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DamageReport" ADD CONSTRAINT "DamageReport_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DamageReport" ADD CONSTRAINT "DamageReport_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DamageReport" ADD CONSTRAINT "DamageReport_issueId_fkey" FOREIGN KEY ("issueId") REFERENCES "Issue"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LinenNotice" ADD CONSTRAINT "LinenNotice_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Staff" ADD CONSTRAINT "Staff_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Facility" ADD COLUMN "requestSeq" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "RecordDispute" (
"id" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"staffId" TEXT NOT NULL,
"itemId" TEXT,
"sizeIndex" INTEGER,
"body" TEXT NOT NULL,
"resolvedAt" TIMESTAMP(3),
"resolvedBy" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RecordDispute_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "RecordDispute_facilityId_resolvedAt_idx" ON "RecordDispute"("facilityId", "resolvedAt");
-- CreateIndex
CREATE INDEX "RecordDispute_staffId_idx" ON "RecordDispute"("staffId");
-- AddForeignKey
ALTER TABLE "RecordDispute" ADD CONSTRAINT "RecordDispute_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RecordDispute" ADD CONSTRAINT "RecordDispute_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RecordDispute" ADD CONSTRAINT "RecordDispute_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- The zone a facility's "today" is measured in. Existing rows keep Brisbane, which is exactly what
-- they have been getting from the hardcoded formatter, so no stored date changes meaning.
ALTER TABLE "Facility" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'Australia/Brisbane';
@@ -0,0 +1,4 @@
-- When an activation slip was printed, so an unused one can go stale. Null for codes issued before
-- this migration: they have no known age, and the activate route treats an undated code as expired
-- rather than as forever fresh, so the linen room reissues the handful that are still outstanding.
ALTER TABLE "Staff" ADD COLUMN "activateCodeAt" TIMESTAMP(3);
@@ -0,0 +1,51 @@
-- One request, many garments.
--
-- A request carried a single garment on the row itself, so a nurse who needed a tunic, trousers
-- and a fleece raised three requests: three codes, three approval emails to the same manager on
-- the same day, three picks, three bags. The garments move onto lines here, and each line keeps
-- its own status so a manager can approve the tunic and refuse the fleece in the one decision.
--
-- Every existing request becomes exactly one line before the old columns go, so nothing is lost.
CREATE TABLE "RequestLine" (
"id" TEXT NOT NULL,
"requestId" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"sizeIndex" INTEGER NOT NULL,
"qty" INTEGER NOT NULL DEFAULT 1,
"status" TEXT NOT NULL DEFAULT 'awaiting',
"declineReason" TEXT,
"sort" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "RequestLine_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "RequestLine_requestId_idx" ON "RequestLine"("requestId");
ALTER TABLE "RequestLine" ADD CONSTRAINT "RequestLine_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "Request"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "RequestLine" ADD CONSTRAINT "RequestLine_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- The backfill. A line's status is the decision the request already carries: anything past
-- awaiting and not declined was approved and has been (or is being) picked, so its one line is
-- approved; a declined request's line is declined and keeps the reason the staff member was
-- given, because that reason is what the screens read from here on; anything still awaiting stays
-- awaiting. The line id is derived from the request id rather than generated, so the backfill is
-- deterministic and any line can still be traced back to the request it came from.
INSERT INTO "RequestLine" ("id", "requestId", "itemId", "sizeIndex", "qty", "status", "declineReason", "sort")
SELECT
'rl_' || r."id",
r."id",
r."itemId",
r."sizeIndex",
r."qty",
CASE
WHEN r."status" = 'awaiting' THEN 'awaiting'
WHEN r."status" = 'declined' THEN 'declined'
ELSE 'approved'
END,
CASE WHEN r."status" = 'declined' THEN r."declineReason" END,
0
FROM "Request" r;
-- Only now that every garment is safely on a line.
ALTER TABLE "Request" DROP COLUMN "itemId",
DROP COLUMN "sizeIndex",
DROP COLUMN "qty";
@@ -0,0 +1,6 @@
-- A counter the screens can poll to find out that somebody else changed something.
--
-- Cheap on purpose: one integer on a row every device already looks up by primary key. The
-- alternative was re-reading the facility's whole snapshot on a timer to spot a difference, which
-- on a ward phone is the entire catalogue every few seconds to learn that nothing happened.
ALTER TABLE "Facility" ADD COLUMN "rev" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,5 @@
-- Numbers the barcodes a room prints for garments that arrived without one.
--
-- Sits beside orderSeq, catalogSeq and requestSeq and is incremented in the same transaction that
-- binds the code, so two people labelling a rack at the same time cannot mint the same number.
ALTER TABLE "Facility" ADD COLUMN "barcodeSeq" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,16 @@
-- Give the slips already in people's hands a printing date.
--
-- Staff.activateCodeAt was added for an expiry rule that was then never wired up: nothing wrote the
-- column and nothing read it. Now that the activation route refuses a slip printed more than
-- fourteen days ago, a row holding a code with no date reads as printed at the epoch, so every slip
-- outstanding on the day of the deploy would be refused as expired -- telling a nurse to go and ask
-- for a new one for a slip she was handed yesterday.
--
-- Stamping them now starts their fortnight from this deploy rather than from whenever they were
-- really printed. That is deliberately generous: these codes were valid a moment ago, and the only
-- alternative is silently killing every one of them. Rows whose code has already been spent or
-- cancelled carry a null activateCode and are left alone.
UPDATE "Staff"
SET "activateCodeAt" = now()
WHERE "activateCode" IS NOT NULL
AND "activateCodeAt" IS NULL;
@@ -0,0 +1,41 @@
-- The uniform entitlement the signed order form describes.
--
-- This runs against a live linen room, so every column added here carries a default: a NOT NULL
-- with nothing behind it fails the moment a table has rows in it, and all of these do.
-- The initial kit for everybody who is not on the nursing FTE table, in sets (a set being a top and
-- a bottom). Separate from defaultEntitlement, which keeps its meaning — garments per financial
-- year — for everything that already reads it.
ALTER TABLE "Facility" ADD COLUMN "initialSets" INTEGER NOT NULL DEFAULT 3;
-- The footer of the printed order form. Empty until a facility fills them in, because the only
-- place a hospital's own e-mail address and phone number may live is that hospital's own settings.
ALTER TABLE "Facility" ADD COLUMN "coordinatorEmail" TEXT NOT NULL DEFAULT '';
ALTER TABLE "Facility" ADD COLUMN "coordinatorPhone" TEXT NOT NULL DEFAULT '';
-- Which staff groups the nursing rules apply to. The four names are the ones the form itself lists
-- under a single FTE table; USINS is on it and contains none of the letters the old test matched.
ALTER TABLE "Facility" ADD COLUMN "nursingGroups" TEXT[] DEFAULT ARRAY['Registered Nurse', 'Enrolled Nurse', 'Assistant in Nursing', 'USINS']::TEXT[];
-- Nobody's entitlement may move on deploy. Until today a group counted as nursing if its name
-- contained "nurs" anywhere, so a facility that invented "Nurse Practitioner" or "Nursing
-- Assistant" has people relying on that right now, and handing them only the four default names
-- would quietly drop them onto a counted allowance. Every group a facility actually uses — in its
-- settings list and on its staff records — that the old test called nursing is therefore carried
-- across alongside the defaults.
UPDATE "Facility" f
SET "nursingGroups" = ARRAY(
SELECT DISTINCT g
FROM (
SELECT unnest(f."nursingGroups") AS g
UNION SELECT unnest(f."staffGroups")
UNION SELECT s."group" FROM "Staff" s WHERE s."facilityId" = f."id"
) t
WHERE g <> '' AND (g ILIKE '%nurs%' OR g = ANY(f."nursingGroups"))
ORDER BY g
);
-- Combined FTE as the form writes it: "1.0" … "0.1", or "Casual". A string, like Approval.fte,
-- because Casual is not a number. Blank on every existing row, and blank proposes no kit, so
-- nothing changes for anyone already on the register until somebody sets one.
ALTER TABLE "Staff" ADD COLUMN "fte" TEXT NOT NULL DEFAULT '';
@@ -0,0 +1,11 @@
-- The ceiling for the groups that never had one.
--
-- Operational Officers have always had a cap. Kitchen, retail, nutrition, security and the
-- ambassadors have had none at all, so a request from one of them was weighed against nothing but
-- the manager's patience. Six sets — twelve garments — is the figure the owner has set, and unlike
-- the operational allowance nothing releases it on its own: the manager approves each set up to
-- this number. Nursing never reaches it, because the NUM's signature is their control.
--
-- The default is not optional here. This runs against a live linen room with a row per facility,
-- and a NOT NULL with nothing behind it fails on the first one.
ALTER TABLE "Facility" ADD COLUMN "capSets" INTEGER NOT NULL DEFAULT 6;
@@ -0,0 +1,26 @@
-- Which person on the register signed the form, not just what they were called at the time.
--
-- "Approved by" has been a typed name since the first form was entered, and the typed name is
-- staying exactly where it is. byName is the name AS SIGNED: a signed order form does not start
-- reading differently because the manager who signed it was later married, promoted or taken off
-- the register. What the typed name could never do was point at a person, so the only way to find
-- what a manager had approved was to match a string, and a spelling change broke the match
-- silently.
--
-- Nullable, and deliberately not backfilled. Every approval already recorded in this linen room
-- was typed before the register search existed, and guessing which person a signature meant would
-- put somebody's name against an approval nobody actually chose them for. Those rows keep their
-- signature and no link, which is the truth about them. New approvals get both, and a manager who
-- is not on the register at all — agency, visiting — still records as a signature alone.
ALTER TABLE "Approval" ADD COLUMN "byStaffId" TEXT;
-- The history of forms per staff member reads this column to answer "what has this manager
-- approved?". Without it that question is a scan of every approval the facility has ever recorded.
CREATE INDEX "Approval_byStaffId_idx" ON "Approval"("byStaffId");
-- ON DELETE SET NULL, and byName is what makes it safe. Removing a manager from the register must
-- not delete the approvals they signed — the sets somebody was actually issued would go off the
-- record with them — and it must not be refused either, or a room could never tidy up a manager
-- who has ever approved anything, which is most of them. The link goes, the approval and the
-- signature stay.
ALTER TABLE "Approval" ADD CONSTRAINT "Approval_byStaffId_fkey" FOREIGN KEY ("byStaffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,55 @@
-- Which staff groups start on a fixed kit, and an empty list that means what it says.
--
-- Until now two answers were made for a facility rather than by it. Whether somebody got a starting
-- kit depended on the letters "operational" appearing in their group's name, so a site that calls
-- the same job "Housekeeping" had no way to give it one. And an empty FTE-table list was read as
-- four particular nursing groups, so a site could not say that none of its groups were on the table.
-- Both answers now belong to the facility: kitGroups lists the groups on the starting kit,
-- nursingGroups the groups on the FTE table, and a group on neither is on manager approval.
--
-- This runs against live linen rooms, and every existing facility has to come out of it behaving
-- exactly as it went in. Nobody's route may move on deploy.
-- Every existing row takes the empty default here and is filled in further down.
ALTER TABLE "Facility" ADD COLUMN "kitGroups" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- A facility whose FTE-table list is empty was relying on the old reading of empty, which put four
-- named nursing groups on the table. Those four are written into that facility's own row so its
-- nurses stay where they are. This is the migration keeping a promise the product had already made
-- to that facility, not a list the product hands out: nothing reads these names after today, and a
-- facility created from here on starts with an empty list that stays empty until it names its own.
UPDATE "Facility"
SET "nursingGroups" = ARRAY['Registered Nurse', 'Enrolled Nurse', 'Assistant in Nursing', 'USINS']::TEXT[]
WHERE coalesce(cardinality("nursingGroups"), 0) = 0;
-- The starting kit went to anybody whose group had "operational" anywhere in its name, in any case.
-- The groups that test said yes to are written down as that facility's kit list: every such name in
-- its settings, and every such name actually filed on a staff record, because a roster import can
-- file people under a spelling the settings list never had and they were on the kit all the same.
-- One entry per group as the app compares names (ignoring case and outer spaces).
--
-- A group already on the FTE-table list is left off. The order form and the counter asked the
-- nursing question first, so such a group was on the FTE table, and a group may not be on both.
UPDATE "Facility" f
SET "kitGroups" = ARRAY(
SELECT min(btrim(t.g))
FROM (
SELECT unnest(f."staffGroups") AS g
UNION ALL
SELECT s."group" FROM "Staff" s WHERE s."facilityId" = f."id"
) t
WHERE t.g ~* 'operational'
AND NOT EXISTS (
SELECT 1 FROM unnest(f."nursingGroups") AS n(name) WHERE lower(btrim(n.name)) = lower(btrim(t.g))
)
GROUP BY lower(btrim(t.g))
ORDER BY 1
);
-- Only now does the default change, so nothing above was filled from it. From here a new facility
-- starts with no group on the FTE table until it says which ones are.
ALTER TABLE "Facility" ALTER COLUMN "nursingGroups" SET DEFAULT ARRAY[]::TEXT[];
-- The delivery slip's standing footer named one employer's job title. Only the default for a new
-- facility changes: every existing facility keeps the footer it has, which is its own text now.
ALTER TABLE "Facility" ALTER COLUMN "slipDeliveryFooter" SET DEFAULT 'After hours deliveries are left with the manager or team leader on duty.';
@@ -0,0 +1,22 @@
-- A garment can be for several staff groups.
--
-- A catalogue item carried one group name, or "All", so a scrub top worn by four nursing groups had
-- to be marked for everybody or entered four times. It now carries a list, and an EMPTY list means
-- every group — what "All" meant.
--
-- Every garment keeps exactly the group it has: "All" or blank becomes the empty list, any other
-- name a list of that one name. Only then does the old column go.
ALTER TABLE "CatalogItem" ADD COLUMN "groups" TEXT[] DEFAULT ARRAY[]::TEXT[];
UPDATE "CatalogItem"
SET "groups" = CASE
WHEN lower(btrim("group")) IN ('', 'all') THEN ARRAY[]::TEXT[]
ELSE ARRAY[btrim("group")]
END;
ALTER TABLE "CatalogItem" DROP COLUMN "group";
-- A garment issued outside the person's own staff group on the coordinator's override. Its own flag
-- rather than "override", which is the six-set ceiling's, so the record tells the two apart. Every
-- existing issue predates the rule and is not one.
ALTER TABLE "Issue" ADD COLUMN "offGroup" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,19 @@
-- Which cut of uniform somebody is offered.
--
-- Men are offered the men's cut, women the women's, everybody the unisex garments — and the
-- coordinator can overrule it. The field is named for what it decides rather than for anybody's
-- gender: a woman who wears the men's cut is set to Men's, and nobody has to argue about the label.
-- Its values are '', 'Men''s', 'Women''s' and 'Either'.
--
-- BLANK MEANS EVERY STYLE. This runs against live linen rooms, and a rule that took effect on
-- deploy would start refusing garments at the counter for people nobody has ever asked about. So
-- every existing record takes the blank default and goes on being offered the whole catalogue,
-- exactly as it is today, until a coordinator sets the field. Blank and 'Either' behave identically
-- when filtering; they are kept apart so the register can still list who nobody has decided yet.
ALTER TABLE "Staff" ADD COLUMN "uniformStyle" TEXT NOT NULL DEFAULT '';
-- A garment issued outside the wearer's uniform style on the coordinator's override. Its own flag
-- beside "offGroup" (the staff-group rule's) and "override" (the six-set ceiling's), so the record
-- says which rule was overridden rather than only that one was. Every existing issue predates the
-- rule and is not one.
ALTER TABLE "Issue" ADD COLUMN "offStyle" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,62 @@
--
-- belongs to none, and it lives in its own table so that a missed role check on User can never
-- become platform-wide access. Nothing existing changes: three new tables and one enum.
CREATE TYPE "OperatorRole" AS ENUM ('OWNER', 'SUPPORT');
CREATE TABLE "Operator" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" "OperatorRole" NOT NULL DEFAULT 'SUPPORT',
"passwordHash" TEXT NOT NULL,
"totpSecret" TEXT NOT NULL DEFAULT '',
"totpEnabledAt" TIMESTAMP(3),
"inactive" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" TIMESTAMP(3),
CONSTRAINT "Operator_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Operator_email_key" ON "Operator"("email");
-- Append-only. "facilityId" is a plain column and deliberately NOT a foreign key: AuditEvent's
-- facilityId cascades on delete, which would let a facility that closes its account erase every
CREATE TABLE "OperatorEvent" (
"id" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"action" TEXT NOT NULL,
"facilityId" TEXT NOT NULL DEFAULT '',
"subject" TEXT NOT NULL DEFAULT '',
"detail" TEXT NOT NULL DEFAULT '',
"ip" TEXT NOT NULL DEFAULT '',
CONSTRAINT "OperatorEvent_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "OperatorEvent_at_idx" ON "OperatorEvent"("at");
CREATE INDEX "OperatorEvent_operatorId_at_idx" ON "OperatorEvent"("operatorId", "at");
CREATE INDEX "OperatorEvent_facilityId_at_idx" ON "OperatorEvent"("facilityId", "at");
ALTER TABLE "OperatorEvent" ADD CONSTRAINT "OperatorEvent_operatorId_fkey"
FOREIGN KEY ("operatorId") REFERENCES "Operator"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- than a cookie claim, so it can be revoked before it expires. Kept forever, like the trail.
CREATE TABLE "RevealGrant" (
"id" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"facilityId" TEXT NOT NULL,
"field" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RevealGrant_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "RevealGrant_operatorId_facilityId_expiresAt_idx" ON "RevealGrant"("operatorId", "facilityId", "expiresAt");
ALTER TABLE "RevealGrant" ADD CONSTRAINT "RevealGrant_operatorId_fkey"
FOREIGN KEY ("operatorId") REFERENCES "Operator"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,14 @@
-- Cascade is right here, unlike the trail: codes are a mechanism, not a record.
CREATE TABLE "OperatorRecoveryCode" (
"id" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"codeHash" TEXT NOT NULL,
"usedAt" TIMESTAMP(3),
CONSTRAINT "OperatorRecoveryCode_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "OperatorRecoveryCode_operatorId_idx" ON "OperatorRecoveryCode"("operatorId");
ALTER TABLE "OperatorRecoveryCode" ADD CONSTRAINT "OperatorRecoveryCode_operatorId_fkey"
FOREIGN KEY ("operatorId") REFERENCES "Operator"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,74 @@
--
-- is the first thing in the product that legitimately reads every facility, and there is no
-- row-level security here — so the promise is kept by a role that CANNOT read customer content,
-- rather than by remembering not to. A future screen that queries the wrong table gets a
-- permission error, not data.
--
-- The role itself is created by hand, as postgres, before this migration reaches the box:
--
--
-- The app role cannot CREATE ROLE, but it owns every table it created, so the grants below run
-- fine under `prisma migrate deploy`. They are guarded: if the role does not exist yet, nothing
-- failure. Create the role, then run the body of this block by hand.
--
-- Facility — the three coordinator contact columns, the logo bytes, the two slip footers and the
-- separate narrow path with its own trail, never through this role.
-- User, StaffAccount — no email, no name, no password hash, no TOTP secret.
-- AuditEvent — at and op only; no userName, target, ip or userId.
-- Content tables — id and facilityId only, which is enough to count per facility and nothing
-- else. Staff also exposes `inactive` so "active staff" can be counted.
-- Child tables with no facilityId (request lines, messages, receipts, kit-check answers…) — nothing.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
RAISE NOTICE 'ops_ro does not exist — grants skipped; create the role, then apply this block by hand';
RETURN;
END IF;
GRANT USAGE ON SCHEMA public TO ops_ro;
-- The facility, minus its contacts and its prose.
GRANT SELECT (
"id", "name", "timezone", "defaultEntitlement", "initialSets", "capSets", "defaultReorder",
"exceptionHigh", "varianceReason", "glAccount", "journalDesc", "lastBackup", "barcodeLookup",
"staffGroups", "nursingGroups", "kitGroups", "orderSeq", "catalogSeq", "requestSeq", "rev",
"barcodeSeq", "slipOrg", "isDemo", "demoResetAt", "createdAt"
) ON "Facility" TO ops_ro;
-- Accounts: enough to count and to date, never to identify.
GRANT SELECT ("id", "facilityId", "role", "inactive", "createdAt", "totpEnabledAt") ON "User" TO ops_ro;
GRANT SELECT ("id", "facilityId", "staffId", "createdAt", "lastSeenAt") ON "StaffAccount" TO ops_ro;
GRANT SELECT ("id", "facilityId", "at", "op") ON "AuditEvent" TO ops_ro;
-- Content tables: count-only.
GRANT SELECT ("id", "facilityId", "inactive") ON "Staff" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Issue" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "CatalogItem" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Order" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Request" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Pickup" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Stocktake" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "HandIn" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Photo" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Barcode" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Location" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "StockLevel" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "StockMove" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Department" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Supplier" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Approval" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "Alteration" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "CostChange" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "WaitlistEntry" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "KitCheck" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "DamageReport" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "RecordDispute" TO ops_ro;
GRANT SELECT ("id", "facilityId") ON "LinenNotice" TO ops_ro;
GRANT SELECT ON "ContactMessage" TO ops_ro;
-- Applied-migrations, for the drift tile: what the database has against what the repo ships.
GRANT SELECT ON "_prisma_migrations" TO ops_ro;
END $$;
@@ -0,0 +1,13 @@
--
-- stray query in a projection would.
--
-- Created by hand as postgres before this migration reaches the box:
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_reveal') THEN
RAISE NOTICE 'ops_reveal does not exist — grants skipped; create the role, then apply this block by hand';
RETURN;
END IF;
GRANT USAGE ON SCHEMA public TO ops_reveal;
GRANT SELECT ("id", "coordinator", "coordinatorEmail", "coordinatorPhone") ON "Facility" TO ops_reveal;
END $$;
@@ -0,0 +1,22 @@
CREATE TABLE "PlatformSwitch" (
"id" TEXT NOT NULL DEFAULT 'platform',
"signupsDisabled" BOOLEAN NOT NULL DEFAULT false,
"demoDisabled" BOOLEAN NOT NULL DEFAULT false,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PlatformSwitch_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "Facility" ADD COLUMN "plan" TEXT NOT NULL DEFAULT '',
ADD COLUMN "planNote" TEXT NOT NULL DEFAULT '';
-- no grant, no failed deploy. (`prisma migrate deploy` runs as the app role, which owns the table,
-- so the grant itself needs no more than that.)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
RAISE NOTICE 'ops_ro does not exist — plan grants skipped';
RETURN;
END IF;
GRANT SELECT ("plan", "planNote") ON "Facility" TO ops_ro;
END $$;
@@ -0,0 +1,18 @@
-- Single sign-on per facility. The IdP metadata itself lives in the Jackson broker, keyed by
-- facility id; these columns are the switches and the routing (email domains → facility).
ALTER TABLE "Facility" ADD COLUMN "ssoEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "ssoRequired" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "ssoStaff" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "ssoDomains" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
-- The admin who keeps a working password when the facility requires SSO.
ALTER TABLE "User" ADD COLUMN "ssoBreakGlass" BOOLEAN NOT NULL DEFAULT false;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
RAISE NOTICE 'ops_ro does not exist — sso grants skipped';
RETURN;
END IF;
GRANT SELECT ("ssoEnabled", "ssoRequired", "ssoStaff") ON "Facility" TO ops_ro;
END $$;
@@ -0,0 +1,22 @@
-- Plans, phase 1: the columns lib/plan.ts reads, and the promise kept before anything reads them.
ALTER TABLE "Facility" ADD COLUMN "planStatus" TEXT NOT NULL DEFAULT 'free',
ADD COLUMN "trialEndsAt" TIMESTAMP(3),
ADD COLUMN "paidUntil" TIMESTAMP(3),
ADD COLUMN "billingEmail" TEXT NOT NULL DEFAULT '',
ADD COLUMN "grandfathered" BOOLEAN NOT NULL DEFAULT false;
-- Every facility that exists when this runs was created while the pricing page said "free, and the
-- rooms using it will hear before the website does". They keep everything, for good. The demo is
-- not a customer and is left out so it never shows a plan.
UPDATE "Facility" SET "grandfathered" = true, "planStatus" = 'free' WHERE "isDemo" = false;
ALTER TABLE "PlatformSwitch" ADD COLUMN "plansLive" BOOLEAN NOT NULL DEFAULT false;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
RAISE NOTICE 'ops_ro does not exist — plan grants skipped';
RETURN;
END IF;
GRANT SELECT ("planStatus", "trialEndsAt", "paidUntil", "grandfathered") ON "Facility" TO ops_ro;
END $$;
@@ -0,0 +1,99 @@
-- Health Service plan (phase 5): organisations above facilities. And the two Stripe id columns for
-- the dormant card path (phase 4), added here so one migration carries both phases.
CREATE TABLE "Organisation" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"plan" TEXT NOT NULL DEFAULT 'health_service',
"planStatus" TEXT NOT NULL DEFAULT 'trial',
"trialEndsAt" TIMESTAMP(3),
"paidUntil" TIMESTAMP(3),
"billingEmail" TEXT NOT NULL DEFAULT '',
"planNote" TEXT NOT NULL DEFAULT '',
"ssoEnabled" BOOLEAN NOT NULL DEFAULT false,
"ssoDomains" TEXT[] DEFAULT ARRAY[]::TEXT[],
CONSTRAINT "Organisation_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "OrgUser" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"totpSecret" TEXT NOT NULL DEFAULT '',
"totpEnabledAt" TIMESTAMP(3),
"inactive" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" TIMESTAMP(3),
CONSTRAINT "OrgUser_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "OrgUser_email_key" ON "OrgUser"("email");
CREATE INDEX "OrgUser_orgId_idx" ON "OrgUser"("orgId");
ALTER TABLE "OrgUser" ADD CONSTRAINT "OrgUser_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "OrgCatalogItem" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"item" TEXT NOT NULL,
"gender" TEXT NOT NULL DEFAULT 'Unisex',
"type" TEXT NOT NULL DEFAULT '',
"sku" TEXT NOT NULL DEFAULT '',
"supplier" TEXT NOT NULL DEFAULT '',
"cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
"groups" TEXT[] DEFAULT ARRAY[]::TEXT[],
"sizes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"notes" TEXT NOT NULL DEFAULT '',
"archived" BOOLEAN NOT NULL DEFAULT false,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrgCatalogItem_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "OrgCatalogItem_orgId_item_gender_key" ON "OrgCatalogItem"("orgId", "item", "gender");
ALTER TABLE "OrgCatalogItem" ADD CONSTRAINT "OrgCatalogItem_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "OrgSupplier" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"contact" TEXT NOT NULL DEFAULT '',
"phone" TEXT NOT NULL DEFAULT '',
"account" TEXT NOT NULL DEFAULT '',
"lead" INTEGER,
CONSTRAINT "OrgSupplier_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "OrgSupplier_orgId_name_key" ON "OrgSupplier"("orgId", "name");
ALTER TABLE "OrgSupplier" ADD CONSTRAINT "OrgSupplier_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "OrgEvent" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"userId" TEXT,
"action" TEXT NOT NULL,
"detail" TEXT NOT NULL DEFAULT '',
"ip" TEXT NOT NULL DEFAULT '',
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrgEvent_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "OrgEvent_orgId_at_idx" ON "OrgEvent"("orgId", "at");
ALTER TABLE "OrgEvent" ADD CONSTRAINT "OrgEvent_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "OrgEvent" ADD CONSTRAINT "OrgEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "OrgUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Facility" ADD COLUMN "orgId" TEXT,
ADD COLUMN "stripeCustomerId" TEXT NOT NULL DEFAULT '',
ADD COLUMN "stripeSubscriptionId" TEXT NOT NULL DEFAULT '';
CREATE INDEX "Facility_orgId_idx" ON "Facility"("orgId");
ALTER TABLE "Facility" ADD CONSTRAINT "Facility_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organisation"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "CatalogItem" ADD COLUMN "orgItemId" TEXT;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
RAISE NOTICE 'ops_ro does not exist — organisation grants skipped';
RETURN;
END IF;
GRANT SELECT ("orgId") ON "Facility" TO ops_ro;
GRANT SELECT ("id", "name", "createdAt", "plan", "planStatus", "trialEndsAt", "paidUntil", "planNote", "ssoEnabled") ON "Organisation" TO ops_ro;
GRANT SELECT ("id", "orgId", "name", "inactive", "createdAt", "lastSeenAt", "totpEnabledAt") ON "OrgUser" TO ops_ro;
END $$;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+1295
View File
File diff suppressed because it is too large Load Diff