From 45fb45845a8e31a8bb8bee550bcca89f2e8c6329 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Sat, 21 Feb 2026 22:17:58 +0100 Subject: [PATCH 1/3] Fix #116 --- backend/src/routes/history.ts | 38 ++++++++++++++++++++++++++ frontend/components/HistoryDisplay.vue | 20 ++++++++------ frontend/components/MainNav.vue | 3 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/backend/src/routes/history.ts b/backend/src/routes/history.ts index eec50fc..0bcb816 100644 --- a/backend/src/routes/history.ts +++ b/backend/src/routes/history.ts @@ -59,6 +59,44 @@ const parseId = (value: string) => { } export default async function resourceHistoryRoutes(server: FastifyInstance) { + server.get("/history", { + schema: { + tags: ["History"], + summary: "Get all history entries for the active tenant", + }, + }, async (req: any) => { + const data = await server.db + .select() + .from(historyitems) + .where(eq(historyitems.tenant, req.user?.tenant_id)) + .orderBy(asc(historyitems.createdAt)); + + const userIds = Array.from( + new Set(data.map((item) => item.createdBy).filter(Boolean)) + ) as string[]; + + const profiles = userIds.length > 0 + ? await server.db + .select() + .from(authProfiles) + .where(and( + eq(authProfiles.tenant_id, req.user?.tenant_id), + inArray(authProfiles.user_id, userIds) + )) + : []; + + const profileByUserId = new Map( + profiles.map((profile) => [profile.user_id, profile]) + ); + + return data.map((historyitem) => ({ + ...historyitem, + created_at: historyitem.createdAt, + created_by: historyitem.createdBy, + created_by_profile: historyitem.createdBy ? profileByUserId.get(historyitem.createdBy) || null : null, + })); + }); + server.get<{ Params: { resource: string; id: string } }>("/resource/:resource/:id/history", { diff --git a/frontend/components/HistoryDisplay.vue b/frontend/components/HistoryDisplay.vue index a8d5a72..6385174 100644 --- a/frontend/components/HistoryDisplay.vue +++ b/frontend/components/HistoryDisplay.vue @@ -3,11 +3,13 @@ import dayjs from "dayjs" const props = defineProps({ type: { type: String, - required: true + required: false, + default: null }, elementId: { type: String, - required: true + required: false, + default: null }, renderHeadline: { type: Boolean, @@ -25,13 +27,11 @@ const items = ref([]) const platform = ref("default") const setup = async () => { - - if(props.type && props.elementId){ items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`) - } /*else { - - }*/ + } else { + items.value = await useNuxtApp().$api(`/api/history`) + } } setup() @@ -43,6 +43,10 @@ const addHistoryItemData = ref({ }) const addHistoryItem = async () => { + if (!props.type || !props.elementId) { + toast.add({ title: "Im zentralen Logbuch können keine direkten Einträge erstellt werden." }) + return + } const res = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`, { method: "POST", @@ -161,4 +165,4 @@ const renderText = (text) => { \ No newline at end of file + diff --git a/frontend/components/MainNav.vue b/frontend/components/MainNav.vue index 770e8ed..c93d910 100644 --- a/frontend/components/MainNav.vue +++ b/frontend/components/MainNav.vue @@ -47,8 +47,7 @@ const links = computed(() => { id: 'historyitems', label: "Logbuch", to: "/historyitems", - icon: "i-heroicons-book-open", - disabled: true + icon: "i-heroicons-book-open" }, { label: "Organisation", From a0e1b8c0eb5e90866ab180a66e5f702f991a4b1b Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Sat, 21 Feb 2026 22:19:45 +0100 Subject: [PATCH 2/3] Fix Error in IcomingInvoice Opening of Drafts --- frontend/pages/incomingInvoices/index.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/pages/incomingInvoices/index.vue b/frontend/pages/incomingInvoices/index.vue index 52e884f..509ea23 100644 --- a/frontend/pages/incomingInvoices/index.vue +++ b/frontend/pages/incomingInvoices/index.vue @@ -18,7 +18,10 @@ defineShortcuts({ 'Enter': { usingInput: true, handler: () => { - router.push(`/incomingInvoices/show/${filteredRows.value[selectedItem.value].id}`) + const invoice = filteredRows.value[selectedItem.value] + if (invoice) { + selectIncomingInvoice(invoice) + } } }, 'arrowdown': () => { @@ -146,13 +149,11 @@ const isPaid = (item) => { } const selectIncomingInvoice = (invoice) => { - if(invoice.state === "Vorbereitet" ) { - router.push(`/incomingInvoices/edit/${invoice.id}`) - } else { + if (invoice.state === "Gebucht") { router.push(`/incomingInvoices/show/${invoice.id}`) + } else { + router.push(`/incomingInvoices/edit/${invoice.id}`) } - - } @@ -291,4 +292,4 @@ const selectIncomingInvoice = (invoice) => { \ No newline at end of file + From 0141a243ce02002b086e39d8a6a9aec4587c39c0 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Sat, 21 Feb 2026 22:21:10 +0100 Subject: [PATCH 3/3] Initial for #123 --- backend/db/migrations/meta/_journal.json | 9 ++++- backend/db/schema/accounts.ts | 1 + backend/db/schema/tenants.ts | 1 + backend/src/routes/admin.ts | 1 + backend/src/routes/auth/me.ts | 1 + backend/src/routes/resources/main.ts | 6 ++++ backend/src/routes/resourcesSpecial.ts | 42 ++++++++++++++++++++++-- backend/src/utils/gpt.ts | 13 ++++++-- frontend/pages/settings/tenant.vue | 31 +++++++++++++++-- 9 files changed, 98 insertions(+), 7 deletions(-) diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index a9dd05f..0d46d0c 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1771704862789, "tag": "0017_slow_the_hood", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1773000900000, + "tag": "0018_account_chart", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/backend/db/schema/accounts.ts b/backend/db/schema/accounts.ts index cc154de..58bf757 100644 --- a/backend/db/schema/accounts.ts +++ b/backend/db/schema/accounts.ts @@ -16,6 +16,7 @@ export const accounts = pgTable("accounts", { number: text("number").notNull(), label: text("label").notNull(), + accountChart: text("accountChart").notNull().default("skr03"), description: text("description"), }) diff --git a/backend/db/schema/tenants.ts b/backend/db/schema/tenants.ts index c73cfbf..82f4ecb 100644 --- a/backend/db/schema/tenants.ts +++ b/backend/db/schema/tenants.ts @@ -94,6 +94,7 @@ export const tenants = pgTable( projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 }, costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 }, }), + accountChart: text("accountChart").notNull().default("skr03"), standardEmailForInvoices: text("standardEmailForInvoices"), diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index 9120d18..10c7d53 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -94,6 +94,7 @@ export default async function adminRoutes(server: FastifyInstance) { short: tenants.short, locked: tenants.locked, numberRanges: tenants.numberRanges, + accountChart: tenants.accountChart, extraModules: tenants.extraModules, }) .from(authTenantUsers) diff --git a/backend/src/routes/auth/me.ts b/backend/src/routes/auth/me.ts index fffb793..2db6457 100644 --- a/backend/src/routes/auth/me.ts +++ b/backend/src/routes/auth/me.ts @@ -54,6 +54,7 @@ export default async function meRoutes(server: FastifyInstance) { extraModules: tenants.extraModules, businessInfo: tenants.businessInfo, numberRanges: tenants.numberRanges, + accountChart: tenants.accountChart, dokuboxkey: tenants.dokuboxkey, standardEmailForInvoices: tenants.standardEmailForInvoices, standardPaymentDays: tenants.standardPaymentDays, diff --git a/backend/src/routes/resources/main.ts b/backend/src/routes/resources/main.ts index 2aa351d..d354c4a 100644 --- a/backend/src/routes/resources/main.ts +++ b/backend/src/routes/resources/main.ts @@ -586,6 +586,9 @@ export default async function resourceRoutes(server: FastifyInstance) { try { if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" }); const { resource } = req.params as { resource: string }; + if (resource === "accounts") { + return reply.code(403).send({ error: "Accounts are read-only" }) + } const body = req.body as Record; const config = resourceConfig[resource]; const table = config.table; @@ -656,6 +659,9 @@ export default async function resourceRoutes(server: FastifyInstance) { server.put("/resource/:resource/:id", async (req, reply) => { try { const { resource, id } = req.params as { resource: string; id: string } + if (resource === "accounts") { + return reply.code(403).send({ error: "Accounts are read-only" }) + } const body = req.body as Record const tenantId = req.user?.tenant_id const userId = req.user?.user_id diff --git a/backend/src/routes/resourcesSpecial.ts b/backend/src/routes/resourcesSpecial.ts index c191615..8717835 100644 --- a/backend/src/routes/resourcesSpecial.ts +++ b/backend/src/routes/resourcesSpecial.ts @@ -1,9 +1,9 @@ import { FastifyInstance } from "fastify" -import { asc, desc } from "drizzle-orm" +import { asc, desc, eq } from "drizzle-orm" import { sortData } from "../utils/sort" // Schema imports -import { accounts, units,countrys } from "../../db/schema" +import { accounts, units, countrys, tenants } from "../../db/schema" const TABLE_MAP: Record = { accounts, @@ -40,6 +40,44 @@ export default async function resourceRoutesSpecial(server: FastifyInstance) { // Wir geben IMMER alle Spalten zurück → kompatibel zum Frontend // --------------------------------------- + if (resource === "accounts") { + const [tenant] = await server.db + .select({ + accountChart: tenants.accountChart, + }) + .from(tenants) + .where(eq(tenants.id, Number(req.user.tenant_id))) + .limit(1) + + const activeAccountChart = tenant?.accountChart || "skr03" + let data + if (sort && (accounts as any)[sort]) { + const col = (accounts as any)[sort] + data = ascQuery === "true" + ? await server.db + .select() + .from(accounts) + .where(eq(accounts.accountChart, activeAccountChart)) + .orderBy(asc(col)) + : await server.db + .select() + .from(accounts) + .where(eq(accounts.accountChart, activeAccountChart)) + .orderBy(desc(col)) + } else { + data = await server.db + .select() + .from(accounts) + .where(eq(accounts.accountChart, activeAccountChart)) + } + + return sortData( + data, + sort as any, + ascQuery === "true" + ) + } + let query = server.db.select().from(table) // --------------------------------------- diff --git a/backend/src/utils/gpt.ts b/backend/src/utils/gpt.ts index 0d73eeb..c61f386 100644 --- a/backend/src/utils/gpt.ts +++ b/backend/src/utils/gpt.ts @@ -11,7 +11,7 @@ import { s3 } from "./s3"; import { secrets } from "./secrets"; // Drizzle schema -import { vendors, accounts } from "../../db/schema"; +import { vendors, accounts, tenants } from "../../db/schema"; import {eq} from "drizzle-orm"; let openai: OpenAI | null = null; @@ -163,13 +163,22 @@ export const getInvoiceDataFromGPT = async function ( .from(vendors) .where(eq(vendors.tenant,tenantId)); + const [tenant] = await server.db + .select({ accountChart: tenants.accountChart }) + .from(tenants) + .where(eq(tenants.id, tenantId)) + .limit(1) + + const activeAccountChart = tenant?.accountChart || "skr03" + const accountList = await server.db .select({ id: accounts.id, label: accounts.label, number: accounts.number, }) - .from(accounts); + .from(accounts) + .where(eq(accounts.accountChart, activeAccountChart)); // --------------------------------------------------------- // 4) GPT ANALYSIS diff --git a/frontend/pages/settings/tenant.vue b/frontend/pages/settings/tenant.vue index 2295e35..26a4b5d 100644 --- a/frontend/pages/settings/tenant.vue +++ b/frontend/pages/settings/tenant.vue @@ -15,6 +15,11 @@ const setupPage = async () => { const features = ref(auth.activeTenantData.features) const businessInfo = ref(auth.activeTenantData.businessInfo) +const accountChart = ref(auth.activeTenantData.accountChart || "skr03") +const accountChartOptions = [ + { label: "SKR 03", value: "skr03" }, + { label: "Verein", value: "verein" } +] const updateTenant = async (newData) => { @@ -24,6 +29,11 @@ const updateTenant = async (newData) => { data: newData, } }) + + if (res) { + itemInfo.value = res + auth.activeTenantData = res + } } setupPage() @@ -63,8 +73,8 @@ setupPage()
- - + + @@ -90,6 +100,23 @@ setupPage() > Speichern + + + + + Kontenrahmen speichern +