Added Paginated Endpoint

Reformatted Code
This commit is contained in:
2025-10-27 17:39:25 +01:00
parent d4fe665462
commit 5d3cdeb960

View File

@@ -1,5 +1,5 @@
import { FastifyInstance } from "fastify"; import {FastifyInstance} from "fastify";
import {insertHistoryItem } from "../utils/history" import {insertHistoryItem} from "../utils/history"
import {diffObjects} from "../utils/diff"; import {diffObjects} from "../utils/diff";
import {sortData} from "../utils/sort"; import {sortData} from "../utils/sort";
import {useNextNumberRangeNumber} from "../utils/functions"; import {useNextNumberRangeNumber} from "../utils/functions";
@@ -26,7 +26,7 @@ const dataTypes: any[] = {
label: "Kunden", label: "Kunden",
labelSingle: "Kunde", labelSingle: "Kunde",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
numberRangeHolder: "customerNumber", numberRangeHolder: "customerNumber",
historyItemHolder: "customer", historyItemHolder: "customer",
supabaseSortColumn: "customerNumber", supabaseSortColumn: "customerNumber",
@@ -35,17 +35,17 @@ const dataTypes: any[] = {
"Allgemeines", "Allgemeines",
"Kontaktdaten" "Kontaktdaten"
], ],
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'}] showTabs: [{label: 'Informationen'}, {label: 'Ansprechpartner'}, {label: 'Dateien'}, {label: 'Ausgangsbelege'}, {label: 'Projekte'}, {label: 'Objekte'}, {label: 'Termine'}, {label: 'Verträge'}]
}, },
contacts: { contacts: {
isArchivable: true, isArchivable: true,
label: "Kontakte", label: "Kontakte",
labelSingle: "Kontakt", labelSingle: "Kontakt",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
historyItemHolder: "contact", historyItemHolder: "contact",
supabaseSelectWithInformation: "*, customer(*), vendor(*)", supabaseSelectWithInformation: "*, customer(*), vendor(*)",
showTabs:[ showTabs: [
{ {
label: 'Informationen', label: 'Informationen',
} }
@@ -57,24 +57,24 @@ const dataTypes: any[] = {
labelSingle: "Vertrag", labelSingle: "Vertrag",
isStandardEntity: true, isStandardEntity: true,
numberRangeHolder: "contractNumber", numberRangeHolder: "contractNumber",
redirect:true, redirect: true,
inputColumns: [ inputColumns: [
"Allgemeines", "Allgemeines",
"Abrechnung" "Abrechnung"
], ],
supabaseSelectWithInformation: "*, customer(*), files(*)", supabaseSelectWithInformation: "*, customer(*), files(*)",
showTabs: [{label: 'Informationen'},{label: 'Dateien'}] showTabs: [{label: 'Informationen'}, {label: 'Dateien'}]
}, },
absencerequests: { absencerequests: {
isArchivable: true, isArchivable: true,
label: "Abwesenheiten", label: "Abwesenheiten",
labelSingle: "Abwesenheit", labelSingle: "Abwesenheit",
isStandardEntity: true, isStandardEntity: true,
supabaseSortColumn:"startDate", supabaseSortColumn: "startDate",
supabaseSortAscending: false, supabaseSortAscending: false,
supabaseSelectWithInformation: "*", supabaseSelectWithInformation: "*",
historyItemHolder: "absencerequest", historyItemHolder: "absencerequest",
redirect:true, redirect: true,
showTabs: [{label: 'Informationen'}] showTabs: [{label: 'Informationen'}]
}, },
plants: { plants: {
@@ -82,17 +82,17 @@ const dataTypes: any[] = {
label: "Objekte", label: "Objekte",
labelSingle: "Objekt", labelSingle: "Objekt",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
historyItemHolder: "plant", historyItemHolder: "plant",
supabaseSelectWithInformation: "*, customer(id,name)", supabaseSelectWithInformation: "*, customer(id,name)",
showTabs: [ showTabs: [
{ {
label: "Informationen" label: "Informationen"
},{ }, {
label: "Projekte" label: "Projekte"
},{ }, {
label: "Aufgaben" label: "Aufgaben"
},{ }, {
label: "Dateien" label: "Dateien"
}] }]
}, },
@@ -101,7 +101,7 @@ const dataTypes: any[] = {
label: "Artikel", label: "Artikel",
labelSingle: "Artikel", labelSingle: "Artikel",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
supabaseSelectWithInformation: "*, unit(name)", supabaseSelectWithInformation: "*, unit(name)",
historyItemHolder: "product", historyItemHolder: "product",
showTabs: [ showTabs: [
@@ -115,7 +115,7 @@ const dataTypes: any[] = {
label: "Projekte", label: "Projekte",
labelSingle: "Projekt", labelSingle: "Projekt",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
historyItemHolder: "project", historyItemHolder: "project",
numberRangeHolder: "projectNumber", 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))", 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))",
@@ -128,17 +128,17 @@ const dataTypes: any[] = {
{ {
key: "phases", key: "phases",
label: "Phasen" label: "Phasen"
},{ }, {
key: "tasks", key: "tasks",
label: "Aufgaben" label: "Aufgaben"
},{ }, {
key: "files", key: "files",
label: "Dateien" label: "Dateien"
},{ }, {
label: "Zeiten" label: "Zeiten"
},{ }, {
label: "Ausgangsbelege" label: "Ausgangsbelege"
},{ }, {
label: "Termine" label: "Termine"
}/*,{ }/*,{
key: "timetracking", key: "timetracking",
@@ -156,7 +156,7 @@ const dataTypes: any[] = {
label: "Fahrzeuge", label: "Fahrzeuge",
labelSingle: "Fahrzeug", labelSingle: "Fahrzeug",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
historyItemHolder: "vehicle", historyItemHolder: "vehicle",
supabaseSelectWithInformation: "*, checks(*), files(*)", supabaseSelectWithInformation: "*, checks(*), files(*)",
showTabs: [ showTabs: [
@@ -174,7 +174,7 @@ const dataTypes: any[] = {
label: "Lieferanten", label: "Lieferanten",
labelSingle: "Lieferant", labelSingle: "Lieferant",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
numberRangeHolder: "vendorNumber", numberRangeHolder: "vendorNumber",
historyItemHolder: "vendor", historyItemHolder: "vendor",
supabaseSortColumn: "vendorNumber", supabaseSortColumn: "vendorNumber",
@@ -182,7 +182,7 @@ const dataTypes: any[] = {
showTabs: [ showTabs: [
{ {
label: 'Informationen', label: 'Informationen',
},{ }, {
label: 'Ansprechpartner', label: 'Ansprechpartner',
}, { }, {
label: 'Dateien', label: 'Dateien',
@@ -212,7 +212,7 @@ const dataTypes: any[] = {
label: 'Informationen', label: 'Informationen',
}, { }, {
label: 'Dateien', label: 'Dateien',
},{label: 'Inventarartikel'} }, {label: 'Inventarartikel'}
] ]
}, },
users: { users: {
@@ -240,7 +240,7 @@ const dataTypes: any[] = {
incominginvoices: { incominginvoices: {
label: "Eingangsrechnungen", label: "Eingangsrechnungen",
labelSingle: "Eingangsrechnung", labelSingle: "Eingangsrechnung",
redirect:true redirect: true
}, },
inventoryitems: { inventoryitems: {
isArchivable: true, isArchivable: true,
@@ -335,7 +335,8 @@ const dataTypes: any[] = {
redirect: true, redirect: true,
showTabs: [ showTabs: [
{ {
label: 'Informationen',} label: 'Informationen',
}
] ]
}, },
profiles: { profiles: {
@@ -421,7 +422,7 @@ const dataTypes: any[] = {
roles: { roles: {
label: "Rollen", label: "Rollen",
labelSingle: "Rolle", labelSingle: "Rolle",
redirect:true, redirect: true,
historyItemHolder: "role", historyItemHolder: "role",
filters: [], filters: [],
templateColumns: [ templateColumns: [
@@ -439,23 +440,23 @@ const dataTypes: any[] = {
label: "Kostenstellen", label: "Kostenstellen",
labelSingle: "Kostenstelle", labelSingle: "Kostenstelle",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
numberRangeHolder: "number", numberRangeHolder: "number",
historyItemHolder: "costcentre", historyItemHolder: "costcentre",
supabaseSortColumn: "number", supabaseSortColumn: "number",
supabaseSelectWithInformation: "*, project(*), vehicle(*), inventoryitem(*)", supabaseSelectWithInformation: "*, project(*), vehicle(*), inventoryitem(*)",
showTabs: [{label: 'Informationen'},{label: 'Auswertung Kostenstelle'}] showTabs: [{label: 'Informationen'}, {label: 'Auswertung Kostenstelle'}]
}, },
ownaccounts: { ownaccounts: {
isArchivable: true, isArchivable: true,
label: "zusätzliche Buchungskonten", label: "zusätzliche Buchungskonten",
labelSingle: "zusätzliches Buchungskonto", labelSingle: "zusätzliches Buchungskonto",
isStandardEntity: true, isStandardEntity: true,
redirect:true, redirect: true,
historyItemHolder: "ownaccount", historyItemHolder: "ownaccount",
supabaseSortColumn: "number", supabaseSortColumn: "number",
supabaseSelectWithInformation: "*, statementallocations(*, bs_id(*))", supabaseSelectWithInformation: "*, statementallocations(*, bs_id(*))",
showTabs: [{label: 'Informationen'},{label: 'Buchungen'}] showTabs: [{label: 'Informationen'}, {label: 'Buchungen'}]
}, },
tickets: { tickets: {
isArchivable: true, isArchivable: true,
@@ -472,7 +473,8 @@ const dataTypes: any[] = {
} }
export default async function resourceRoutes(server: FastifyInstance) { export default async function resourceRoutes(server: FastifyInstance) {
// Liste
//Liste
server.get("/resource/:resource", async (req, reply) => { server.get("/resource/:resource", async (req, reply) => {
if (!req.user?.tenant_id) { if (!req.user?.tenant_id) {
return reply.code(400).send({ error: "No tenant selected" }); return reply.code(400).send({ error: "No tenant selected" });
@@ -499,24 +501,179 @@ export default async function resourceRoutes(server: FastifyInstance) {
return sorted; return sorted;
}); });
// 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(queryConfig)
// --- Supabase-Basisabfrage ---
let baseQuery = server.supabase
.from(resource)
.select(select ? select : "*", {count: "exact"}) // 👈 Zählt Ergebnisse direkt mit
.eq("tenant", req.user.tenant_id)
// --- 🔍 Serverseitige Suche über angegebene Spalten ---
if (search && search.trim().length > 0) {
const cols = searchColumns
? searchColumns.split(',').map(c => c.trim()).filter(Boolean)
: []
if (cols.length > 0) {
const searchValue = `%${search.trim()}%`
// JSONB-Unterfelder umwandeln in "->>" Syntax
const formattedCols = cols.map(c => {
if (c.includes('.')) {
const [jsonField, jsonKey] = c.split('.')
return `${jsonField}->>${jsonKey}`
}
return c
})
// or() Query dynamisch zusammenbauen
const orConditions = formattedCols
.map(f => `${f}.ilike.${searchValue}`)
.join(',')
baseQuery = baseQuery.or(orConditions)
}
}
// --- Filterung (intelligente Typ-Erkennung) ---
for (const [key, val] of Object.entries(queryConfig.filters)) {
if (Array.isArray(val)) {
baseQuery = baseQuery.in(key, val)
//@ts-ignore
} else 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)
}
// --- Abfrage ausführen ---
const {data, error, count} = await baseQuery
if (error) {
server.log.error(error)
return reply.code(400).send({error: error.message})
}
// --- Distinct-Werte ermitteln ---
const distinctValues: Record<string, any[]> = {}
if (distinctColumns) {
const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean)
for (const col of cols) {
const isJson = col.includes(".")
const {data: allRows, error: distinctErr} = await server.supabase
.from(resource)
.select(isJson ? "*" : col)
.eq("tenant", req.user.tenant_id)
if (distinctErr) continue
const values = (allRows || [])
.map(row => {
if (isJson) {
const [jsonField, jsonKey] = col.split(".")
return row?.[jsonField]?.[jsonKey] ?? null
}
return row?.[col] ?? null
})
.filter(v => v !== null && v !== undefined && v !== "")
distinctValues[col] = [...new Set(values)].sort()
}
}
// --- Gesamtanzahl & Seitenberechnung ---
const total = count
const totalPages =
!paginationDisabled && pagination?.limit
? Math.ceil(total / pagination.limit)
: 1
// --- queryConfig erweitern ---
const enrichedConfig = {
...queryConfig,
total,
totalPages,
distinctValues,
search: search || null
}
return {
data: data,
queryConfig: enrichedConfig
}
/*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;*/
});
// Detail // Detail
server.get("/resource/:resource/:id/:with_information?", async (req, reply) => { server.get("/resource/:resource/:id/:with_information?", async (req, reply) => {
if (!req.user?.tenant_id) { if (!req.user?.tenant_id) {
return reply.code(400).send({ error: "No tenant selected" }); 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 {resource, id, with_information} = req.params as {
const {select } = req.query as { select?: string } resource: string;
id: string,
with_information: boolean
};
const {select} = req.query as { select?: string }
// @ts-ignore // @ts-ignore
const { data, error } = await server.supabase.from(resource).select(with_information ? dataTypes[resource].supabaseSelectWithInformation : (select ? select : "*")) const {
data,
error
} = await server.supabase.from(resource).select(with_information ? dataTypes[resource].supabaseSelectWithInformation : (select ? select : "*"))
.eq("id", id) .eq("id", id)
.eq("tenant", req.user.tenant_id) .eq("tenant", req.user.tenant_id)
.single(); .single();
if (error || !data) { if (error || !data) {
return reply.code(404).send({ error: "Not found" }); return reply.code(404).send({error: "Not found"});
} }
return data; return data;
@@ -525,10 +682,10 @@ export default async function resourceRoutes(server: FastifyInstance) {
// Create // Create
server.post("/resource/:resource", async (req, reply) => { server.post("/resource/:resource", async (req, reply) => {
if (!req.user?.tenant_id) { if (!req.user?.tenant_id) {
return reply.code(400).send({ error: "No tenant selected" }); return reply.code(400).send({error: "No tenant selected"});
} }
const { resource } = req.params as { resource: string }; const {resource} = req.params as { resource: string };
const body = req.body as Record<string, any>; const body = req.body as Record<string, any>;
const dataType = dataTypes[resource]; const dataType = dataTypes[resource];
@@ -538,22 +695,22 @@ export default async function resourceRoutes(server: FastifyInstance) {
archived: false, // Standardwert archived: false, // Standardwert
} }
if(dataType.numberRangeHolder && !body[dataType.numberRangeHolder]) { if (dataType.numberRangeHolder && !body[dataType.numberRangeHolder]) {
const result = await useNextNumberRangeNumber(server,req.user.tenant_id, resource) const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
createData[dataType.numberRangeHolder] = result.usedNumber createData[dataType.numberRangeHolder] = result.usedNumber
} }
const { data, error } = await server.supabase const {data, error} = await server.supabase
.from(resource) .from(resource)
.insert(createData) .insert(createData)
.select("*") .select("*")
.single(); .single();
if (error) { if (error) {
return reply.code(400).send({ error: error.message }); return reply.code(400).send({error: error.message});
} }
await insertHistoryItem(server,{ await insertHistoryItem(server, {
entity: resource, entity: resource,
entityId: data.id, entityId: data.id,
action: "created", action: "created",
@@ -570,40 +727,39 @@ export default async function resourceRoutes(server: FastifyInstance) {
// UPDATE (inkl. Soft-Delete/Archive) // UPDATE (inkl. Soft-Delete/Archive)
server.put("/resource/:resource/:id", async (req, reply) => { server.put("/resource/:resource/:id", async (req, reply) => {
console.log("hi") console.log("hi")
const { resource, id } = req.params as { resource: string; id: string } const {resource, id} = req.params as { resource: string; id: string }
const body = req.body as Record<string, any> const body = req.body as Record<string, any>
const tenantId = (req.user as any)?.tenant_id const tenantId = (req.user as any)?.tenant_id
const userId = (req.user as any)?.user_id const userId = (req.user as any)?.user_id
if (!tenantId || !userId) { if (!tenantId || !userId) {
return reply.code(401).send({ error: "Unauthorized" }) return reply.code(401).send({error: "Unauthorized"})
} }
// vorherige Version für History laden // vorherige Version für History laden
const { data: oldItem } = await server.supabase const {data: oldItem} = await server.supabase
.from(resource) .from(resource)
.select("*") .select("*")
.eq("id", id) .eq("id", id)
.eq("tenant", tenantId) .eq("tenant", tenantId)
.single() .single()
const { data:newItem, error } = await server.supabase const {data: newItem, error} = await server.supabase
.from(resource) .from(resource)
.update({ ...body, updated_at: new Date().toISOString(), updated_by: userId }) .update({...body, updated_at: new Date().toISOString(), updated_by: userId})
.eq("id", id) .eq("id", id)
.eq("tenant", tenantId) .eq("tenant", tenantId)
.select() .select()
.single() .single()
if (error) return reply.code(500).send({ error }) if (error) return reply.code(500).send({error})
const diffs = diffObjects(oldItem, newItem); const diffs = diffObjects(oldItem, newItem);
for (const d of diffs) { for (const d of diffs) {
await insertHistoryItem(server,{ await insertHistoryItem(server, {
entity: resource, entity: resource,
entityId: id, entityId: id,
action: d.type, action: d.type,