import { readdirSync } from "fs"; import path from "path"; import { opsDb } from "./db"; import { entitlements, type Entitlements, type PlanRow } from "../plan"; /* Everything the operations console knows about the facilities, in one place. * * This is the only module in the product that reads across facilities, and it reads through * opsDb() — the ops_ro role, which the database itself restricts to control-plane columns. So the * shape of what leaves here is decided twice: by the SELECTs below, and by grants that would * refuse a wider one. Nothing in a projection is a person: no name, no email, no photo, no note. * Group lists come out as lengths, the GL account as set-or-not, and contacts do not come out at * all — a reveal is a separate, narrow, trailed path. * * The demo facility is rebuilt every twenty minutes and would otherwise dominate every count, so * every projection splits it out rather than mixing it in. * * scripts/check-ops.sh asserts this file never imports the main client. */ const DAY = 24 * 60 * 60 * 1000; /** The facility columns the console may read — the same allow-list as the ops_ro grant. */ const FACILITY_COLS = { id: true, name: true, timezone: true, defaultEntitlement: true, initialSets: true, capSets: true, defaultReorder: true, exceptionHigh: true, varianceReason: true, glAccount: true, journalDesc: true, lastBackup: true, barcodeLookup: true, staffGroups: true, nursingGroups: true, kitGroups: true, orderSeq: true, catalogSeq: true, requestSeq: true, rev: true, barcodeSeq: true, slipOrg: true, isDemo: true, demoResetAt: true, createdAt: true, plan: true, planNote: true, ssoEnabled: true, ssoRequired: true, ssoStaff: true, planStatus: true, trialEndsAt: true, paidUntil: true, grandfathered: true, } as const; /** The plan as the console shows it — the raw columns for the form, and what they mean today. */ export type PlanView = { plan: string; planNote: string; planStatus: string; trialEndsAt: Date | null; paidUntil: Date | null; grandfathered: boolean; label: string; state: Entitlements["state"]; readOnly: boolean; maxStaff: number | null; endsAt: Date | null; graceEndsAt: Date | null; }; function planView(f: PlanRow & { plan: string; planNote: string }): PlanView { const e = entitlements(f); return { plan: f.plan, planNote: f.planNote, planStatus: f.planStatus, trialEndsAt: f.trialEndsAt, paidUntil: f.paidUntil, grandfathered: f.grandfathered, label: e.label, state: e.state, readOnly: e.readOnly, maxStaff: e.maxStaff, endsAt: e.endsAt, graceEndsAt: e.graceEndsAt, }; } export type FacilityRow = { id: string; name: string; createdAt: Date; isDemo: boolean; timezone: string; rev: number; lastBackup: string; backupAgeDays: number | null; plan: string; planNote: string; planView: PlanView; users: number; staff: number; items: number; issues: number; requests: number; groups: number; setUp: boolean; state: "live" | "dormant" | "unconfigured" | "demo"; }; /** "YYYY-MM-DD" in the facility's zone → whole days ago, or null when never. */ function ageDays(lastBackup: string): number | null { if (!lastBackup) return null; const t = Date.parse(lastBackup + "T00:00:00Z"); return Number.isFinite(t) ? Math.max(0, Math.floor((Date.now() - t) / DAY)) : null; } function stateOf(f: { isDemo: boolean; rev: number; staffGroups: string[] }): FacilityRow["state"] { if (f.isDemo) return "demo"; if (f.staffGroups.length === 0) return "unconfigured"; return f.rev > 0 ? "live" : "dormant"; } export async function facilities(): Promise { const rows = await opsDb().facility.findMany({ select: { ...FACILITY_COLS, _count: { select: { users: true, staff: true, items: true, issues: true, requests: true } } }, orderBy: { createdAt: "desc" }, }); return rows.map((f) => ({ id: f.id, name: f.name, createdAt: f.createdAt, isDemo: f.isDemo, timezone: f.timezone, rev: f.rev, lastBackup: f.lastBackup, backupAgeDays: ageDays(f.lastBackup), plan: f.plan, planNote: f.planNote, planView: planView(f), users: f._count.users, staff: f._count.staff, items: f._count.items, issues: f._count.issues, requests: f._count.requests, groups: f.staffGroups.length, setUp: f.staffGroups.length > 0, state: stateOf(f), })); } export type FacilityDetail = FacilityRow & { config: { capSets: number; initialSets: number; defaultEntitlement: number; defaultReorder: number; exceptionHigh: number; varianceReason: number; fteGroups: number; kitGroups: number; groups: number; glAccountSet: boolean; journalDesc: string; barcodeLookup: boolean; slipOrgSet: boolean; sso: "off" | "optional" | "required"; ssoStaff: boolean; }; counts: { users: number; admins: number; usersWith2fa: number; inactiveUsers: number; staff: number; activeStaff: number; staffAccounts: number; accountsSeen7d: number; items: number; issues: number; orders: number; requests: number; events24h: number; }; seq: { orders: number; catalogue: number; requests: number; barcodes: number }; }; export async function facility(id: string): Promise { const db = opsDb(); const f = await db.facility.findUnique({ where: { id }, select: FACILITY_COLS }); if (!f) return null; const since7d = new Date(Date.now() - 7 * DAY); const since24h = new Date(Date.now() - DAY); const [users, admins, usersWith2fa, inactiveUsers, staff, activeStaff, staffAccounts, accountsSeen7d, items, issues, orders, requests, events24h] = await Promise.all([ db.user.count({ where: { facilityId: id } }), db.user.count({ where: { facilityId: id, role: "ADMIN", inactive: false } }), db.user.count({ where: { facilityId: id, totpEnabledAt: { not: null } } }), db.user.count({ where: { facilityId: id, inactive: true } }), db.staff.count({ where: { facilityId: id } }), db.staff.count({ where: { facilityId: id, inactive: false } }), db.staffAccount.count({ where: { facilityId: id } }), db.staffAccount.count({ where: { facilityId: id, lastSeenAt: { gte: since7d } } }), db.catalogItem.count({ where: { facilityId: id } }), db.issue.count({ where: { facilityId: id } }), db.order.count({ where: { facilityId: id } }), db.request.count({ where: { facilityId: id } }), db.auditEvent.count({ where: { facilityId: id, at: { gte: since24h } } }), ]); return { id: f.id, name: f.name, createdAt: f.createdAt, isDemo: f.isDemo, timezone: f.timezone, rev: f.rev, lastBackup: f.lastBackup, backupAgeDays: ageDays(f.lastBackup), plan: f.plan, planNote: f.planNote, planView: planView(f), users, staff, items, issues, requests, groups: f.staffGroups.length, setUp: f.staffGroups.length > 0, state: stateOf(f), config: { capSets: f.capSets, initialSets: f.initialSets, defaultEntitlement: f.defaultEntitlement, defaultReorder: f.defaultReorder, exceptionHigh: f.exceptionHigh, varianceReason: f.varianceReason, fteGroups: f.nursingGroups.length, kitGroups: f.kitGroups.length, groups: f.staffGroups.length, glAccountSet: !!f.glAccount, journalDesc: f.journalDesc, barcodeLookup: f.barcodeLookup, slipOrgSet: !!f.slipOrg, sso: f.ssoEnabled ? (f.ssoRequired ? "required" : "optional") : "off", ssoStaff: f.ssoStaff, }, counts: { users, admins, usersWith2fa, inactiveUsers, staff, activeStaff, staffAccounts, accountsSeen7d, items, issues, orders, requests, events24h }, seq: { orders: f.orderSeq, catalogue: f.catalogSeq, requests: f.requestSeq, barcodes: f.barcodeSeq }, }; } export type Overview = { facilities: number; live: number; dormant: number; unconfigured: number; demo: number; signupsThisMonth: number; reachedFirstIssue: number; overdueBackups: FacilityRow[]; neverIssued: FacilityRow[]; unconfiguredList: FacilityRow[]; enquiriesUnhandled: number; migrations: { applied: number; inRepo: number }; demoRebuiltAgoMin: number | null; }; /** How many migration folders the repo ships, read from disk on the box that is running. */ function migrationsInRepo(): number { try { return readdirSync(path.join(process.cwd(), "prisma", "migrations"), { withFileTypes: true }).filter((d) => d.isDirectory()).length; } catch { return 0; } } export async function overview(): Promise { const db = opsDb(); const all = await facilities(); const real = all.filter((f) => !f.isDemo); const monthStart = new Date(); monthStart.setUTCDate(1); monthStart.setUTCHours(0, 0, 0, 0); const [enquiriesUnhandled, appliedRows, demo] = await Promise.all([ db.contactMessage.count({ where: { handled: false } }), db.$queryRaw<{ n: bigint }[]>`SELECT count(*)::bigint AS n FROM "_prisma_migrations" WHERE "finished_at" IS NOT NULL AND "rolled_back_at" IS NULL`, db.facility.findFirst({ where: { isDemo: true }, select: { demoResetAt: true } }), ]); const signups = real.filter((f) => f.createdAt >= monthStart); return { facilities: real.length, live: real.filter((f) => f.state === "live").length, dormant: real.filter((f) => f.state === "dormant").length, unconfigured: real.filter((f) => f.state === "unconfigured").length, demo: all.length - real.length, signupsThisMonth: signups.length, reachedFirstIssue: signups.filter((f) => f.issues > 0).length, // Overdue: live facilities with no export in seven days, or never — the figure the Terms used to promise a reminder on. overdueBackups: real.filter((f) => f.state === "live" && (f.backupAgeDays === null || f.backupAgeDays > 7)), neverIssued: real.filter((f) => f.state === "dormant"), unconfiguredList: real.filter((f) => f.state === "unconfigured"), enquiriesUnhandled, migrations: { applied: Number(appliedRows[0]?.n ?? 0), inRepo: migrationsInRepo() }, demoRebuiltAgoMin: demo?.demoResetAt ? Math.floor((Date.now() - demo.demoResetAt.getTime()) / 60000) : null, }; }