removed Routes, Introduced Single Route with Cecking

This commit is contained in:
2025-12-06 22:50:15 +01:00
parent dff2b05401
commit 0f3c8c862f
11 changed files with 1279 additions and 2094 deletions

View File

@@ -71,7 +71,7 @@ export const projects = pgTable("projects", {
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
activePhase: text("active_phase"),
active_phase: text("active_phase"),
})
export type Project = typeof projects.$inferSelect

View File

@@ -14,7 +14,7 @@ import { authUsers } from "./auth_users"
import { timesStateEnum } from "./enums"
import {sql} from "drizzle-orm";
export const staffTimeEntries = pgTable("staff_time_entries", {
export const stafftimeentries = pgTable("staff_time_entries", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
@@ -64,5 +64,5 @@ export const staffTimeEntries = pgTable("staff_time_entries", {
sickReason: text("sick_reason"),
})
export type StaffTimeEntry = typeof staffTimeEntries.$inferSelect
export type NewStaffTimeEntry = typeof staffTimeEntries.$inferInsert
export type StaffTimeEntry = typeof stafftimeentries.$inferSelect
export type NewStaffTimeEntry = typeof stafftimeentries.$inferInsert

View File

@@ -13,7 +13,6 @@ import adminRoutes from "./routes/admin";
import corsPlugin from "./plugins/cors";
import queryConfigPlugin from "./plugins/queryconfig";
import dbPlugin from "./plugins/db";
import resourceRoutes from "./routes/resources";
import resourceRoutesSpecial from "./routes/resourcesSpecial";
import fastifyCookie from "@fastify/cookie";
import historyRoutes from "./routes/history";
@@ -31,9 +30,9 @@ import staffTimeRoutes from "./routes/staff/time";
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
//Resources
import customerRoutes from "./routes/resources/customers";
import vendorRoutes from "./routes/resources/vendors";
import productsAndServicesRoutes from "./routes/resources/productsServices";
import resourceRoutes from "./routes/resources/main";
//M2M
import authM2m from "./plugins/auth.m2m";
@@ -104,7 +103,6 @@ async function main() {
await subApp.register(meRoutes);
await subApp.register(tenantRoutes);
await subApp.register(adminRoutes);
await subApp.register(resourceRoutes);
await subApp.register(resourceRoutesSpecial);
await subApp.register(historyRoutes);
await subApp.register(fileRoutes);
@@ -120,8 +118,8 @@ async function main() {
await subApp.register(staffTimeConnectRoutes);
await subApp.register(customerRoutes);
await subApp.register(vendorRoutes);
await subApp.register(productsAndServicesRoutes);
await subApp.register(resourceRoutes);
},{prefix: "/api"})

View File

@@ -1,829 +0,0 @@
import {FastifyInstance} from "fastify";
import {insertHistoryItem} from "../utils/history"
import {diffObjects} from "../utils/diff";
import {sortData} from "../utils/sort";
import {useNextNumberRangeNumber} from "../utils/functions";
import {compareValues, getNestedValue} from "../utils/helpers";
const dataTypes: any[] = {
// @ts-ignore
tasks: {
isArchivable: true,
label: "Aufgaben",
labelSingle: "Aufgabe",
isStandardEntity: true,
redirect: true,
historyItemHolder: "task",
supabaseSelectWithInformation: "*, plant(*), project(*), customer(*)",
inputColumns: [
"Allgemeines",
"Zuweisungen"
],
showTabs: [{label: 'Informationen'}]
},
customers: {
isArchivable: true,
label: "Kunden",
labelSingle: "Kunde",
isStandardEntity: true,
redirect: true,
numberRangeHolder: "customerNumber",
historyItemHolder: "customer",
supabaseSortColumn: "customerNumber",
supabaseSelectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*)",
inputColumns: [
"Allgemeines",
"Kontaktdaten"
],
showTabs: [{label: 'Informationen'}, {label: 'Ansprechpartner'}, {label: 'Dateien'}, {label: 'Ausgangsbelege'}, {label: 'Projekte'}, {label: 'Objekte'}, {label: 'Termine'}, {label: 'Verträge'}]
},
contacts: {
isArchivable: true,
label: "Kontakte",
labelSingle: "Kontakt",
isStandardEntity: true,
redirect: true,
historyItemHolder: "contact",
supabaseSelectWithInformation: "*, customer(*), vendor(*)",
showTabs: [
{
label: 'Informationen',
}
]
},
contracts: {
isArchivable: true,
label: "Verträge",
labelSingle: "Vertrag",
isStandardEntity: true,
numberRangeHolder: "contractNumber",
redirect: true,
inputColumns: [
"Allgemeines",
"Abrechnung"
],
supabaseSelectWithInformation: "*, customer(*), files(*)",
showTabs: [{label: 'Informationen'}, {label: 'Dateien'}]
},
absencerequests: {
isArchivable: true,
label: "Abwesenheiten",
labelSingle: "Abwesenheit",
isStandardEntity: true,
supabaseSortColumn: "startDate",
supabaseSortAscending: false,
supabaseSelectWithInformation: "*",
historyItemHolder: "absencerequest",
redirect: true,
showTabs: [{label: 'Informationen'}]
},
plants: {
isArchivable: true,
label: "Objekte",
labelSingle: "Objekt",
isStandardEntity: true,
redirect: true,
historyItemHolder: "plant",
supabaseSelectWithInformation: "*, customer(id,name)",
showTabs: [
{
label: "Informationen"
}, {
label: "Projekte"
}, {
label: "Aufgaben"
}, {
label: "Dateien"
}]
},
products: {
isArchivable: true,
label: "Artikel",
labelSingle: "Artikel",
isStandardEntity: true,
redirect: true,
supabaseSelectWithInformation: "*, unit(name)",
historyItemHolder: "product",
showTabs: [
{
label: "Informationen"
}
]
},
projects: {
isArchivable: true,
label: "Projekte",
labelSingle: "Projekt",
isStandardEntity: true,
redirect: true,
historyItemHolder: "project",
numberRangeHolder: "projectNumber",
supabaseSelectWithInformation: "*, customer(id,name), plant(id,name), projecttype(name, id), tasks(*, project(id,name), customer(id,name), plant(id,name)), files(*), createddocuments(*, statementallocations(*)), events(*), times(*, profile(id, fullName))",
supabaseSortColumn: "projectNumber",
showTabs: [
{
key: "information",
label: "Informationen"
},
{
key: "phases",
label: "Phasen"
}, {
key: "tasks",
label: "Aufgaben"
}, {
key: "files",
label: "Dateien"
}, {
label: "Zeiten"
}, {
label: "Ausgangsbelege"
}, {
label: "Termine"
}/*,{
key: "timetracking",
label: "Zeiterfassung"
},{
key: "events",
label: "Termine"
},{
key: "material",
label: "Material"
}*/]
},
vehicles: {
isArchivable: true,
label: "Fahrzeuge",
labelSingle: "Fahrzeug",
isStandardEntity: true,
redirect: true,
historyItemHolder: "vehicle",
supabaseSelectWithInformation: "*, checks(*), files(*)",
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
}, {
label: 'Überprüfungen',
}
]
},
vendors: {
isArchivable: true,
label: "Lieferanten",
labelSingle: "Lieferant",
isStandardEntity: true,
redirect: true,
numberRangeHolder: "vendorNumber",
historyItemHolder: "vendor",
supabaseSortColumn: "vendorNumber",
supabaseSelectWithInformation: "*, contacts(*)",
showTabs: [
{
label: 'Informationen',
}, {
label: 'Ansprechpartner',
}, {
label: 'Dateien',
}
]
},
messages: {
label: "Nachrichten",
labelSingle: "Nachricht"
},
spaces: {
isArchivable: true,
label: "Lagerplätze",
labelSingle: "Lagerplatz",
isStandardEntity: true,
supabaseSelectWithInformation: "*, files(*)",
supabaseSortColumn: "spaceNumber",
redirect: true,
numberRangeHolder: "spaceNumber",
historyItemHolder: "space",
inputColumns: [
"Allgemeines",
"Ort"
],
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
}, {label: 'Inventarartikel'}
]
},
users: {
label: "Benutzer",
labelSingle: "Benutzer"
},
createddocuments: {
isArchivable: true,
label: "Dokumente",
labelSingle: "Dokument",
supabaseSelectWithInformation: "*, files(*), statementallocations(*)",
},
files: {
isArchivable: true,
label: "Dateien",
labelSingle: "Datei",
supabaseSelectWithInformation: "*",
},
folders: {
isArchivable: true,
label: "Ordner",
labelSingle: "Ordner",
supabaseSelectWithInformation: "*",
},
incominginvoices: {
label: "Eingangsrechnungen",
labelSingle: "Eingangsrechnung",
redirect: true
},
inventoryitems: {
isArchivable: true,
label: "Inventarartikel",
labelSingle: "Inventarartikel",
isStandardEntity: true,
supabaseSelectWithInformation: "*, files(*), vendor(id,name), currentSpace(id,name)",
redirect: true,
numberRangeHolder: "articleNumber",
historyItemHolder: "inventoryitem",
inputColumns: [
"Allgemeines",
"Anschaffung"
],
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
}
]
},
inventoryitemgroups: {
isArchivable: true,
label: "Inventarartikelgruppen",
labelSingle: "Inventarartikelgruppe",
isStandardEntity: true,
historyItemHolder: "inventoryitemgroup",
supabaseSelectWithInformation: "*",
redirect: true,
showTabs: [
{
label: 'Informationen',
}
]
},
documentboxes: {
isArchivable: true,
label: "Dokumentenboxen",
labelSingle: "Dokumentenbox",
isStandardEntity: true,
supabaseSelectWithInformation: "*, space(*), files(*)",
redirect: true,
numberRangeHolder: "key",
historyItemHolder: "documentbox",
inputColumns: [
"Allgemeines",
],
showTabs: [
{
label: 'Informationen',
}, {
label: 'Dateien',
}
]
},
services: {
isArchivable: true,
label: "Leistungen",
labelSingle: "Leistung",
isStandardEntity: true,
redirect: true,
supabaseSelectWithInformation: "*, unit(*)",
historyItemHolder: "service",
showTabs: [
{
label: 'Informationen',
}
]
},
hourrates: {
isArchivable: true,
label: "Stundensätze",
labelSingle: "Stundensatz",
isStandardEntity: true,
redirect: true,
supabaseSelectWithInformation: "*",
historyItemHolder: "hourrate",
showTabs: [
{
label: 'Informationen',
}
]
},
events: {
isArchivable: true,
label: "Termine",
labelSingle: "Termin",
isStandardEntity: true,
historyItemHolder: "event",
supabaseSelectWithInformation: "*, project(id,name), customer(*)",
redirect: true,
showTabs: [
{
label: 'Informationen',
}
]
},
profiles: {
label: "Mitarbeiter",
labelSingle: "Mitarbeiter",
redirect: true,
historyItemHolder: "profile"
},
workingtimes: {
isArchivable: true,
label: "Anwesenheiten",
labelSingle: "Anwesenheit",
redirect: true,
redirectToList: true
},
texttemplates: {
isArchivable: true,
label: "Textvorlagen",
labelSingle: "Textvorlage"
},
bankstatements: {
isArchivable: true,
label: "Kontobewegungen",
labelSingle: "Kontobewegung",
historyItemHolder: "bankStatement",
},
statementallocations: {
label: "Bankzuweisungen",
labelSingle: "Bankzuweisung"
},
productcategories: {
isArchivable: true,
label: "Artikelkategorien",
labelSingle: "Artikelkategorie",
isStandardEntity: true,
redirect: true,
supabaseSelectWithInformation: "*",
showTabs: [
{
label: 'Informationen',
}
]
},
servicecategories: {
isArchivable: true,
label: "Leistungskategorien",
labelSingle: "Leistungskategorie",
isStandardEntity: true,
redirect: true,
supabaseSelectWithInformation: "*",
showTabs: [
{
label: 'Informationen',
}
]
},
trackingtrips: {
label: "Fahrten",
labelSingle: "Fahrt",
redirect: true,
historyItemHolder: "trackingtrip",
},
projecttypes: {
isArchivable: true,
label: "Projekttypen",
labelSingle: "Projekttyp",
redirect: true,
historyItemHolder: "projecttype"
},
checks: {
isArchivable: true,
label: "Überprüfungen",
labelSingle: "Überprüfung",
isStandardEntity: true,
supabaseSelectWithInformation: "*, vehicle(id,licensePlate), profile(id, fullName), inventoryitem(name), files(*)",
redirect: true,
historyItemHolder: "check",
showTabs: [
{
label: 'Informationen',
}, {label: 'Dateien'}, {label: 'Ausführungen'}]
},
roles: {
label: "Rollen",
labelSingle: "Rolle",
redirect: true,
historyItemHolder: "role",
filters: [],
templateColumns: [
{
key: "name",
label: "Name"
}, {
key: "description",
label: "Beschreibung"
}
]
},
costcentres: {
isArchivable: true,
label: "Kostenstellen",
labelSingle: "Kostenstelle",
isStandardEntity: true,
redirect: true,
numberRangeHolder: "number",
historyItemHolder: "costcentre",
supabaseSortColumn: "number",
supabaseSelectWithInformation: "*, project(*), vehicle(*), inventoryitem(*)",
showTabs: [{label: 'Informationen'}, {label: 'Auswertung Kostenstelle'}]
},
ownaccounts: {
isArchivable: true,
label: "zusätzliche Buchungskonten",
labelSingle: "zusätzliches Buchungskonto",
isStandardEntity: true,
redirect: true,
historyItemHolder: "ownaccount",
supabaseSortColumn: "number",
supabaseSelectWithInformation: "*, statementallocations(*, bs_id(*))",
showTabs: [{label: 'Informationen'}, {label: 'Buchungen'}]
},
tickets: {
isArchivable: true,
label: "Tickets",
labelSingle: "Ticket",
},
ticketmessages: {
isArchivable: true,
label: "Nachrichten",
labelSingle: "Nachricht",
},
}
export default async function resourceRoutes(server: FastifyInstance) {
//Liste
server.get("/resource/:resource", async (req, reply) => {
if (!req.user?.tenant_id) {
return reply.code(400).send({ error: "No tenant selected" });
}
const { resource } = req.params as { resource: string };
const {select, sort, asc } = req.query as { select?: string, sort?: string, asc?: string }
console.log(select, sort, asc)
const { data, error } = await server.supabase
.from(resource)
//@ts-ignore
.select(select || dataTypes[resource].supabaseSelectWithInformation)
.eq("tenant", req.user.tenant_id)
if (error) {
console.log(error)
return reply.code(400).send({ error: error.message });
}
const sorted =sortData(data,sort,asc === "true" ? true : false)
return sorted;
});
// Helper Funktionen
// Liste Paginated
server.get("/resource/:resource/paginated", async (req, reply) => {
if (!req.user?.tenant_id) {
return reply.code(400).send({ error: "No tenant selected" });
}
const { resource } = req.params as { resource: string };
const { queryConfig } = req;
const { pagination, sort, filters, paginationDisabled } = queryConfig;
const { select, search, searchColumns, distinctColumns } = req.query as {
select?: string;
search?: string;
searchColumns?: string;
distinctColumns?: string;
};
console.log(req.query);
console.log(select);
// --- 🔍 Suche (im Backend mit Joins) ---
if (search && search.trim().length > 0) {
// 1. Alle Daten mit Joins holen (OHNE Pagination, aber mit Filtern)
let searchQuery = server.supabase
.from(resource)
.select(select || dataTypes[resource].supabaseSelectWithInformation)
.eq("tenant", req.user.tenant_id);
// --- Filterung anwenden ---
for (const [key, val] of Object.entries(filters || {})) {
if (Array.isArray(val)) {
searchQuery = searchQuery.in(key, val);
} else { // @ts-ignore
if (val === true || val === false || val === null) {
searchQuery = searchQuery.is(key, val);
} else {
searchQuery = searchQuery.eq(key, val);
}
}
}
const { data: allData, error: searchError } = await searchQuery;
if (searchError) {
server.log.error(searchError);
return reply.code(400).send({ error: searchError.message });
}
// 2. Im Backend nach Suchbegriff filtern
const searchTerm = search.trim().toLowerCase();
const searchCols = searchColumns
? searchColumns.split(",").map(c => c.trim()).filter(Boolean)
: dataTypes[resource].searchableColumns || [];
const filteredData = (allData || []).filter(row => {
/*if (searchCols.length === 0) {
// Fallback: Durchsuche alle String-Felder der Hauptebene
return Object.values(row).some(val =>
JSON.stringify(val).toString().toLowerCase().includes(searchTerm)
);
}
return searchCols.some(col => {
const value = getNestedValue(row, col);
return JSON.stringify(value).toLowerCase().includes(searchTerm);
});*/
return JSON.stringify(row).toLowerCase().includes(searchTerm);
});
// 3. Im Backend sortieren
let sortedData = [...filteredData];
if (sort?.length > 0) {
sortedData.sort((a, b) => {
for (const s of sort) {
const aVal = getNestedValue(a, s.field);
const bVal = getNestedValue(b, s.field);
const comparison = compareValues(aVal, bVal);
if (comparison !== 0) {
return s.direction === "asc" ? comparison : -comparison;
}
}
return 0;
});
}
// 4. Im Backend paginieren
const total = sortedData.length;
const paginatedData = !paginationDisabled && pagination
? sortedData.slice(pagination.offset, pagination.offset + pagination.limit)
: sortedData;
// 5. Distinct Values berechnen
const distinctValues: Record<string, any[]> = {};
if (distinctColumns) {
const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean);
for (const col of cols) {
// Distinct values aus den gefilterten Daten
const values = filteredData
.map(row => getNestedValue(row, col))
.filter(v => v !== null && v !== undefined && v !== "");
distinctValues[col] = [...new Set(values)].sort();
}
}
const totalPages = !paginationDisabled && pagination?.limit
? Math.ceil(total / pagination.limit)
: 1;
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
};
return { data: paginatedData, queryConfig: enrichedConfig };
}
// --- Standardabfrage (ohne Suche) ---
let baseQuery = server.supabase
.from(resource)
.select(select || dataTypes[resource].supabaseSelectWithInformation, { count: "exact" })
.eq("tenant", req.user.tenant_id);
// --- Filterung ---
for (const [key, val] of Object.entries(filters || {})) {
if (Array.isArray(val)) {
baseQuery = baseQuery.in(key, val);
} else { // @ts-ignore
if (val == true || val == false || val === null) {
baseQuery = baseQuery.is(key, val);
} else {
baseQuery = baseQuery.eq(key, val);
}
}
}
// --- Sortierung ---
if (sort?.length > 0) {
for (const s of sort) {
baseQuery = baseQuery.order(s.field, { ascending: s.direction === "asc" });
}
}
// --- Pagination ---
if (!paginationDisabled && pagination) {
const { offset, limit } = pagination;
baseQuery = baseQuery.range(offset, offset + limit - 1);
}
const { data, error, count } = await baseQuery;
if (error) {
server.log.error(error);
return reply.code(400).send({ error: error.message });
}
// --- Distinct-Werte (auch ohne Suche) ---
const distinctValues: Record<string, any[]> = {};
if (distinctColumns) {
const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean);
for (const col of cols) {
const { data: allRows, error: distinctErr } = await server.supabase
.from(resource)
.select(col)
.eq("tenant", req.user.tenant_id);
if (distinctErr) continue;
const values = (allRows || [])
.map((row) => row?.[col] ?? null)
.filter((v) => v !== null && v !== undefined && v !== "");
distinctValues[col] = [...new Set(values)].sort();
}
}
const total = count || 0;
const totalPages = !paginationDisabled && pagination?.limit
? Math.ceil(total / pagination.limit)
: 1;
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
};
return { data, queryConfig: enrichedConfig };
});
// Detail
server.get("/resource/:resource/:id/:with_information?", async (req, reply) => {
if (!req.user?.tenant_id) {
return reply.code(400).send({error: "No tenant selected"});
}
const {resource, id, with_information} = req.params as {
resource: string;
id: string,
with_information: boolean
};
const {select} = req.query as { select?: string }
// @ts-ignore
const {
data,
error
} = await server.supabase.from(resource).select(with_information ? dataTypes[resource].supabaseSelectWithInformation : (select ? select : "*"))
.eq("id", id)
.eq("tenant", req.user.tenant_id)
.single();
if (error || !data) {
return reply.code(404).send({error: "Not found"});
}
return data;
});
// Create
server.post("/resource/:resource", async (req, reply) => {
if (!req.user?.tenant_id) {
return reply.code(400).send({error: "No tenant selected"});
}
const {resource} = req.params as { resource: string };
const body = req.body as Record<string, any>;
const dataType = dataTypes[resource];
let createData = {
...body,
tenant: req.user.tenant_id,
archived: false, // Standardwert
}
if (dataType.numberRangeHolder && !body[dataType.numberRangeHolder]) {
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
createData[dataType.numberRangeHolder] = result.usedNumber
}
const {data, error} = await server.supabase
.from(resource)
.insert(createData)
.select("*")
.single();
if (error) {
return reply.code(400).send({error: error.message});
}
await insertHistoryItem(server, {
entity: resource,
entityId: data.id,
action: "created",
created_by: req.user.user_id,
tenant_id: req.user.tenant_id,
oldVal: null,
newVal: data,
text: `${dataType.labelSingle} erstellt`,
});
return data;
});
// UPDATE (inkl. Soft-Delete/Archive)
server.put("/resource/:resource/:id", async (req, reply) => {
console.log("hi")
const {resource, id} = req.params as { resource: string; id: string }
const body = req.body as Record<string, any>
const tenantId = (req.user as any)?.tenant_id
const userId = (req.user as any)?.user_id
if (!tenantId || !userId) {
return reply.code(401).send({error: "Unauthorized"})
}
// vorherige Version für History laden
const {data: oldItem} = await server.supabase
.from(resource)
.select("*")
.eq("id", id)
.eq("tenant", tenantId)
.single()
const {data: newItem, error} = await server.supabase
.from(resource)
.update({...body, updated_at: new Date().toISOString(), updated_by: userId})
.eq("id", id)
.eq("tenant", tenantId)
.select()
.single()
if (error) return reply.code(500).send({error})
const diffs = diffObjects(oldItem, newItem);
for (const d of diffs) {
await insertHistoryItem(server, {
entity: resource,
entityId: id,
action: d.type,
created_by: userId,
tenant_id: tenantId,
oldVal: d.oldValue ? String(d.oldValue) : null,
newVal: d.newValue ? String(d.newValue) : null,
text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""}${d.newValue ?? ""}`,
});
}
return newItem
})
}

View File

@@ -1,155 +0,0 @@
import { FastifyInstance } from "fastify"
import { asc, desc, eq, ilike, and } from "drizzle-orm"
// Beispiel-Import — wird in der echten Datei ersetzt
import { exampleTable } from "../../../db/schema/exampleTable"
export default async function exampleRoutes(server: FastifyInstance) {
// -------------------------------------------
// LIST (ohne Pagination)
// -------------------------------------------
server.get("/resource/example", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let query = server.db
.select()
.from(exampleTable)
.where(eq(exampleTable.tenant, tenantId))
// 🔍 OPTIONAL: einfache Suche (LIKE)
if (search) {
query = server.db
.select()
.from(exampleTable)
.where(
and(
eq(exampleTable.tenant, tenantId),
ilike(exampleTable.name as any, `%${search}%`)
)
)
}
// 🔄 Sortierung
if (sort) {
query = query.orderBy(
ascQuery === "true"
? asc((exampleTable as any)[sort])
: desc((exampleTable as any)[sort])
)
}
const results = await query
return results
})
// -------------------------------------------
// PAGINATED LIST
// -------------------------------------------
server.get("/resource/example/paginated", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const {
offset = "0",
limit = "25",
search,
sort,
asc: ascQuery,
} = req.query as {
offset?: string
limit?: string
search?: string
sort?: string
asc?: string
}
const offsetNum = parseInt(offset)
const limitNum = parseInt(limit)
// --- Basis WHERE
let whereClause: any = eq(exampleTable.tenant, tenantId)
// --- Suche
if (search) {
whereClause = and(
eq(exampleTable.tenant, tenantId),
ilike(exampleTable.name as any, `%${search}%`)
)
}
// -------------------------
// 1. COUNT Query (total rows)
// -------------------------
const totalRowsResult = await server.db
.select({ count: server.db.fn.count(exampleTable.id) })
.from(exampleTable)
.where(whereClause)
const total = Number(totalRowsResult[0].count)
// -------------------------
// 2. DATA Query
// -------------------------
let dataQuery = server.db
.select()
.from(exampleTable)
.where(whereClause)
.offset(offsetNum)
.limit(limitNum)
if (sort) {
dataQuery = dataQuery.orderBy(
ascQuery === "true"
? asc((exampleTable as any)[sort])
: desc((exampleTable as any)[sort])
)
}
const rows = await dataQuery
return {
data: rows,
pagination: {
total,
offset: offsetNum,
limit: limitNum,
totalPages: Math.ceil(total / limitNum),
},
}
})
// -------------------------------------------
// DETAIL ROUTE
// -------------------------------------------
server.get("/resource/example/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const result = await server.db
.select()
.from(exampleTable)
.where(
and(
eq(exampleTable.id, id),
eq(exampleTable.tenant, tenantId)
)
)
.limit(1)
if (!result.length) {
return reply.code(404).send({ error: "Not found" })
}
return result[0]
})
}

View File

@@ -1,276 +0,0 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
or
} from "drizzle-orm"
import {
contacts,
customers,
vendors
} from "../../../db/schema"
// -------------------------------------------------------------
// 🔍 Helper für SQL-Suche über mehrere Spalten
// -------------------------------------------------------------
function buildSearchCondition(table: any, columns: string[], search: string) {
if (!search || !columns.length) return null
const term = `%${search.toLowerCase()}%`
const conditions = columns
.map(c => table[c])
.filter(Boolean)
.map(col => ilike(col, term))
if (conditions.length === 0) return null
// @ts-ignore
return or(...conditions)
}
export default async function contactsRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/contacts", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId)
return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
// Grundfilter
let whereCond: any = eq(contacts.tenant, tenantId)
// 🔍 Suche
if (search) {
const searchCond = buildSearchCondition(
contacts,
["firstName", "lastName", "email", "phone", "notes"],
search
)
if (searchCond) whereCond = and(whereCond, searchCond)
}
// Query
let q = server.db.select().from(contacts).where(whereCond)
// Sortierung
if (sort) {
const field = (contacts as any)[sort]
if (field) {
//@ts-ignore
q = q.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
return await q
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/contacts/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId)
return reply.code(400).send({ error: "No tenant selected" })
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled
} = queryConfig
const { search, distinctColumns } = req.query as {
search?: string
distinctColumns?: string
}
// -----------------------------------
// WHERE CONDITIONS
// -----------------------------------
let whereCond: any = eq(contacts.tenant, tenantId)
// Filter
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (contacts as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
// 🔍 Suche
if (search && search.trim().length > 0) {
const searchCond = buildSearchCondition(
contacts,
["firstName", "lastName", "email", "phone", "notes"],
search
)
if (searchCond) whereCond = and(whereCond, searchCond)
}
// -----------------------------------
// COUNT
// -----------------------------------
const totalRes = await server.db
.select({ value: count(contacts.id) })
.from(contacts)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// -----------------------------------
// DISTINCT VALUES
// -----------------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (contacts as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(contacts)
.where(eq(contacts.tenant, tenantId))
distinctValues[colName] =
[...new Set(rows.map(r => r.v).filter(v => v !== null && v !== ""))]
.sort()
}
}
// -----------------------------------
// PAGINATION
// -----------------------------------
let offset = pagination?.offset ?? 0
let limit = pagination?.limit ?? 999999
// -----------------------------------
// ORDER
// -----------------------------------
let orderField = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (contacts as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
// -----------------------------------
// DATA QUERY
// -----------------------------------
let dataQuery = server.db
.select()
.from(contacts)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
//@ts-ignore
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
return {
data,
queryConfig: {
...queryConfig,
total,
totalPages: pagination?.limit
? Math.ceil(total / pagination.limit)
: 1,
distinctValues,
search: search || null
}
}
} catch (err) {
console.error(err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// DETAIL (customer + vendor)
// -------------------------------------------------------------
server.get("/resource/contacts/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId)
return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(contacts)
.where(
and(
eq(contacts.id, Number(id)),
eq(contacts.tenant, tenantId)
)
)
.limit(1)
if (!rows.length)
return reply.code(404).send({ error: "Not found" })
const contact = rows[0]
const [customerRecord, vendorRecord] = await Promise.all([
contact.customer
? server.db.select().from(customers).where(eq(customers.id, contact.customer))
: [],
contact.vendor
? server.db.select().from(vendors).where(eq(vendors.id, contact.vendor))
: [],
])
return {
...contact,
customer: customerRecord[0] ?? null,
vendor: vendorRecord[0] ?? null,
}
})
}

View File

@@ -1,244 +0,0 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
or
} from "drizzle-orm"
import {
contracts,
customers,
files
} from "../../../db/schema"
// -------------------------------------------------------------
// Helper: SQLLIKE Suche über mehrere Felder
// -------------------------------------------------------------
function buildSearchCondition(table: any, columns: string[], search?: string) {
if (!search) return null
const term = `%${search.toLowerCase()}%`
const conditions = columns
.map(col => table[col])
.filter(Boolean)
.map(col => ilike(col, term))
if (conditions.length === 0) return null
// @ts-ignore
return or(...conditions)
}
export default async function contractsRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/contracts", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let whereCond: any = eq(contracts.tenant, tenantId)
// SQL SEARCH
const searchCond = buildSearchCondition(
contracts,
["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
search
)
if (searchCond) whereCond = and(whereCond, searchCond)
// Query
let q = server.db
.select()
.from(contracts)
.where(whereCond)
// SORT
if (sort) {
const field = (contracts as any)[sort]
if (field) {
//@ts-ignore
q = q.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
return await q
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/contracts/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const queryConfig = req.queryConfig
const { pagination, sort, filters, paginationDisabled } = queryConfig
const { search, distinctColumns } = req.query as {
search?: string
distinctColumns?: string
}
// -----------------------------------
// WHERE
// -----------------------------------
let whereCond: any = eq(contracts.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (contracts as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
// SQL SEARCH
const searchCond = buildSearchCondition(
contracts,
["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
search
)
if (searchCond) whereCond = and(whereCond, searchCond)
// -----------------------------------
// COUNT
// -----------------------------------
const totalRes = await server.db
.select({ value: count(contracts.id) })
.from(contracts)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// -----------------------------------
// DISTINCT
// -----------------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",")) {
const col = (contracts as any)[colName.trim()]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(contracts)
.where(eq(contracts.tenant, tenantId))
distinctValues[colName] =
[...new Set(rows.map(r => r.v).filter(v => v != null && v !== ""))]
.sort()
}
}
// -----------------------------------
// PAGINATION
// -----------------------------------
let offset = pagination?.offset ?? 0
let limit = pagination?.limit ?? 999999
// -----------------------------------
// SORT
// -----------------------------------
let orderField = null
let orderDir: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (contracts as any)[s.field]
if (col) {
orderField = col
orderDir = s.direction === "asc" ? "asc" : "desc"
}
}
// -----------------------------------
// QUERY DATA
// -----------------------------------
let q = server.db
.select()
.from(contracts)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
//@ts-ignore
q = orderDir === "asc" ? q.orderBy(asc(orderField)) : q.orderBy(desc(orderField))
}
const data = await q
return {
data,
queryConfig: {
...queryConfig,
total,
totalPages: pagination?.limit ? Math.ceil(total / pagination.limit) : 1,
distinctValues,
search: search || null,
}
}
} catch (e) {
console.error(e)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// DETAIL (+ JOINS)
// -------------------------------------------------------------
server.get("/resource/contracts/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(contracts)
.where(and(eq(contracts.id, Number(id)), eq(contracts.tenant, tenantId)))
.limit(1)
if (!rows.length) return reply.code(404).send({ error: "Not found" })
const contract = rows[0]
const [customerRecord, fileList] = await Promise.all([
contract.customer
? server.db.select().from(customers).where(eq(customers.id, contract.customer))
: [],
server.db.select().from(files).where(eq(files.contract, Number(id))),
])
return {
...contract,
customer: customerRecord[0] ?? null,
files: fileList,
}
})
}

View File

@@ -1,329 +0,0 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
or,
} from "drizzle-orm"
import {
customers,
projects,
plants,
contracts,
contacts,
createddocuments,
statementallocations,
files,
events,
} from "../../../db/schema"
// -------------------------------------------------------------
// 🔍 Helper für SQL-Suche über mehrere Spalten
// -------------------------------------------------------------
function buildSearchCondition(table: any, columns: string[], search: string) {
if (!search || !columns.length) return null
const term = `%${search.toLowerCase()}%`
const conditions = columns
.map((colName) => table[colName])
.filter(Boolean)
.map((col) => ilike(col, term))
if (conditions.length === 0) return null
// @ts-ignore
return or(...conditions)
}
export default async function customerRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/customers", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
// Basisquery
let whereCond: any = eq(customers.tenant, tenantId)
// 🔍 SQL-Suche
if (search) {
const searchCond = buildSearchCondition(
customers,
["name", "customerNumber", "firstname", "lastname", "notes"],
search
)
if (searchCond) {
whereCond = and(whereCond, searchCond)
}
}
let baseQuery = server.db
.select()
.from(customers)
.where(whereCond)
// Sortierung
if (sort) {
const field = (customers as any)[sort]
if (field) {
//@ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
return await baseQuery
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/customers/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled
} = queryConfig
const {
search,
distinctColumns
} = req.query as {
search?: string
distinctColumns?: string
}
// ----------------------------
// WHERE CONDITIONS (Basis)
// ----------------------------
let whereCond: any = eq(customers.tenant, tenantId)
// Filters
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (customers as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
// ----------------------------
// 🔍 SEARCH
// ----------------------------
if (search && search.trim().length > 0) {
const searchCond = buildSearchCondition(
customers,
["name", "customerNumber", "firstname", "lastname", "notes"],
search.trim()
)
if (searchCond) {
whereCond = and(whereCond, searchCond)
}
}
// ----------------------------
// COUNT
// ----------------------------
const totalRes = await server.db
.select({ value: count(customers.id) })
.from(customers)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// ----------------------------
// DISTINCT VALUES
// ----------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (customers as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(customers)
.where(eq(customers.tenant, tenantId))
distinctValues[colName] =
[...new Set(rows.map(r => r.v).filter(v => v != null && v !== ""))]
.sort()
}
}
// ----------------------------
// PAGINATION
// ----------------------------
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
// ----------------------------
// ORDER BY
// ----------------------------
let orderField = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (customers as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
// ----------------------------
// QUERY DATA
// ----------------------------
let dataQuery = server.db
.select()
.from(customers)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
//@ts-ignore
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
// ----------------------------
// CONFIG RESPONSE
// ----------------------------
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
}
catch (e) {
console.log(e)
}
})
// -------------------------------------------------------------
// DETAIL (mit ALLEN JOINS)
// -------------------------------------------------------------
server.get("/resource/customers/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
// --- 1) Customer selbst laden
const customerRecord = await server.db
.select()
.from(customers)
.where(and(eq(customers.id, Number(id)), eq(customers.tenant, tenantId)))
.limit(1)
if (!customerRecord.length) {
return reply.code(404).send({ error: "Customer not found" })
}
const customer = customerRecord[0]
// --- 2) Relations:
const [
customerProjects,
customerPlants,
customerContracts,
customerContacts,
customerDocuments,
customerFiles,
customerEvents,
] = await Promise.all([
server.db.select().from(projects).where(eq(projects.customer, Number(id))),
server.db.select().from(plants).where(eq(plants.customer, Number(id))),
server.db.select().from(contracts).where(eq(contracts.customer, Number(id))),
server.db.select().from(contacts).where(eq(contacts.customer, Number(id))),
server.db
.select({
...createddocuments,
allocations: statementallocations,
})
.from(createddocuments)
.leftJoin(
statementallocations,
eq(statementallocations.cd_id, createddocuments.id)
)
.where(eq(createddocuments.customer, Number(id))),
server.db.select().from(files).where(eq(files.customer, Number(id))),
server.db.select().from(events).where(eq(events.customer, Number(id))),
])
return {
...customer,
projects: customerProjects,
plants: customerPlants,
contracts: customerContracts,
contacts: customerContacts,
createddocuments: customerDocuments,
files: customerFiles,
events: customerEvents,
}
})
}

View File

@@ -0,0 +1,413 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
or
} from "drizzle-orm"
import {
projects,
customers,
plants,
contracts,
projecttypes,
createddocuments,
files,
events,
tasks, contacts, vendors
} from "../../../db/schema"
import * as sea from "node:sea";
// -------------------------------------------------------------
// SQL Volltextsuche auf mehreren Feldern
// -------------------------------------------------------------
function buildSearchCondition(table: any, columns: string[], search: string) {
if (!search || !columns.length) return null
const term = `%${search.toLowerCase()}%`
const conditions = columns
.map((colName) => table[colName])
.filter(Boolean)
.map((col) => ilike(col, term))
if (conditions.length === 0) return null
// @ts-ignore
return or(...conditions)
}
export default async function resourceRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
/*server.get("/resource/:resource", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId)
return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
// WHERE-Basis
let whereCond: any = eq(projects.tenant, tenantId)
// 🔍 SQL Search
const searchCond = buildProjectSearch(search)
if (searchCond) whereCond = and(whereCond, searchCond)
// Base Query
let q = server.db.select().from(projects).where(whereCond)
// Sortierung
if (sort) {
const col = (projects as any)[sort]
if (col) {
q = ascQuery === "true"
? q.orderBy(asc(col))
: q.orderBy(desc(col))
}
}
const data = await q
return data
} catch (err) {
console.error("ERROR /resource/projects", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})*/
// -------------------------------------------------------------
// PAGINATED LIST
// -------------------------------------------------------------
server.get("/resource/:resource/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id;
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" });
}
const {resource} = req.params as {resource: string};
const {queryConfig} = req;
const {
pagination,
sort,
filters,
paginationDisabled
} = queryConfig;
const { search, distinctColumns } = req.query as {
search?: string;
distinctColumns?: string;
};
const config = {
projects: {
searchColumns: ["name"],
mtoLoad: ["customer","plant","contract","projecttype"],
table: projects
},
customers: {
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
table: customers,
},
contacts: {
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
table: contacts,
mtoLoad: ["customer","vendor"]
},
contracts: {
table: contracts,
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"]
},
plants: {
table: plants
},
projecttypes: {
table: projecttypes
},
vendors: {
table: vendors,
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
},
files: {
table: files
}
}
let table = config[resource].table
let whereCond: any = eq(table.tenant, tenantId);
if(search) {
const searchCond = buildSearchCondition(
table,
config[resource].searchColumns,
search.trim()
)
if (searchCond) {
whereCond = and(whereCond, searchCond)
}
}
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (table as any)[key];
if (!col) continue;
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val));
} else {
whereCond = and(whereCond, eq(col, val as any));
}
}
}
// -----------------------------------------------
// COUNT (for pagination)
// -----------------------------------------------
const totalRes = await server.db
.select({ value: count(table.id) })
.from(table)
.where(whereCond);
const total = Number(totalRes[0]?.value ?? 0);
// -----------------------------------------------
// DISTINCT VALUES (regardless of pagination)
// -----------------------------------------------
const distinctValues: Record<string, any[]> = {};
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(c => c.trim())) {
const col = (table as any)[colName];
if (!col) continue;
const rows = await server.db
.select({ v: col })
.from(table)
.where(eq(table.tenant, tenantId));
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "");
distinctValues[colName] = [...new Set(values)].sort();
}
}
// PAGINATION
const offset = pagination?.offset ?? 0;
const limit = pagination?.limit ?? 100;
// SORTING
let orderField: any = null;
let direction: "asc" | "desc" = "asc";
if (sort?.length > 0) {
const s = sort[0];
const col = (projects as any)[s.field];
if (col) {
orderField = col;
direction = s.direction === "asc" ? "asc" : "desc";
}
}
// MAIN QUERY (Paginated)
let q = server.db
.select()
.from(table)
.where(whereCond)
.offset(offset)
.limit(limit);
if (orderField) {
//@ts-ignore
q = direction === "asc"
? q.orderBy(asc(orderField))
: q.orderBy(desc(orderField));
}
const rows = await q;
if (!rows.length) {
return {
data: [],
queryConfig: {
...queryConfig,
total,
totalPages: 0,
distinctValues
}
};
}
// RELATION LOADING (MANY-TO-ONE)
let ids = {}
let lists = {}
let maps = {}
let data = []
if(config[resource].mtoLoad) {
config[resource].mtoLoad.forEach(relation => {
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
})
for await (const relation of config[resource].mtoLoad ) {
lists[relation] = ids[relation].length ? await server.db.select().from(config[relation + "s"].table).where(inArray(config[relation + "s"].table.id, ids[relation])) : []
}
config[resource].mtoLoad.forEach(relation => {
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
})
data = rows.map(row => {
let toReturn = {
...row
}
config[resource].mtoLoad.forEach(relation => {
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
})
return toReturn
});
}
// -----------------------------------------------
// RETURN DATA
// -----------------------------------------------
return {
data,
queryConfig: {
...queryConfig,
total,
totalPages: Math.ceil(total / limit),
distinctValues
}
};
} catch (err) {
console.error(`ERROR /resource/:resource/paginated:`, err);
return reply.code(500).send({ error: "Internal Server Error" });
}
});
// -------------------------------------------------------------
// DETAIL (mit JOINS)
// -------------------------------------------------------------
/*server.get("/resource/projects/:id", async (req, reply) => {
try {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const pid = Number(id)
const projRows = await server.db
.select()
.from(projects)
.where(and(eq(projects.id, pid), eq(projects.tenant, tenantId)))
.limit(1)
if (!projRows.length)
return reply.code(404).send({ error: "Project not found" })
const project = projRows[0]
// ------------------------------------
// LOAD RELATIONS
// ------------------------------------
const [
customerRecord,
plantRecord,
contractRecord,
projectTypeRecord,
projectTasks,
projectFiles,
projectDocuments,
projectEvents,
] = await Promise.all([
project.customer
? server.db.select().from(customers).where(eq(customers.id, project.customer))
: [],
project.plant
? server.db.select().from(plants).where(eq(plants.id, project.plant))
: [],
project.contract
? server.db.select().from(contracts).where(eq(contracts.id, project.contract))
: [],
project.projecttype
? server.db.select().from(projecttypes).where(eq(projecttypes.id, project.projecttype))
: [],
// Tasks
server.db
.select()
.from(tasks)
.where(eq(tasks.project, pid)),
// Files
server.db
.select()
.from(files)
.where(eq(files.project, pid)),
// Documents
server.db
.select()
.from(createddocuments)
.where(eq(createddocuments.project, pid)),
// Events
server.db
.select()
.from(events)
.where(eq(events.project, pid)),
])
return {
...project,
customer: customerRecord[0] ?? null,
plant: plantRecord[0] ?? null,
contract: contractRecord[0] ?? null,
projecttype: projectTypeRecord[0] ?? null,
tasks: projectTasks,
files: projectFiles,
createddocuments: projectDocuments,
events: projectEvents,
}
} catch (err) {
console.error("ERROR /resource/projects/:id", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})*/
}

View File

@@ -0,0 +1,858 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
} from "drizzle-orm"
import {
products,
productcategories,
services,
servicecategories,
} from "../../../db/schema"
// -----------------------------------------------------------------------------
// PRODUCTS
// -----------------------------------------------------------------------------
export default async function productsAndServicesRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST: /resource/products
// -------------------------------------------------------------
server.get("/resource/products", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let baseQuery = server.db
.select()
.from(products)
.where(eq(products.tenant, tenantId))
if (search) {
baseQuery = server.db
.select()
.from(products)
.where(
and(
eq(products.tenant, tenantId),
ilike(products.name, `%${search}%`)
)
)
}
if (sort) {
const field = (products as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
const list = await baseQuery
return list
})
// -------------------------------------------------------------
// PAGINATED: /resource/products/paginated
// -------------------------------------------------------------
server.get("/resource/products/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled,
} = queryConfig
const {
select, // aktuell ignoriert, wie bei customers
search,
searchColumns,
distinctColumns,
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
let whereCond: any = eq(products.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (products as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val as any))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
if (search && search.trim().length > 0) {
const searchTerm = `%${search.trim().toLowerCase()}%`
whereCond = and(
whereCond,
ilike(products.name, searchTerm)
)
}
const totalRes = await server.db
.select({ value: count(products.id) })
.from(products)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (products as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(products)
.where(eq(products.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
let orderField: any = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (products as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
let dataQuery = server.db
.select()
.from(products)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
} catch (e) {
server.log.error(e)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// DETAIL: /resource/products/:id
// (aktuell ohne weitere Joins)
// -------------------------------------------------------------
server.get("/resource/products/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(products)
.where(
and(
eq(products.id, Number(id)),
eq(products.tenant, tenantId)
)
)
.limit(1)
if (!rows.length) {
return reply.code(404).send({ error: "Product not found" })
}
return rows[0]
})
// ---------------------------------------------------------------------------
// PRODUCTCATEGORIES
// ---------------------------------------------------------------------------
server.get("/resource/productcategories", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let baseQuery = server.db
.select()
.from(productcategories)
.where(eq(productcategories.tenant, tenantId))
if (search) {
const searchTerm = `%${search}%`
baseQuery = server.db
.select()
.from(productcategories)
.where(
and(
eq(productcategories.tenant, tenantId),
ilike(productcategories.name, searchTerm)
)
)
}
if (sort) {
const field = (productcategories as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
const list = await baseQuery
return list
})
server.get("/resource/productcategories/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled,
} = queryConfig
const {
select,
search,
searchColumns,
distinctColumns,
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
let whereCond: any = eq(productcategories.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (productcategories as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val as any))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
if (search && search.trim().length > 0) {
const searchTerm = `%${search.trim().toLowerCase()}%`
whereCond = and(
whereCond,
ilike(productcategories.name, searchTerm)
)
}
const totalRes = await server.db
.select({ value: count(productcategories.id) })
.from(productcategories)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (productcategories as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(productcategories)
.where(eq(productcategories.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
let orderField: any = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (productcategories as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
let dataQuery = server.db
.select()
.from(productcategories)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
} catch (e) {
server.log.error(e)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.get("/resource/productcategories/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(productcategories)
.where(
and(
eq(productcategories.id, Number(id)),
eq(productcategories.tenant, tenantId)
)
)
.limit(1)
if (!rows.length) {
return reply.code(404).send({ error: "Product category not found" })
}
// Später hier: products mit Join-Tabelle
return rows[0]
})
// ---------------------------------------------------------------------------
// SERVICES
// ---------------------------------------------------------------------------
server.get("/resource/services", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let baseQuery = server.db
.select()
.from(services)
.where(eq(services.tenant, tenantId))
if (search) {
const searchTerm = `%${search}%`
baseQuery = server.db
.select()
.from(services)
.where(
and(
eq(services.tenant, tenantId),
ilike(services.name, searchTerm)
)
)
}
if (sort) {
const field = (services as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
const list = await baseQuery
return list
})
server.get("/resource/services/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled,
} = queryConfig
const {
select,
search,
searchColumns,
distinctColumns,
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
let whereCond: any = eq(services.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (services as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val as any))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
if (search && search.trim().length > 0) {
const searchTerm = `%${search.trim().toLowerCase()}%`
whereCond = and(
whereCond,
ilike(services.name, searchTerm)
)
}
const totalRes = await server.db
.select({ value: count(services.id) })
.from(services)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (services as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(services)
.where(eq(services.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
let orderField: any = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (services as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
let dataQuery = server.db
.select()
.from(services)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
} catch (e) {
server.log.error(e)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.get("/resource/services/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(services)
.where(
and(
eq(services.id, Number(id)),
eq(services.tenant, tenantId)
)
)
.limit(1)
if (!rows.length) {
return reply.code(404).send({ error: "Service not found" })
}
// Später: Unit, Kategorien, etc. als Joins
return rows[0]
})
// ---------------------------------------------------------------------------
// SERVICECATEGORIES
// ---------------------------------------------------------------------------
server.get("/resource/servicecategories", async (req, reply) => {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
let baseQuery = server.db
.select()
.from(servicecategories)
.where(eq(servicecategories.tenant, tenantId))
if (search) {
const searchTerm = `%${search}%`
baseQuery = server.db
.select()
.from(servicecategories)
.where(
and(
eq(servicecategories.tenant, tenantId),
ilike(servicecategories.name, searchTerm)
)
)
}
if (sort) {
const field = (servicecategories as any)[sort]
if (field) {
// @ts-ignore
baseQuery = baseQuery.orderBy(
ascQuery === "true" ? asc(field) : desc(field)
)
}
}
const list = await baseQuery
return list
})
server.get("/resource/servicecategories/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) {
return reply.code(400).send({ error: "No tenant selected" })
}
const queryConfig = req.queryConfig
const {
pagination,
sort,
filters,
paginationDisabled,
} = queryConfig
const {
select,
search,
searchColumns,
distinctColumns,
} = req.query as {
select?: string
search?: string
searchColumns?: string
distinctColumns?: string
}
let whereCond: any = eq(servicecategories.tenant, tenantId)
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (servicecategories as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else if (val === true || val === false || val === null) {
whereCond = and(whereCond, eq(col, val as any))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
if (search && search.trim().length > 0) {
const searchTerm = `%${search.trim().toLowerCase()}%`
whereCond = and(
whereCond,
ilike(servicecategories.name, searchTerm)
)
}
const totalRes = await server.db
.select({ value: count(servicecategories.id) })
.from(servicecategories)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
const col = (servicecategories as any)[colName]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(servicecategories)
.where(eq(servicecategories.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v != null && v !== "")
distinctValues[colName] = [...new Set(values)].sort()
}
}
let offset = 0
let limit = 999999
if (!paginationDisabled && pagination) {
offset = pagination.offset
limit = pagination.limit
}
let orderField: any = null
let orderDirection: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (servicecategories as any)[s.field]
if (col) {
orderField = col
orderDirection = s.direction === "asc" ? "asc" : "desc"
}
}
let dataQuery = server.db
.select()
.from(servicecategories)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
dataQuery =
orderDirection === "asc"
? dataQuery.orderBy(asc(orderField))
: dataQuery.orderBy(desc(orderField))
}
const data = await dataQuery
const totalPages = pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null,
}
return {
data,
queryConfig: enrichedConfig,
}
} catch (e) {
server.log.error(e)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
server.get("/resource/servicecategories/:id", async (req, reply) => {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const rows = await server.db
.select()
.from(servicecategories)
.where(
and(
eq(servicecategories.id, Number(id)),
eq(servicecategories.tenant, tenantId)
)
)
.limit(1)
if (!rows.length) {
return reply.code(404).send({ error: "Service category not found" })
}
// Später: zugehörige Services über Join-Tabelle
return rows[0]
})
}

View File

@@ -1,251 +0,0 @@
import { FastifyInstance } from "fastify"
import {
eq,
ilike,
asc,
desc,
and,
count,
inArray,
or
} from "drizzle-orm"
import {
vendors,
contacts,
files,
} from "../../../db/schema"
// -------------------------------------------------------------
// SQL Volltext-Suche (über mehrere relevante Felder)
// -------------------------------------------------------------
function buildVendorSearchTerm(search?: string) {
if (!search) return null
const term = `%${search.toLowerCase()}%`
return or(
ilike(vendors.name, term),
ilike(vendors.vendorNumber, term),
ilike(vendors.notes, term),
ilike(vendors.defaultPaymentMethod, term)
)
}
export default async function vendorRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
// LIST
// -------------------------------------------------------------
server.get("/resource/vendors", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const { search, sort, asc: ascQuery } = req.query as {
search?: string
sort?: string
asc?: string
}
// WHERE
let whereCond: any = eq(vendors.tenant, tenantId)
const searchCond = buildVendorSearchTerm(search)
if (searchCond) whereCond = and(whereCond, searchCond)
// QUERY
let q = server.db
.select()
.from(vendors)
.where(whereCond)
// SORT
if (sort) {
const col = (vendors as any)[sort]
if (col) {
//@ts-ignore
q = ascQuery === "true"
? q.orderBy(asc(col))
: q.orderBy(desc(col))
}
}
return await q
} catch (err) {
console.error("ERROR /resource/vendors:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// PAGINATED
// -------------------------------------------------------------
server.get("/resource/vendors/paginated", async (req, reply) => {
try {
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const queryConfig = req.queryConfig
const { pagination, sort, filters, paginationDisabled } = queryConfig
const { search, distinctColumns } = req.query as {
search?: string
distinctColumns?: string
}
// ------------------------------------
// WHERE
// ------------------------------------
let whereCond: any = eq(vendors.tenant, tenantId)
// Filters
if (filters) {
for (const [key, val] of Object.entries(filters)) {
const col = (vendors as any)[key]
if (!col) continue
if (Array.isArray(val)) {
whereCond = and(whereCond, inArray(col, val))
} else {
whereCond = and(whereCond, eq(col, val as any))
}
}
}
// SEARCH
const searchCond = buildVendorSearchTerm(search)
if (searchCond) whereCond = and(whereCond, searchCond)
// ------------------------------------
// COUNT
// ------------------------------------
const totalRes = await server.db
.select({ value: count(vendors.id) })
.from(vendors)
.where(whereCond)
const total = Number(totalRes[0]?.value ?? 0)
// ------------------------------------
// DISTINCT VALUES
// ------------------------------------
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
for (const field of distinctColumns.split(",")) {
const col = (vendors as any)[field.trim()]
if (!col) continue
const rows = await server.db
.select({ v: col })
.from(vendors)
.where(eq(vendors.tenant, tenantId))
const values = rows
.map(r => r.v)
.filter(v => v !== null && v !== "")
distinctValues[field] = [...new Set(values)].sort()
}
}
// ------------------------------------
// PAGINATION
// ------------------------------------
const offset = pagination?.offset ?? 0
const limit = pagination?.limit ?? 5000
// ------------------------------------
// SORT
// ------------------------------------
let orderField: any = null
let orderDir: "asc" | "desc" = "asc"
if (sort?.length > 0) {
const s = sort[0]
const col = (vendors as any)[s.field]
if (col) {
orderField = col
orderDir = s.direction === "asc" ? "asc" : "desc"
}
}
// ------------------------------------
// DATA QUERY
// ------------------------------------
let q = server.db
.select()
.from(vendors)
.where(whereCond)
.offset(offset)
.limit(limit)
if (orderField) {
//@ts-ignore
q = orderDir === "asc"
? q.orderBy(asc(orderField))
: q.orderBy(desc(orderField))
}
const data = await q
return {
data,
queryConfig: {
...queryConfig,
total,
totalPages: pagination?.limit
? Math.ceil(total / pagination.limit)
: 1,
distinctValues,
search: search || null
}
}
} catch (err) {
console.error("ERROR /resource/vendors/paginated:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
// -------------------------------------------------------------
// DETAIL (mit JOINs: contacts, files)
// -------------------------------------------------------------
server.get("/resource/vendors/:id", async (req, reply) => {
try {
const { id } = req.params as { id: string }
const tenantId = req.user?.tenant_id
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
const vendorId = Number(id)
const vendorRows = await server.db
.select()
.from(vendors)
.where(and(eq(vendors.id, vendorId), eq(vendors.tenant, tenantId)))
.limit(1)
if (!vendorRows.length) return reply.code(404).send({ error: "Vendor not found" })
const vendor = vendorRows[0]
const [vendorContacts, vendorFiles] = await Promise.all([
server.db.select().from(contacts).where(eq(contacts.vendor, vendorId)),
server.db.select().from(files).where(eq(files.vendor, vendorId))
])
return {
...vendor,
contacts: vendorContacts,
files: vendorFiles
}
} catch (err) {
console.error("ERROR /resource/vendors/:id:", err)
return reply.code(500).send({ error: "Internal Server Error" })
}
})
}