Downtime catalog

This commit is contained in:
Marcelo
2026-05-06 00:36:48 +00:00
parent 0491237bad
commit bfc1673d89
42 changed files with 8035 additions and 1093 deletions

98
lib/reasonCatalogDb.ts Normal file
View File

@@ -0,0 +1,98 @@
import type { Prisma } from "@prisma/client";
import { prisma } from "@/lib/prisma";
import type { ReasonCatalog, ReasonCatalogCategory, ReasonCatalogDetail } from "@/lib/reasonCatalog";
import { normalizeReasonCatalog } from "@/lib/reasonCatalog";
import { loadFallbackReasonCatalog } from "@/lib/reasonCatalogFallback";
function isPlainObject(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
/**
* Full printed code from category prefix + operator numeric suffix (or suffix digits from seed).
* Downtime-style keys use a hyphen before the numeric part (e.g. DTPRC-01); short scrap-style
* prefixes (e.g. MX) concatenate without hyphen (MX001).
*/
export function composeReasonCode(prefix: string, suffix: string): string {
const p = String(prefix ?? "").trim().toUpperCase();
const s = String(suffix ?? "").trim();
if (/^\d+$/.test(s) && p.length >= 3) {
return `${p}-${s}`.toUpperCase();
}
return `${p}${s}`.toUpperCase();
}
export function isNumericSuffix(value: string): boolean {
return /^\d+$/.test(String(value ?? "").trim());
}
function mapKind(kind: string): "downtime" | "scrap" | null {
const k = String(kind).toLowerCase();
if (k === "downtime" || k === "scrap") return k;
return null;
}
/**
* Load catalog from Postgres tables. Returns null if org has no catalog rows yet.
* Includes inactive rows for historical label resolution (same as prior JSON behavior).
*/
export async function loadReasonCatalogFromDb(
orgId: string,
catalogVersion: number
): Promise<ReasonCatalog | null> {
const rows = await prisma.reasonCatalogCategory.findMany({
where: { orgId },
include: {
items: { orderBy: { sortOrder: "asc" } },
},
orderBy: [{ kind: "asc" }, { sortOrder: "asc" }],
});
if (!rows.length) return null;
const downtime: ReasonCatalogCategory[] = [];
const scrap: ReasonCatalogCategory[] = [];
for (const cat of rows) {
const k = mapKind(cat.kind);
if (!k) continue;
const details: ReasonCatalogDetail[] = cat.items.map((it) => ({
id: it.id,
label: it.name,
reasonCode: it.reasonCode,
active: it.active,
}));
const bucket: ReasonCatalogCategory = {
id: cat.id,
label: cat.name,
details,
};
if (k === "downtime") downtime.push(bucket);
else scrap.push(bucket);
}
if (!downtime.length && !scrap.length) return null;
return { version: Math.max(1, catalogVersion), downtime, scrap };
}
/** DB first, then legacy JSON in defaults, then file fallback. */
export async function effectiveReasonCatalogForOrg(
orgId: string,
defaultsJson: unknown,
settingsVersion: number
): Promise<ReasonCatalog> {
const fromDb = await loadReasonCatalogFromDb(orgId, settingsVersion);
if (fromDb) return fromDb;
const defs = isPlainObject(defaultsJson) ? defaultsJson : {};
const fromJson = normalizeReasonCatalog(defs.reasonCatalog ?? defs.reasonCatalogData);
if (fromJson) return fromJson;
return loadFallbackReasonCatalog();
}
export async function bumpOrgSettingsVersion(tx: Prisma.TransactionClient, orgId: string, userId: string) {
await tx.orgSettings.update({
where: { orgId },
data: { version: { increment: 1 }, updatedBy: userId },
});
}