Compare commits
8 Commits
d3e2b106af
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e7554fa2cc | |||
| 7c1fabf58a | |||
| 1203b6cbd1 | |||
| 525f2906fb | |||
| b105382abf | |||
| b1cdec7d17 | |||
| f1d512b2e5 | |||
| db21b43120 |
@@ -2,37 +2,18 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||||
title: '[BUG] '
|
title: '[BUG] '
|
||||||
labels: bug
|
labels: Problem
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Beschreibung**
|
**Beschreibung**
|
||||||
Eine klare und prägnante Beschreibung des Fehlers.
|
|
||||||
|
|
||||||
**Reproduktion**
|
**Reproduktion**
|
||||||
Schritte, um den Fehler zu reproduzieren:
|
|
||||||
|
|
||||||
Entweder:
|
|
||||||
1. Gehe zu '...'
|
|
||||||
2. Klicke auf '...'
|
|
||||||
3. Scrolle runter zu '...'
|
|
||||||
4. Siehe Fehler
|
|
||||||
|
|
||||||
Oder Link zur Seite
|
|
||||||
|
|
||||||
**Erwartetes Verhalten**
|
|
||||||
Eine klare Beschreibung dessen, was du erwartet hast.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
|
|
||||||
|
|
||||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||||
|
|
||||||
**Umgebung:**
|
|
||||||
- Betriebssystem: [z.B. Windows, macOS, Linux]
|
|
||||||
- Browser / Version (falls relevant): [z.B. Chrome 120]
|
|
||||||
- Projekt-Version: [z.B. v1.0.2]
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Füge hier alle anderen Informationen zum Problem hinzu.
|
|
||||||
@@ -2,19 +2,16 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
about: Schlage eine Idee für dieses Projekt vor.
|
about: Schlage eine Idee für dieses Projekt vor.
|
||||||
title: '[FEATURE] '
|
title: '[FEATURE] '
|
||||||
labels: enhancement
|
labels: Funktionswunsch
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
||||||
Eine klare Beschreibung des Problems (z.B. "Ich bin immer genervt, wenn...").
|
|
||||||
|
|
||||||
**Lösungsvorschlag**
|
**Lösungsvorschlag**
|
||||||
Eine klare Beschreibung dessen, was du dir wünschst und wie es funktionieren soll.
|
|
||||||
|
|
||||||
**Alternativen**
|
**Alternativen**
|
||||||
Hast du über alternative Lösungen oder Workarounds nachgedacht?
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Hier ist Platz für weitere Informationen, Skizzen oder Beispiele von anderen Tools.
|
|
||||||
@@ -10,23 +10,30 @@ import {
|
|||||||
or
|
or
|
||||||
} from "drizzle-orm"
|
} from "drizzle-orm"
|
||||||
|
|
||||||
import { resourceConfig } from "../../utils/resource.config";
|
|
||||||
import { useNextNumberRangeNumber } from "../../utils/functions";
|
|
||||||
|
import {resourceConfig} from "../../utils/resource.config";
|
||||||
|
import {useNextNumberRangeNumber} from "../../utils/functions";
|
||||||
|
import {stafftimeentries} from "../../../db/schema";
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// SQL Suche auf mehreren Feldern (Haupttabelle + Relationen)
|
// SQL Volltextsuche auf mehreren Feldern
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
function buildSearchCondition(columns: any[], search: string) {
|
|
||||||
|
|
||||||
|
function buildSearchCondition(table: any, columns: string[], search: string) {
|
||||||
if (!search || !columns.length) return null
|
if (!search || !columns.length) return null
|
||||||
|
|
||||||
const term = `%${search.toLowerCase()}%`
|
const term = `%${search.toLowerCase()}%`
|
||||||
|
|
||||||
const conditions = columns
|
const conditions = columns
|
||||||
|
.map((colName) => table[colName])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((col) => ilike(col, term))
|
.map((col) => ilike(col, term))
|
||||||
|
|
||||||
if (conditions.length === 0) return null
|
if (conditions.length === 0) return null
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
return or(...conditions)
|
return or(...conditions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,85 +54,96 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
asc?: string
|
asc?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resource } = req.params as { resource: string }
|
const {resource} = req.params as {resource: string}
|
||||||
const config = resourceConfig[resource]
|
const table = resourceConfig[resource].table
|
||||||
const table = config.table
|
|
||||||
|
|
||||||
|
// WHERE-Basis
|
||||||
let whereCond: any = eq(table.tenant, tenantId)
|
let whereCond: any = eq(table.tenant, tenantId)
|
||||||
let q = server.db.select().from(table).$dynamic()
|
|
||||||
|
|
||||||
const searchCols: any[] = (config.searchColumns || []).map(c => table[c])
|
// 🔍 SQL Search
|
||||||
|
if(search) {
|
||||||
|
const searchCond = buildSearchCondition(
|
||||||
|
table,
|
||||||
|
resourceConfig[resource].searchColumns,
|
||||||
|
search.trim()
|
||||||
|
)
|
||||||
|
|
||||||
if (config.mtoLoad) {
|
if (searchCond) {
|
||||||
config.mtoLoad.forEach(rel => {
|
whereCond = and(whereCond, searchCond)
|
||||||
const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel]
|
|
||||||
if (relConfig) {
|
|
||||||
const relTable = relConfig.table
|
|
||||||
|
|
||||||
// FIX: Nur joinen, wenn es keine Self-Reference ist (verhindert ERROR 42712)
|
|
||||||
if (relTable !== table) {
|
|
||||||
q = q.leftJoin(relTable, eq(table[rel], relTable.id))
|
|
||||||
if (relConfig.searchColumns) {
|
|
||||||
relConfig.searchColumns.forEach(c => {
|
|
||||||
if (relTable[c]) searchCols.push(relTable[c])
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
// Base Query
|
||||||
const searchCond = buildSearchCondition(searchCols, search.trim())
|
let q = server.db.select().from(table).where(whereCond)
|
||||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
|
||||||
}
|
|
||||||
|
|
||||||
q = q.where(whereCond)
|
|
||||||
|
|
||||||
|
// Sortierung
|
||||||
if (sort) {
|
if (sort) {
|
||||||
const col = (table as any)[sort]
|
const col = (table as any)[sort]
|
||||||
if (col) {
|
if (col) {
|
||||||
q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col))
|
//@ts-ignore
|
||||||
|
q = ascQuery === "true"
|
||||||
|
? q.orderBy(asc(col))
|
||||||
|
: q.orderBy(desc(col))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryData = await q
|
const queryData = await q
|
||||||
// Transformation: Falls Joins genutzt wurden, das Hauptobjekt extrahieren
|
|
||||||
const rows = queryData.map(r => r[resource] || r.table || r);
|
|
||||||
|
|
||||||
// RELATION LOADING
|
// RELATION LOADING (MANY-TO-ONE)
|
||||||
let data = [...rows]
|
|
||||||
if(config.mtoLoad) {
|
let ids = {}
|
||||||
let ids: any = {}
|
let lists = {}
|
||||||
let lists: any = {}
|
let maps = {}
|
||||||
let maps: any = {}
|
let data = [...queryData]
|
||||||
config.mtoLoad.forEach(rel => {
|
|
||||||
ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
|
if(resourceConfig[resource].mtoLoad) {
|
||||||
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
|
ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))];
|
||||||
})
|
})
|
||||||
for await (const rel of config.mtoLoad) {
|
|
||||||
const relConf = resourceConfig[rel + "s"] || resourceConfig[rel];
|
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||||
const relTab = relConf.table
|
console.log(relation)
|
||||||
lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : []
|
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : []
|
||||||
maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i]));
|
|
||||||
}
|
}
|
||||||
data = rows.map(row => {
|
|
||||||
let toReturn = { ...row }
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
config.mtoLoad.forEach(rel => {
|
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
|
||||||
toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
data = queryData.map(row => {
|
||||||
|
let toReturn = {
|
||||||
|
...row
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
|
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
||||||
|
})
|
||||||
|
|
||||||
return toReturn
|
return toReturn
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(config.mtmListLoad) {
|
if(resourceConfig[resource].mtmListLoad) {
|
||||||
for await (const relation of config.mtmListLoad) {
|
for await (const relation of resourceConfig[resource].mtmListLoad) {
|
||||||
const relTable = resourceConfig[relation].table
|
console.log(relation)
|
||||||
const parentKey = resource.substring(0, resource.length - 1)
|
console.log(resource.substring(0,resource.length-1))
|
||||||
const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id)))
|
|
||||||
data = data.map(row => ({
|
const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id)))
|
||||||
...row,
|
|
||||||
[relation]: relationRows.filter(i => i[parentKey] === row.id)
|
console.log(relationRows.length)
|
||||||
}))
|
|
||||||
|
data = data.map(row => {
|
||||||
|
let toReturn = {
|
||||||
|
...row
|
||||||
|
}
|
||||||
|
|
||||||
|
toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id)
|
||||||
|
|
||||||
|
return toReturn
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,129 +155,212 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// PAGINATED LIST
|
// PAGINATED LIST
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
server.get("/resource/:resource/paginated", async (req, reply) => {
|
server.get("/resource/:resource/paginated", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const tenantId = req.user?.tenant_id;
|
const tenantId = req.user?.tenant_id;
|
||||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" });
|
if (!tenantId) {
|
||||||
|
return reply.code(400).send({ error: "No tenant selected" });
|
||||||
|
}
|
||||||
|
|
||||||
const { resource } = req.params as { resource: string };
|
const {resource} = req.params as {resource: string};
|
||||||
const config = resourceConfig[resource];
|
|
||||||
const table = config.table;
|
const {queryConfig} = req;
|
||||||
|
const {
|
||||||
|
pagination,
|
||||||
|
sort,
|
||||||
|
filters,
|
||||||
|
paginationDisabled
|
||||||
|
} = queryConfig;
|
||||||
|
|
||||||
|
const { search, distinctColumns } = req.query as {
|
||||||
|
search?: string;
|
||||||
|
distinctColumns?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let table = resourceConfig[resource].table
|
||||||
|
|
||||||
const { queryConfig } = req;
|
|
||||||
const { pagination, sort, filters } = queryConfig;
|
|
||||||
const { search, distinctColumns } = req.query as { search?: string; distinctColumns?: string; };
|
|
||||||
|
|
||||||
let whereCond: any = eq(table.tenant, tenantId);
|
let whereCond: any = eq(table.tenant, tenantId);
|
||||||
const searchCols: any[] = (config.searchColumns || []).map(c => table[c]);
|
|
||||||
|
|
||||||
let countQuery = server.db.select({ value: count(table.id) }).from(table).$dynamic();
|
|
||||||
let mainQuery = server.db.select().from(table).$dynamic();
|
|
||||||
|
|
||||||
if (config.mtoLoad) {
|
if(search) {
|
||||||
config.mtoLoad.forEach(rel => {
|
const searchCond = buildSearchCondition(
|
||||||
const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel];
|
table,
|
||||||
if (relConfig) {
|
resourceConfig[resource].searchColumns,
|
||||||
const relTable = relConfig.table;
|
search.trim()
|
||||||
|
)
|
||||||
|
|
||||||
// FIX: Self-Reference Check
|
if (searchCond) {
|
||||||
if (relTable !== table) {
|
whereCond = and(whereCond, searchCond)
|
||||||
countQuery = countQuery.leftJoin(relTable, eq(table[rel], relTable.id));
|
|
||||||
mainQuery = mainQuery.leftJoin(relTable, eq(table[rel], relTable.id));
|
|
||||||
if (relConfig.searchColumns) {
|
|
||||||
relConfig.searchColumns.forEach(c => {
|
|
||||||
if (relTable[c]) searchCols.push(relTable[c]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const searchCond = buildSearchCondition(searchCols, search.trim());
|
|
||||||
if (searchCond) whereCond = and(whereCond, searchCond);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters) {
|
if (filters) {
|
||||||
for (const [key, val] of Object.entries(filters)) {
|
for (const [key, val] of Object.entries(filters)) {
|
||||||
const col = (table as any)[key];
|
const col = (table as any)[key];
|
||||||
if (!col) continue;
|
if (!col) continue;
|
||||||
whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : and(whereCond, eq(col, val as any));
|
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
whereCond = and(whereCond, inArray(col, val));
|
||||||
|
} else {
|
||||||
|
whereCond = and(whereCond, eq(col, val as any));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalRes = await countQuery.where(whereCond);
|
// -----------------------------------------------
|
||||||
|
// COUNT (for pagination)
|
||||||
|
// -----------------------------------------------
|
||||||
|
const totalRes = await server.db
|
||||||
|
.select({ value: count(table.id) })
|
||||||
|
.from(table)
|
||||||
|
.where(whereCond);
|
||||||
|
|
||||||
const total = Number(totalRes[0]?.value ?? 0);
|
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 offset = pagination?.offset ?? 0;
|
||||||
const limit = pagination?.limit ?? 100;
|
const limit = pagination?.limit ?? 100;
|
||||||
|
|
||||||
mainQuery = mainQuery.where(whereCond).offset(offset).limit(limit);
|
// SORTING
|
||||||
|
let orderField: any = null;
|
||||||
|
let direction: "asc" | "desc" = "asc";
|
||||||
|
|
||||||
if (sort?.length > 0) {
|
if (sort?.length > 0) {
|
||||||
const s = sort[0];
|
const s = sort[0];
|
||||||
const col = (table as any)[s.field];
|
const col = (table as any)[s.field];
|
||||||
if (col) {
|
if (col) {
|
||||||
mainQuery = s.direction === "asc" ? mainQuery.orderBy(asc(col)) : mainQuery.orderBy(desc(col));
|
orderField = col;
|
||||||
|
direction = s.direction === "asc" ? "asc" : "desc";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawRows = await mainQuery;
|
// MAIN QUERY (Paginated)
|
||||||
// Transformation für Drizzle Joins
|
let q = server.db
|
||||||
let rows = rawRows.map(r => r[resource] || r.table || r);
|
.select()
|
||||||
|
.from(table)
|
||||||
|
.where(whereCond)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
const distinctValues: Record<string, any[]> = {};
|
if (orderField) {
|
||||||
if (distinctColumns) {
|
//@ts-ignore
|
||||||
for (const colName of distinctColumns.split(",").map(c => c.trim())) {
|
q = direction === "asc"
|
||||||
const col = (table as any)[colName];
|
? q.orderBy(asc(orderField))
|
||||||
if (!col) continue;
|
: q.orderBy(desc(orderField));
|
||||||
const dRows = await server.db.select({ v: col }).from(table).where(eq(table.tenant, tenantId));
|
|
||||||
distinctValues[colName] = [...new Set(dRows.map(r => r.v).filter(v => v != null && v !== ""))].sort();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = [...rows];
|
const rows = await q;
|
||||||
if (config.mtoLoad) {
|
|
||||||
let ids: any = {};
|
if (!rows.length) {
|
||||||
let lists: any = {};
|
return {
|
||||||
let maps: any = {};
|
data: [],
|
||||||
config.mtoLoad.forEach(rel => {
|
queryConfig: {
|
||||||
ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
|
...queryConfig,
|
||||||
});
|
total,
|
||||||
for await (const rel of config.mtoLoad) {
|
totalPages: 0,
|
||||||
const relConf = resourceConfig[rel + "s"] || resourceConfig[rel];
|
distinctValues
|
||||||
const relTab = relConf.table;
|
|
||||||
lists[rel] = ids[rel].length ? await server.db.select().from(relTab).where(inArray(relTab.id, ids[rel])) : [];
|
|
||||||
maps[rel] = Object.fromEntries(lists[rel].map((i: any) => [i.id, i]));
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let data = [...rows]
|
||||||
|
//Many to One
|
||||||
|
if(resourceConfig[resource].mtoLoad) {
|
||||||
|
let ids = {}
|
||||||
|
let lists = {}
|
||||||
|
let maps = {}
|
||||||
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
|
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||||
|
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : []
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
|
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
|
||||||
|
})
|
||||||
|
|
||||||
data = rows.map(row => {
|
data = rows.map(row => {
|
||||||
let toReturn = { ...row };
|
let toReturn = {
|
||||||
config.mtoLoad.forEach(rel => {
|
...row
|
||||||
toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null;
|
}
|
||||||
});
|
|
||||||
return toReturn;
|
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||||
|
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
||||||
|
})
|
||||||
|
|
||||||
|
return toReturn
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.mtmListLoad) {
|
if(resourceConfig[resource].mtmListLoad) {
|
||||||
for await (const relation of config.mtmListLoad) {
|
for await (const relation of resourceConfig[resource].mtmListLoad) {
|
||||||
const relTable = resourceConfig[relation].table;
|
console.log(relation)
|
||||||
const parentKey = resource.substring(0, resource.length - 1);
|
|
||||||
const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id)));
|
const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id)))
|
||||||
data = data.map(row => ({
|
|
||||||
...row,
|
console.log(relationRows)
|
||||||
[relation]: relationRows.filter(i => i[parentKey] === row.id)
|
|
||||||
}));
|
data = data.map(row => {
|
||||||
|
let toReturn = {
|
||||||
|
...row
|
||||||
|
}
|
||||||
|
|
||||||
|
toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id)
|
||||||
|
|
||||||
|
return toReturn
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// RETURN DATA
|
||||||
|
// -----------------------------------------------
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
queryConfig: { ...queryConfig, total, totalPages: Math.ceil(total / limit), distinctValues }
|
queryConfig: {
|
||||||
|
...queryConfig,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
distinctValues
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -268,8 +369,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// DETAIL
|
// DETAIL (mit JOINS)
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => {
|
server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
@@ -277,7 +379,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
const tenantId = req.user?.tenant_id
|
const tenantId = req.user?.tenant_id
|
||||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||||
|
|
||||||
const { resource, no_relations } = req.params as { resource: string, no_relations?: boolean }
|
const {resource, no_relations} = req.params as { resource: string, no_relations?: boolean }
|
||||||
const table = resourceConfig[resource].table
|
const table = resourceConfig[resource].table
|
||||||
|
|
||||||
const projRows = await server.db
|
const projRows = await server.db
|
||||||
@@ -289,32 +391,40 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
if (!projRows.length)
|
if (!projRows.length)
|
||||||
return reply.code(404).send({ error: "Resource not found" })
|
return reply.code(404).send({ error: "Resource not found" })
|
||||||
|
|
||||||
let data = { ...projRows[0] }
|
// ------------------------------------
|
||||||
|
// LOAD RELATIONS
|
||||||
|
// ------------------------------------
|
||||||
|
|
||||||
if (!no_relations) {
|
let ids = {}
|
||||||
if (resourceConfig[resource].mtoLoad) {
|
let lists = {}
|
||||||
for await (const relation of resourceConfig[resource].mtoLoad) {
|
let maps = {}
|
||||||
if (data[relation]) {
|
let data = {
|
||||||
const relConf = resourceConfig[relation + "s"] || resourceConfig[relation];
|
...projRows[0]
|
||||||
const relTable = relConf.table
|
}
|
||||||
const relData = await server.db.select().from(relTable).where(eq(relTable.id, data[relation]))
|
|
||||||
data[relation] = relData[0] || null
|
if(!no_relations) {
|
||||||
|
if(resourceConfig[resource].mtoLoad) {
|
||||||
|
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||||
|
if(data[relation]) {
|
||||||
|
data[relation] = (await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation])))[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceConfig[resource].mtmLoad) {
|
if(resourceConfig[resource].mtmLoad) {
|
||||||
for await (const relation of resourceConfig[resource].mtmLoad) {
|
for await (const relation of resourceConfig[resource].mtmLoad ) {
|
||||||
const relTable = resourceConfig[relation].table
|
console.log(relation)
|
||||||
const parentKey = resource.substring(0, resource.length - 1)
|
data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id))
|
||||||
data[relation] = await server.db.select().from(relTable).where(eq(relTable[parentKey], id))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("ERROR /resource/:resource/:id", err)
|
console.error("ERROR /resource/projects/:id", err)
|
||||||
return reply.code(500).send({ error: "Internal Server Error" })
|
return reply.code(500).send({ error: "Internal Server Error" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -322,58 +432,132 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
// Create
|
// Create
|
||||||
server.post("/resource/:resource", async (req, reply) => {
|
server.post("/resource/:resource", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" });
|
if (!req.user?.tenant_id) {
|
||||||
const { resource } = req.params as { resource: string };
|
return reply.code(400).send({error: "No tenant selected"});
|
||||||
const body = req.body as Record<string, any>;
|
}
|
||||||
const config = resourceConfig[resource];
|
|
||||||
const table = config.table;
|
const {resource} = req.params as { resource: string };
|
||||||
|
const body = req.body as Record<string, any>;
|
||||||
let createData = { ...body, tenant: req.user.tenant_id, archived: false };
|
|
||||||
|
const table = resourceConfig[resource].table
|
||||||
if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
|
|
||||||
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
|
let createData = {
|
||||||
createData[config.numberRangeHolder] = result.usedNumber
|
...body,
|
||||||
|
tenant: req.user.tenant_id,
|
||||||
|
archived: false, // Standardwert
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(resourceConfig[resource].numberRangeHolder)
|
||||||
|
|
||||||
|
if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) {
|
||||||
|
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
|
||||||
|
console.log(result)
|
||||||
|
createData[resourceConfig[resource].numberRangeHolder] = result.usedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }
|
|
||||||
Object.keys(createData).forEach((key) => {
|
Object.keys(createData).forEach((key) => {
|
||||||
if (key.toLowerCase().includes("date") && key !== "deliveryDateType") createData[key] = normalizeDate(createData[key])
|
if(key.toLowerCase().includes("date") && key !== "deliveryDateType") createData[key] = normalizeDate(createData[key])
|
||||||
})
|
})
|
||||||
|
|
||||||
const [created] = await server.db.insert(table).values(createData).returning()
|
const [created] = await server.db
|
||||||
|
.insert(table)
|
||||||
|
.values(createData)
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
|
||||||
|
/*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 created;
|
return created;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.log(error)
|
||||||
reply.status(500);
|
reply.status(500)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update
|
// UPDATE (inkl. Soft-Delete/Archive)
|
||||||
server.put("/resource/:resource/:id", async (req, reply) => {
|
server.put("/resource/:resource/:id", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
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?.tenant_id
|
|
||||||
const userId = req.user?.user_id
|
|
||||||
|
|
||||||
if (!tenantId || !userId) return reply.code(401).send({ error: "Unauthorized" })
|
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"})
|
||||||
|
}
|
||||||
|
|
||||||
const table = resourceConfig[resource].table
|
const table = resourceConfig[resource].table
|
||||||
const normalizeDate = (val: any) => { const d = new Date(val); return isNaN(d.getTime()) ? null : d; }
|
|
||||||
|
|
||||||
let data = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
|
//TODO: HISTORY
|
||||||
delete data.updatedBy; delete data.updatedAt;
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {...body, updated_at: new Date().toISOString(), updated_by: userId}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
delete data.updatedBy
|
||||||
|
//@ts-ignore
|
||||||
|
delete data.updatedAt
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") {
|
console.log(key)
|
||||||
|
|
||||||
|
if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) {
|
||||||
data[key] = normalizeDate(data[key])
|
data[key] = normalizeDate(data[key])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const [updated] = await server.db.update(table).set(data).where(and(eq(table.id, id), eq(table.tenant, tenantId))).returning()
|
console.log(data)
|
||||||
|
|
||||||
|
const [updated] = await server.db
|
||||||
|
.update(table)
|
||||||
|
.set(data)
|
||||||
|
.where(and(
|
||||||
|
eq(table.id, id),
|
||||||
|
eq(table.tenant, tenantId)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
//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 updated
|
return updated
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.log("ERROR /resource/projects/:id", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
|
|
||||||
export const resourceConfig = {
|
export const resourceConfig = {
|
||||||
projects: {
|
projects: {
|
||||||
searchColumns: ["name","customerRef","projectNumber","notes"],
|
searchColumns: ["name"],
|
||||||
mtoLoad: ["customer","plant","contract","projecttype"],
|
mtoLoad: ["customer","plant","contract","projecttype"],
|
||||||
mtmLoad: ["tasks", "files","createddocuments"],
|
mtmLoad: ["tasks", "files","createddocuments"],
|
||||||
table: projects,
|
table: projects,
|
||||||
@@ -61,7 +61,6 @@ export const resourceConfig = {
|
|||||||
},
|
},
|
||||||
plants: {
|
plants: {
|
||||||
table: plants,
|
table: plants,
|
||||||
searchColumns: ["name"],
|
|
||||||
mtoLoad: ["customer"],
|
mtoLoad: ["customer"],
|
||||||
mtmLoad: ["projects","tasks","files"],
|
mtmLoad: ["projects","tasks","files"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1387,7 +1387,7 @@ const saveDocument = async (state, resetup = false) => {
|
|||||||
endText: itemInfo.value.endText,
|
endText: itemInfo.value.endText,
|
||||||
rows: itemInfo.value.rows,
|
rows: itemInfo.value.rows,
|
||||||
contactPerson: itemInfo.value.contactPerson,
|
contactPerson: itemInfo.value.contactPerson,
|
||||||
createddocument: itemInfo.value.createddocument,
|
linkedDocument: itemInfo.value.linkedDocument,
|
||||||
agriculture: itemInfo.value.agriculture,
|
agriculture: itemInfo.value.agriculture,
|
||||||
letterhead: itemInfo.value.letterhead,
|
letterhead: itemInfo.value.letterhead,
|
||||||
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices,
|
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices,
|
||||||
|
|||||||
@@ -17,18 +17,11 @@ const dataStore = useDataStore()
|
|||||||
|
|
||||||
const itemInfo = ref({})
|
const itemInfo = ref({})
|
||||||
const linkedDocument =ref({})
|
const linkedDocument =ref({})
|
||||||
const links = ref([])
|
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
if(route.params) {
|
if(route.params) {
|
||||||
if(route.params.id) itemInfo.value = await useEntities("createddocuments").selectSingle(route.params.id,"*,files(*),linkedDocument(*), statementallocations(bs_id)")
|
if(route.params.id) itemInfo.value = await useEntities("createddocuments").selectSingle(route.params.id,"*,files(*),linkedDocument(*), statementallocations(bs_id)")
|
||||||
|
|
||||||
if(itemInfo.value.type === "invoices"){
|
console.log(itemInfo.value)
|
||||||
const createddocuments = await useEntities("createddocuments").select()
|
|
||||||
console.log(createddocuments)
|
|
||||||
links.value = createddocuments.filter(i => i.createddocument?.id === itemInfo.value.id)
|
|
||||||
console.log(links.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
|
linkedDocument.value = await useFiles().selectDocument(itemInfo.value.files[0].id)
|
||||||
}
|
}
|
||||||
@@ -87,23 +80,17 @@ const openBankstatements = () => {
|
|||||||
>
|
>
|
||||||
E-Mail
|
E-Mail
|
||||||
</UButton>
|
</UButton>
|
||||||
<UTooltip
|
|
||||||
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"
|
|
||||||
:text="links.find(i => i.type === 'cancellationInvoices') ? 'Bereits stoniert' : ''"
|
|
||||||
>
|
|
||||||
<UButton
|
<UButton
|
||||||
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
|
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
color="rose"
|
color="rose"
|
||||||
:disabled="links.find(i => i.type === 'cancellationInvoices')"
|
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"
|
||||||
>
|
>
|
||||||
Stornieren
|
Stornieren
|
||||||
</UButton>
|
</UButton>
|
||||||
</UTooltip>
|
|
||||||
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="itemInfo.project"
|
v-if="itemInfo.project"
|
||||||
@click="router.push(`/standardEntity/projects/show/${itemInfo.project?.id}`)"
|
@click="router.push(`/standardEntity/projects/show/${itemInfo.project}`)"
|
||||||
icon="i-heroicons-link"
|
icon="i-heroicons-link"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
@@ -111,36 +98,12 @@ const openBankstatements = () => {
|
|||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
v-if="itemInfo.customer"
|
v-if="itemInfo.customer"
|
||||||
@click="router.push(`/standardEntity/customers/show/${itemInfo.customer?.id}`)"
|
@click="router.push(`/standardEntity/customers/show/${itemInfo.customer}`)"
|
||||||
icon="i-heroicons-link"
|
icon="i-heroicons-link"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Kunde
|
Kunde
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
|
||||||
v-if="itemInfo.plant"
|
|
||||||
@click="router.push(`/standardEntity/plants/show/${itemInfo.plant?.id}`)"
|
|
||||||
icon="i-heroicons-link"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Objekt
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
v-if="itemInfo.contract"
|
|
||||||
@click="router.push(`/standardEntity/contracts/show/${itemInfo.contract?.id}`)"
|
|
||||||
icon="i-heroicons-link"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Vertrag
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
v-if="itemInfo.contact"
|
|
||||||
@click="router.push(`/standardEntity/contacts/show/${itemInfo.contact?.id}`)"
|
|
||||||
icon="i-heroicons-link"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
Ansprechpartner
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="itemInfo.createddocument"
|
v-if="itemInfo.createddocument"
|
||||||
@click="router.push(`/createDocument/show/${itemInfo.createddocument}`)"
|
@click="router.push(`/createDocument/show/${itemInfo.createddocument}`)"
|
||||||
|
|||||||
Reference in New Issue
Block a user