71 lines
5.4 KiB
TypeScript
71 lines
5.4 KiB
TypeScript
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<string, Target> = {
|
|
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<string, string | number | null>
|
|
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<FastifyInstance["db"], "select">
|
|
export async function assignmentOptions(db: Reader, tenantId: number, kind: string, search = "") {
|
|
const target = targetFor(kind)
|
|
const name = sql<string>`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(100)
|
|
return rows.map(row => ({ id: String(row.id), name: row.name }))
|
|
}
|
|
export async function currentAssignments(db: Reader, tenantId: number, file: Record<string, unknown>) {
|
|
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
|
|
}
|