import { and, asc, eq, ilike, isNull, sql, type SQL } from "drizzle-orm" import type { AnyPgColumn, AnyPgTable } from "drizzle-orm/pg-core" import type { FastifyInstance } from "fastify" import { authProfiles, checks, contracts, createddocuments, customers, incominginvoices, inventoryitems, plants, products, projects, vehicles, vendors } from "../../db/schema" type Target = { label: string; table: AnyPgTable; id: AnyPgColumn; tenant: AnyPgColumn; name: AnyPgColumn; available: SQL; uuid?: boolean } export const assignmentTargets: Record = { project: { label: "Projekt", table: projects, id: projects.id, tenant: projects.tenant, name: projects.name, available: eq(projects.archived, false) }, customer: { label: "Kunde", table: customers, id: customers.id, tenant: customers.tenant, name: customers.name, available: eq(customers.archived, false) }, vendor: { label: "Lieferant", table: vendors, id: vendors.id, tenant: vendors.tenant, name: vendors.name, available: eq(vendors.archived, false) }, vehicle: { label: "Fahrzeug", table: vehicles, id: vehicles.id, tenant: vehicles.tenant, name: vehicles.license_plate, available: eq(vehicles.archived, false) }, plant: { label: "Objekt", table: plants, id: plants.id, tenant: plants.tenant, name: plants.name, available: eq(plants.archived, false) }, contract: { label: "Vertrag", table: contracts, id: contracts.id, tenant: contracts.tenant, name: contracts.name, available: eq(contracts.archived, false) }, product: { label: "Produkt", table: products, id: products.id, tenant: products.tenant, name: products.name, available: eq(products.archived, false) }, createddocument: { label: "Ausgangsbeleg", table: createddocuments, id: createddocuments.id, tenant: createddocuments.tenant, name: createddocuments.documentNumber, available: eq(createddocuments.archived, false) }, incominginvoice: { label: "Eingangsrechnung", table: incominginvoices, id: incominginvoices.id, tenant: incominginvoices.tenant, name: incominginvoices.reference, available: eq(incominginvoices.archived, false) }, inventoryitem: { label: "Inventarartikel", table: inventoryitems, id: inventoryitems.id, tenant: inventoryitems.tenant, name: inventoryitems.name, available: eq(inventoryitems.archived, false) }, check: { label: "Überprüfung", table: checks, id: checks.id, tenant: checks.tenant, name: checks.name, available: eq(checks.archived, false), uuid: true }, authProfile: { label: "Mitarbeiter", table: authProfiles, id: authProfiles.id, tenant: authProfiles.tenant_id, name: authProfiles.full_name, available: and(eq(authProfiles.active, true), isNull(authProfiles.customer_for_portal)), uuid: true }, } export const assignmentKinds = Object.entries(assignmentTargets).map(([value, target]) => ({ value, label: target.label })) export type FileAssignments = Record const invalidAssignment = () => Object.assign(new Error("Die gewählte Zuordnung ist nicht verfügbar."), { statusCode: 400 }) const targetFor = (kind: string) => { if (!Object.prototype.hasOwnProperty.call(assignmentTargets, kind)) throw invalidAssignment() return assignmentTargets[kind] } export function assignmentId(kind: string, id: unknown) { const target = targetFor(kind) if (target.uuid) { if (typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) throw invalidAssignment() return id } if (!/^[1-9]\d*$/.test(String(id)) || !Number.isSafeInteger(Number(id))) throw invalidAssignment() return Number(id) } type Reader = Pick export async function assignmentOptions(db: Reader, tenantId: number, kind: string, search = "", limit = 100, offset = 0) { const target = targetFor(kind) const name = sql`coalesce(nullif(${target.name}, ''), '#' || ${target.id}::text)` const rows = await db.select({ id: target.id, name }).from(target.table) .where(and(eq(target.tenant, tenantId), target.available, search ? ilike(name, `%${search.replace(/[\\%_]/g, "\\$&")}%`) : undefined)) .orderBy(asc(target.name), asc(target.id)).limit(limit).offset(offset) return rows.map(row => ({ id: String(row.id), name: row.name })) } export async function currentAssignments(db: Reader, tenantId: number, file: Record) { const assignments = [] for (const [kind, target] of Object.entries(assignmentTargets)) { if (file[kind] == null) continue const [entry] = await db.select({ name: target.name }).from(target.table) .where(and(eq(target.id, file[kind]), eq(target.tenant, tenantId), target.available)).limit(1) assignments.push({ kind, id: String(file[kind]), name: entry?.name || (entry ? `#${file[kind]}` : "Nicht mehr verfügbar"), available: Boolean(entry) }) } return assignments } export async function validateAssignments(tx: Reader, tenantId: number, assignments: FileAssignments) { const changes: FileAssignments = {} for (const [kind, value] of Object.entries(assignments)) { const target = targetFor(kind) if (value === null) { changes[kind] = null; continue } const id = assignmentId(kind, value) const [row] = await tx.select({ id: target.id }).from(target.table) .where(and(eq(target.id, id), eq(target.tenant, tenantId), target.available)).for("share").limit(1) if (!row) throw invalidAssignment() changes[kind] = id } return changes }