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 38e16eb on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
@@ -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 $$;
|
||||
Reference in New Issue
Block a user