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 d947f89 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
@@ -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 $$;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- On-site checkout: the business details a facility types when it subscribes by card, the
|
||||
-- cadence it chose, and when it agreed to the Terms, the Privacy Policy and the SLA.
|
||||
ALTER TABLE "Facility" ADD COLUMN "billingLegalName" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "billingTaxId" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "billingAddress" JSONB,
|
||||
ADD COLUMN "billingCountry" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "billingCadence" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "agreedTermsAt" TIMESTAMP(3),
|
||||
ADD COLUMN "agreedPrivacyAt" TIMESTAMP(3),
|
||||
ADD COLUMN "agreedSlaAt" TIMESTAMP(3),
|
||||
ADD COLUMN "agreedVersion" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
|
||||
RAISE NOTICE 'ops_ro does not exist — checkout grants skipped';
|
||||
RETURN;
|
||||
END IF;
|
||||
GRANT SELECT ("billingCountry", "billingCadence", "agreedTermsAt", "agreedPrivacyAt", "agreedSlaAt", "agreedVersion") ON "Facility" TO ops_ro;
|
||||
END $$;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Billing reminders sent (trial ending, trial ended, read-only), one row each, so the daily timer
|
||||
-- is idempotent. The kind carries the period end it refers to.
|
||||
CREATE TABLE "PlanMail" (
|
||||
"id" TEXT NOT NULL,
|
||||
"facilityId" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "PlanMail_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "PlanMail_facilityId_kind_key" ON "PlanMail"("facilityId", "kind");
|
||||
ALTER TABLE "PlanMail" ADD CONSTRAINT "PlanMail_facilityId_fkey" FOREIGN KEY ("facilityId") REFERENCES "Facility"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ops_ro') THEN
|
||||
RAISE NOTICE 'ops_ro does not exist — PlanMail grant skipped';
|
||||
RETURN;
|
||||
END IF;
|
||||
GRANT SELECT ON "PlanMail" TO ops_ro;
|
||||
END $$;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- The supplier order list: a supplier product code per size, a supplier email address, and when an
|
||||
-- its id/facilityId-only grants on these tables.
|
||||
ALTER TABLE "StockLevel" ADD COLUMN "supplierCode" TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE "Supplier" ADD COLUMN "email" TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE "Order" ADD COLUMN "emailedAt" TIMESTAMP(3);
|
||||
ALTER TABLE "Order" ADD COLUMN "printedAt" TIMESTAMP(3);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- The first-run checklist on the coordinator dashboard can be dismissed; that is the only state it
|
||||
-- keeps. Everything else it shows is derived from the records already in the snapshot.
|
||||
ALTER TABLE "Facility" ADD COLUMN "checklistDismissed" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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"
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user