removed Routes, Introduced Single Route with Cecking
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
10
src/index.ts
10
src/index.ts
@@ -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"})
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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]
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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: SQL‐LIKE 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,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
413
src/routes/resources/main.ts
Normal file
413
src/routes/resources/main.ts
Normal 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" })
|
||||
}
|
||||
})*/
|
||||
}
|
||||
858
src/routes/resources/productsServices.ts
Normal file
858
src/routes/resources/productsServices.ts
Normal 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]
|
||||
})
|
||||
}
|
||||
@@ -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" })
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user