Compare commits

..

6 Commits

Author SHA1 Message Date
e7fb2df5c7 Added Debouncing #36
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 1m8s
2026-01-15 18:19:05 +01:00
f27fd3f6da Fix TS
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 29s
Build and Push Docker Images / build-frontend (push) Successful in 15s
2026-01-15 18:07:58 +01:00
d3e2b106af Storno Fix createddocument link. Added Disable and Tooltip for Storno Button
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 29s
Build and Push Docker Images / build-frontend (push) Successful in 1m8s
2026-01-15 18:05:44 +01:00
769d2059ca Redone Search to inluce more Columns #36
TODO: Spalten nachpflegen
2026-01-15 18:05:14 +01:00
53349fae83 Fix Link Buttons Added New link buttons
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Successful in 1m7s
2026-01-15 13:38:01 +01:00
d8eb1559c8 Update Problem bei #54
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 31s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-15 13:18:58 +01:00
7 changed files with 297 additions and 460 deletions

View File

@@ -2,18 +2,37 @@
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: Problem labels: bug
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.

View File

@@ -2,16 +2,19 @@
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: Funktionswunsch labels: enhancement
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.

View File

@@ -10,30 +10,23 @@ import {
or or
} from "drizzle-orm" } from "drizzle-orm"
import { resourceConfig } from "../../utils/resource.config"; import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions"; import { useNextNumberRangeNumber } from "../../utils/functions";
import {stafftimeentries} from "../../../db/schema";
// ------------------------------------------------------------- // -------------------------------------------------------------
// SQL Volltextsuche auf mehreren Feldern // SQL Suche auf mehreren Feldern (Haupttabelle + Relationen)
// ------------------------------------------------------------- // -------------------------------------------------------------
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)
} }
@@ -55,95 +48,85 @@ export default async function resourceRoutes(server: FastifyInstance) {
} }
const { resource } = req.params as { resource: string } const { resource } = req.params as { resource: string }
const table = resourceConfig[resource].table const config = resourceConfig[resource]
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])
if (config.mtoLoad) {
config.mtoLoad.forEach(rel => {
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) {
// @ts-ignore
q = q.leftJoin(relTable, eq(table[rel], relTable.id))
if (relConfig.searchColumns) {
relConfig.searchColumns.forEach(c => {
if (relTable[c]) searchCols.push(relTable[c])
})
}
}
}
})
}
// 🔍 SQL Search
if (search) { if (search) {
const searchCond = buildSearchCondition( const searchCond = buildSearchCondition(searchCols, search.trim())
table, if (searchCond) whereCond = and(whereCond, searchCond)
resourceConfig[resource].searchColumns,
search.trim()
)
if (searchCond) {
whereCond = and(whereCond, searchCond)
}
} }
// Base Query q = q.where(whereCond)
let q = server.db.select().from(table).where(whereCond)
// Sortierung
if (sort) { if (sort) {
const col = (table as any)[sort] const col = (table as any)[sort]
if (col) { if (col) {
//@ts-ignore q = ascQuery === "true" ? q.orderBy(asc(col)) : q.orderBy(desc(col))
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 (MANY-TO-ONE) // RELATION LOADING
let data = [...rows]
let ids = {} if(config.mtoLoad) {
let lists = {} let ids: any = {}
let maps = {} let lists: any = {}
let data = [...queryData] let maps: any = {}
config.mtoLoad.forEach(rel => {
if(resourceConfig[resource].mtoLoad) { ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
resourceConfig[resource].mtoLoad.forEach(relation => {
ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))];
}) })
for await (const rel of config.mtoLoad) {
for await (const relation of resourceConfig[resource].mtoLoad ) { const relConf = resourceConfig[rel + "s"] || resourceConfig[rel];
console.log(relation) const relTab = relConf.table
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] 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]));
} }
data = rows.map(row => {
resourceConfig[resource].mtoLoad.forEach(relation => { let toReturn = { ...row }
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); config.mtoLoad.forEach(rel => {
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(resourceConfig[resource].mtmListLoad) { if(config.mtmListLoad) {
for await (const relation of resourceConfig[resource].mtmListLoad) { for await (const relation of config.mtmListLoad) {
console.log(relation) const relTable = resourceConfig[relation].table
console.log(resource.substring(0,resource.length-1)) 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.length) [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
})
} }
} }
@@ -155,212 +138,130 @@ 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) { if (!tenantId) 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 config = resourceConfig[resource];
const table = config.table;
const { queryConfig } = req; const { queryConfig } = req;
const { const { pagination, sort, filters } = queryConfig;
pagination, const { search, distinctColumns } = req.query as { search?: string; distinctColumns?: string; };
sort,
filters,
paginationDisabled
} = queryConfig;
const { search, distinctColumns } = req.query as {
search?: string;
distinctColumns?: string;
};
let table = resourceConfig[resource].table
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) {
config.mtoLoad.forEach(rel => {
const relConfig = resourceConfig[rel + "s"] || resourceConfig[rel];
if (relConfig) {
const relTable = relConfig.table;
// FIX: Self-Reference Check
if (relTable !== table) {
countQuery = countQuery.leftJoin(relTable, eq(table[rel], relTable.id));
// @ts-ignore
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) { if (search) {
const searchCond = buildSearchCondition( const searchCond = buildSearchCondition(searchCols, search.trim());
table, if (searchCond) whereCond = and(whereCond, searchCond);
resourceConfig[resource].searchColumns,
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;
// SORTING mainQuery = mainQuery.where(whereCond).offset(offset).limit(limit);
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) {
orderField = col; mainQuery = s.direction === "asc" ? mainQuery.orderBy(asc(col)) : mainQuery.orderBy(desc(col));
direction = s.direction === "asc" ? "asc" : "desc";
} }
} }
// MAIN QUERY (Paginated) const rawRows = await mainQuery;
let q = server.db // Transformation für Drizzle Joins
.select() let rows = rawRows.map(r => r[resource] || r.table || r);
.from(table)
.where(whereCond)
.offset(offset)
.limit(limit);
if (orderField) { const distinctValues: Record<string, any[]> = {};
//@ts-ignore if (distinctColumns) {
q = direction === "asc" for (const colName of distinctColumns.split(",").map(c => c.trim())) {
? q.orderBy(asc(orderField)) const col = (table as any)[colName];
: q.orderBy(desc(orderField)); if (!col) continue;
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();
}
} }
const rows = await q; let data = [...rows];
if (config.mtoLoad) {
if (!rows.length) { let ids: any = {};
return { let lists: any = {};
data: [], let maps: any = {};
queryConfig: { config.mtoLoad.forEach(rel => {
...queryConfig, ids[rel] = [...new Set(rows.map(r => r[rel]).filter(Boolean))];
total, });
totalPages: 0, for await (const rel of config.mtoLoad) {
distinctValues const relConf = resourceConfig[rel + "s"] || resourceConfig[rel];
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 = { let toReturn = { ...row };
...row config.mtoLoad.forEach(rel => {
} toReturn[rel] = row[rel] ? maps[rel][row[rel]] : null;
});
resourceConfig[resource].mtoLoad.forEach(relation => { return toReturn;
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
})
return toReturn
}); });
} }
if(resourceConfig[resource].mtmListLoad) { if (config.mtmListLoad) {
for await (const relation of resourceConfig[resource].mtmListLoad) { for await (const relation of config.mtmListLoad) {
console.log(relation) const relTable = resourceConfig[relation].table;
const parentKey = resource.substring(0, resource.length - 1);
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))) const relationRows = await server.db.select().from(relTable).where(inArray(relTable[parentKey], data.map(i => i.id)));
data = data.map(row => ({
console.log(relationRows) ...row,
[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: { ...queryConfig, total, totalPages: Math.ceil(total / limit), distinctValues }
...queryConfig,
total,
totalPages: Math.ceil(total / limit),
distinctValues
}
}; };
} catch (err) { } catch (err) {
@@ -369,9 +270,8 @@ export default async function resourceRoutes(server: FastifyInstance) {
} }
}); });
// ------------------------------------------------------------- // -------------------------------------------------------------
// DETAIL (mit JOINS) // DETAIL
// ------------------------------------------------------------- // -------------------------------------------------------------
server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => { server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => {
try { try {
@@ -391,40 +291,32 @@ 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
// ------------------------------------
let ids = {}
let lists = {}
let maps = {}
let data = {
...projRows[0]
}
if (!no_relations) { if (!no_relations) {
if (resourceConfig[resource].mtoLoad) { if (resourceConfig[resource].mtoLoad) {
for await (const relation of resourceConfig[resource].mtoLoad) { for await (const relation of resourceConfig[resource].mtoLoad) {
if (data[relation]) { if (data[relation]) {
data[relation] = (await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation])))[0] const relConf = resourceConfig[relation + "s"] || resourceConfig[relation];
const relTable = relConf.table
const relData = await server.db.select().from(relTable).where(eq(relTable.id, data[relation]))
data[relation] = relData[0] || null
} }
} }
} }
if (resourceConfig[resource].mtmLoad) { if (resourceConfig[resource].mtmLoad) {
for await (const relation of resourceConfig[resource].mtmLoad) { for await (const relation of resourceConfig[resource].mtmLoad) {
console.log(relation) const relTable = resourceConfig[relation].table
data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id)) const parentKey = resource.substring(0, resource.length - 1)
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/projects/:id", err) console.error("ERROR /resource/:resource/:id", err)
return reply.code(500).send({ error: "Internal Server Error" }) return reply.code(500).send({ error: "Internal Server Error" })
} }
}) })
@@ -432,132 +324,59 @@ 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) { 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 config = resourceConfig[resource];
const table = config.table;
const table = resourceConfig[resource].table let createData = { ...body, tenant: req.user.tenant_id, archived: false };
let createData = { if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
...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) const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
console.log(result) createData[config.numberRangeHolder] = result.usedNumber
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 const [created] = await server.db.insert(table).values(createData).returning()
.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.log(error) console.error(error);
reply.status(500) reply.status(500);
} }
}); });
// UPDATE (inkl. Soft-Delete/Archive) // Update
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
const tenantId = (req.user as any)?.tenant_id if (!tenantId || !userId) return reply.code(401).send({ error: "Unauthorized" })
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; }
//TODO: HISTORY
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 } let data = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
//@ts-ignore //@ts-ignore
delete data.updatedBy delete data.updatedBy; delete data.updatedAt;
//@ts-ignore
delete data.updatedAt
console.log(data)
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
console.log(key) if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") {
if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) {
data[key] = normalizeDate(data[key]) data[key] = normalizeDate(data[key])
} }
}) })
console.log(data) const [updated] = await server.db.update(table).set(data).where(and(eq(table.id, id), eq(table.tenant, tenantId))).returning()
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.log("ERROR /resource/projects/:id", err) console.error(err)
} }
}) })
} }

View File

@@ -36,7 +36,7 @@ import {
export const resourceConfig = { export const resourceConfig = {
projects: { projects: {
searchColumns: ["name"], searchColumns: ["name","customerRef","projectNumber","notes"],
mtoLoad: ["customer","plant","contract","projecttype"], mtoLoad: ["customer","plant","contract","projecttype"],
mtmLoad: ["tasks", "files","createddocuments"], mtmLoad: ["tasks", "files","createddocuments"],
table: projects, table: projects,
@@ -61,6 +61,7 @@ export const resourceConfig = {
}, },
plants: { plants: {
table: plants, table: plants,
searchColumns: ["name"],
mtoLoad: ["customer"], mtoLoad: ["customer"],
mtmLoad: ["projects","tasks","files"], mtmLoad: ["projects","tasks","files"],
}, },

View File

@@ -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,
linkedDocument: itemInfo.value.linkedDocument, createddocument: itemInfo.value.createddocument,
agriculture: itemInfo.value.agriculture, agriculture: itemInfo.value.agriculture,
letterhead: itemInfo.value.letterhead, letterhead: itemInfo.value.letterhead,
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices, usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices,

View File

@@ -8,7 +8,6 @@
class="hidden lg:block" class="hidden lg:block"
icon="i-heroicons-funnel" icon="i-heroicons-funnel"
placeholder="Suche..." placeholder="Suche..."
@change="tempStore.modifySearchString('createddocuments',searchString)"
@keydown.esc="$event.target.blur()" @keydown.esc="$event.target.blur()"
> >
<template #trailing> <template #trailing>
@@ -30,22 +29,7 @@
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<UDashboardToolbar> <UDashboardToolbar>
<template #right> <template #right>
<!-- <USelectMenu
v-model="selectedColumns"
:options="templateColumns"
by="key"
class="hidden lg:block"
icon="i-heroicons-adjustments-horizontal-solid"
multiple
@change="tempStore.modifyColumns('createddocuments',selectedColumns)"
>
<template #label>
Spalten
</template>
</USelectMenu>-->
<USelectMenu <USelectMenu
v-if="selectableFilters.length > 0" v-if="selectableFilters.length > 0"
v-model="selectedFilters" v-model="selectedFilters"
@@ -157,6 +141,31 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ref, computed, watch } from 'vue';
const dataStore = useDataStore()
const tempStore = useTempStore()
const router = useRouter()
const type = "createddocuments"
const dataType = dataStore.dataTypes[type]
const items = ref([])
const selectedItem = ref(0)
const activeTabIndex = ref(0)
// Debounce-Logik für die Suche
const searchString = ref(tempStore.searchStrings['createddocuments'] || '')
const debouncedSearchString = ref(searchString.value)
let debounceTimeout = null
watch(searchString, (newVal) => {
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(() => {
debouncedSearchString.value = newVal
tempStore.modifySearchString('createddocuments', newVal)
}, 300) // 300ms warten nach dem letzten Tastendruck
})
defineShortcuts({ defineShortcuts({
'/': () => { '/': () => {
@@ -168,10 +177,6 @@ defineShortcuts({
'Enter': { 'Enter': {
usingInput: true, usingInput: true,
handler: () => { handler: () => {
// Zugriff auf das aktuell sichtbare Element basierend auf Tab und Selektion
const currentList = getRowsForTab(selectedTypes.value[activeTabIndex.value]?.key || 'drafts')
// Fallback auf globale Liste falls nötig, aber Logik sollte auf Tab passen
if (filteredRows.value[selectedItem.value]) { if (filteredRows.value[selectedItem.value]) {
router.push(`/createDocument/show/${filteredRows.value[selectedItem.value].id}`) router.push(`/createDocument/show/${filteredRows.value[selectedItem.value].id}`)
} }
@@ -193,17 +198,6 @@ defineShortcuts({
} }
}) })
const dataStore = useDataStore()
const tempStore = useTempStore()
const router = useRouter()
const type = "createddocuments"
const dataType = dataStore.dataTypes[type]
const items = ref([])
const selectedItem = ref(0)
const activeTabIndex = ref(0)
const setupPage = async () => { const setupPage = async () => {
items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)", "documentNumber", true, true)) items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)", "documentNumber", true, true))
} }
@@ -222,10 +216,9 @@ const templateColumns = [
{key: "dueDate", label: "Fällig"} {key: "dueDate", label: "Fällig"}
] ]
// Eigene Spalten für Entwürfe: Referenz raus, Status rein
const draftColumns = [ const draftColumns = [
{key: 'type', label: "Typ"}, {key: 'type', label: "Typ"},
{key: 'state', label: "Status"}, // Status wieder drin {key: 'state', label: "Status"},
{key: 'partner', label: "Kunde"}, {key: 'partner', label: "Kunde"},
{key: "date", label: "Erstellt am"}, {key: "date", label: "Erstellt am"},
{key: "amount", label: "Betrag"} {key: "amount", label: "Betrag"}
@@ -242,31 +235,11 @@ const getColumnsForTab = (tabKey) => {
} }
const templateTypes = [ const templateTypes = [
{ { key: "drafts", label: "Entwürfe" },
key: "drafts", { key: "invoices", label: "Rechnungen" },
label: "Entwürfe" { key: "quotes", label: "Angebote" },
}, { key: "deliveryNotes", label: "Lieferscheine" },
{ { key: "confirmationOrders", label: "Auftragsbestätigungen" }
key: "invoices",
label: "Rechnungen"
},
/*,{
key: "cancellationInvoices",
label: "Stornorechnungen"
},{
key: "advanceInvoices",
label: "Abschlagsrechnungen"
},*/
{
key: "quotes",
label: "Angebote"
}, {
key: "deliveryNotes",
label: "Lieferscheine"
}, {
key: "confirmationOrders",
label: "Auftragsbestätigungen"
}
] ]
const selectedTypes = ref(tempStore.filters["createddocuments"] ? tempStore.filters["createddocuments"] : templateTypes) const selectedTypes = ref(tempStore.filters["createddocuments"] ? tempStore.filters["createddocuments"] : templateTypes)
const types = computed(() => { const types = computed(() => {
@@ -274,10 +247,9 @@ const types = computed(() => {
}) })
const selectItem = (item) => { const selectItem = (item) => {
console.log(item)
if (item.state === "Entwurf") { if (item.state === "Entwurf") {
router.push(`/createDocument/edit/${item.id}`) router.push(`/createDocument/edit/${item.id}`)
} else if (item.state !== "Entwurf") { } else {
router.push(`/createDocument/show/${item.id}`) router.push(`/createDocument/show/${item.id}`)
} }
} }
@@ -286,24 +258,19 @@ const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".", ",")} ${currency}` return `${Number(value).toFixed(2).replace(".", ",")} ${currency}`
} }
const searchString = ref(tempStore.searchStrings['createddocuments'] || '')
const clearSearchString = () => { const clearSearchString = () => {
tempStore.clearSearchString('createddocuments') tempStore.clearSearchString('createddocuments')
searchString.value = '' searchString.value = ''
debouncedSearchString.value = ''
} }
const selectableFilters = ref(dataType.filters.map(i => i.name)) const selectableFilters = ref(dataType.filters.map(i => i.name))
const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.name) || []) const selectedFilters = ref(dataType.filters.filter(i => i.default).map(i => i.name) || [])
const filteredRows = computed(() => { const filteredRows = computed(() => {
let tempItems = items.value.filter(i => types.value.find(x => { let tempItems = items.value.filter(i => types.value.find(x => {
// 1. Draft Tab Logic
if (x.key === 'drafts') return i.state === 'Entwurf' if (x.key === 'drafts') return i.state === 'Entwurf'
// 2. Global Draft Exclusion (drafts shouldn't be in other tabs)
if (i.state === 'Entwurf' && x.key !== 'drafts') return false if (i.state === 'Entwurf' && x.key !== 'drafts') return false
// 3. Normal Type Logic
if (x.key === 'invoices') return ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type) if (x.key === 'invoices') return ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type)
return x.key === i.type return x.key === i.type
})) }))
@@ -324,22 +291,16 @@ const filteredRows = computed(() => {
}) })
} }
tempItems = useSearch(searchString.value, tempItems) // Hier nutzen wir nun den debounced Wert für die lokale Suche
const results = useSearch(debouncedSearchString.value, tempItems.slice().reverse())
return useSearch(searchString.value, tempItems.slice().reverse()) return results
}) })
const getRowsForTab = (tabKey) => { const getRowsForTab = (tabKey) => {
return filteredRows.value.filter(row => { return filteredRows.value.filter(row => {
if (tabKey === 'drafts') { if (tabKey === 'drafts') return row.state === 'Entwurf'
return row.state === 'Entwurf'
}
if (row.state === 'Entwurf') return false if (row.state === 'Entwurf') return false
if (tabKey === 'invoices') return ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(row.type)
if (tabKey === 'invoices') {
return ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(row.type)
}
return row.type === tabKey return row.type === tabKey
}) })
} }
@@ -350,6 +311,3 @@ const isPaid = (item) => {
return Number(amountPaid.toFixed(2)) === useSum().getCreatedDocumentSum(item, items.value) return Number(amountPaid.toFixed(2)) === useSum().getCreatedDocumentSum(item, items.value)
} }
</script> </script>
<style scoped>
</style>

View File

@@ -17,11 +17,18 @@ 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)")
console.log(itemInfo.value) if(itemInfo.value.type === "invoices"){
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)
} }
@@ -80,17 +87,23 @@ 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"
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'" :disabled="links.find(i => i.type === 'cancellationInvoices')"
> >
Stornieren Stornieren
</UButton> </UButton>
</UTooltip>
<UButton <UButton
v-if="itemInfo.project" v-if="itemInfo.project"
@click="router.push(`/standardEntity/projects/show/${itemInfo.project}`)" @click="router.push(`/standardEntity/projects/show/${itemInfo.project?.id}`)"
icon="i-heroicons-link" icon="i-heroicons-link"
variant="outline" variant="outline"
> >
@@ -98,12 +111,36 @@ const openBankstatements = () => {
</UButton> </UButton>
<UButton <UButton
v-if="itemInfo.customer" v-if="itemInfo.customer"
@click="router.push(`/standardEntity/customers/show/${itemInfo.customer}`)" @click="router.push(`/standardEntity/customers/show/${itemInfo.customer?.id}`)"
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}`)"