Compare commits

...

3 Commits

Author SHA1 Message Date
0141a243ce Initial for #123
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 1m10s
2026-02-21 22:21:10 +01:00
a0e1b8c0eb Fix Error in IcomingInvoice Opening of Drafts 2026-02-21 22:19:45 +01:00
45fb45845a Fix #116 2026-02-21 22:17:58 +01:00
13 changed files with 157 additions and 24 deletions

View File

@@ -127,6 +127,13 @@
"when": 1771704862789, "when": 1771704862789,
"tag": "0017_slow_the_hood", "tag": "0017_slow_the_hood",
"breakpoints": true "breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1773000900000,
"tag": "0018_account_chart",
"breakpoints": true
} }
] ]
} }

View File

@@ -16,6 +16,7 @@ export const accounts = pgTable("accounts", {
number: text("number").notNull(), number: text("number").notNull(),
label: text("label").notNull(), label: text("label").notNull(),
accountChart: text("accountChart").notNull().default("skr03"),
description: text("description"), description: text("description"),
}) })

View File

@@ -94,6 +94,7 @@ export const tenants = pgTable(
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 }, projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 }, costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
}), }),
accountChart: text("accountChart").notNull().default("skr03"),
standardEmailForInvoices: text("standardEmailForInvoices"), standardEmailForInvoices: text("standardEmailForInvoices"),

View File

@@ -94,6 +94,7 @@ export default async function adminRoutes(server: FastifyInstance) {
short: tenants.short, short: tenants.short,
locked: tenants.locked, locked: tenants.locked,
numberRanges: tenants.numberRanges, numberRanges: tenants.numberRanges,
accountChart: tenants.accountChart,
extraModules: tenants.extraModules, extraModules: tenants.extraModules,
}) })
.from(authTenantUsers) .from(authTenantUsers)

View File

@@ -54,6 +54,7 @@ export default async function meRoutes(server: FastifyInstance) {
extraModules: tenants.extraModules, extraModules: tenants.extraModules,
businessInfo: tenants.businessInfo, businessInfo: tenants.businessInfo,
numberRanges: tenants.numberRanges, numberRanges: tenants.numberRanges,
accountChart: tenants.accountChart,
dokuboxkey: tenants.dokuboxkey, dokuboxkey: tenants.dokuboxkey,
standardEmailForInvoices: tenants.standardEmailForInvoices, standardEmailForInvoices: tenants.standardEmailForInvoices,
standardPaymentDays: tenants.standardPaymentDays, standardPaymentDays: tenants.standardPaymentDays,

View File

@@ -59,6 +59,44 @@ const parseId = (value: string) => {
} }
export default async function resourceHistoryRoutes(server: FastifyInstance) { 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<{ server.get<{
Params: { resource: string; id: string } Params: { resource: string; id: string }
}>("/resource/:resource/:id/history", { }>("/resource/:resource/:id/history", {

View File

@@ -586,6 +586,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
try { try {
if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" }); if (!req.user?.tenant_id) return reply.code(400).send({ error: "No tenant selected" });
const { resource } = req.params as { resource: string }; 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<string, any>; const body = req.body as Record<string, any>;
const config = resourceConfig[resource]; const config = resourceConfig[resource];
const table = config.table; const table = config.table;
@@ -656,6 +659,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
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 }
if (resource === "accounts") {
return reply.code(403).send({ error: "Accounts are read-only" })
}
const body = req.body as Record<string, any> const body = req.body as Record<string, any>
const tenantId = req.user?.tenant_id const tenantId = req.user?.tenant_id
const userId = req.user?.user_id const userId = req.user?.user_id

View File

@@ -1,9 +1,9 @@
import { FastifyInstance } from "fastify" import { FastifyInstance } from "fastify"
import { asc, desc } from "drizzle-orm" import { asc, desc, eq } from "drizzle-orm"
import { sortData } from "../utils/sort" import { sortData } from "../utils/sort"
// Schema imports // Schema imports
import { accounts, units,countrys } from "../../db/schema" import { accounts, units, countrys, tenants } from "../../db/schema"
const TABLE_MAP: Record<string, any> = { const TABLE_MAP: Record<string, any> = {
accounts, accounts,
@@ -40,6 +40,44 @@ export default async function resourceRoutesSpecial(server: FastifyInstance) {
// Wir geben IMMER alle Spalten zurück → kompatibel zum Frontend // 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) let query = server.db.select().from(table)
// --------------------------------------- // ---------------------------------------

View File

@@ -11,7 +11,7 @@ import { s3 } from "./s3";
import { secrets } from "./secrets"; import { secrets } from "./secrets";
// Drizzle schema // Drizzle schema
import { vendors, accounts } from "../../db/schema"; import { vendors, accounts, tenants } from "../../db/schema";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
let openai: OpenAI | null = null; let openai: OpenAI | null = null;
@@ -163,13 +163,22 @@ export const getInvoiceDataFromGPT = async function (
.from(vendors) .from(vendors)
.where(eq(vendors.tenant,tenantId)); .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 const accountList = await server.db
.select({ .select({
id: accounts.id, id: accounts.id,
label: accounts.label, label: accounts.label,
number: accounts.number, number: accounts.number,
}) })
.from(accounts); .from(accounts)
.where(eq(accounts.accountChart, activeAccountChart));
// --------------------------------------------------------- // ---------------------------------------------------------
// 4) GPT ANALYSIS // 4) GPT ANALYSIS

View File

@@ -3,11 +3,13 @@ import dayjs from "dayjs"
const props = defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
required: true required: false,
default: null
}, },
elementId: { elementId: {
type: String, type: String,
required: true required: false,
default: null
}, },
renderHeadline: { renderHeadline: {
type: Boolean, type: Boolean,
@@ -25,13 +27,11 @@ const items = ref([])
const platform = ref("default") const platform = ref("default")
const setup = async () => { const setup = async () => {
if(props.type && props.elementId){ if(props.type && props.elementId){
items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`) items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`)
} /*else { } else {
items.value = await useNuxtApp().$api(`/api/history`)
}*/ }
} }
setup() setup()
@@ -43,6 +43,10 @@ const addHistoryItemData = ref({
}) })
const addHistoryItem = async () => { 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`, { const res = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`, {
method: "POST", method: "POST",
@@ -161,4 +165,4 @@ const renderText = (text) => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -47,8 +47,7 @@ const links = computed(() => {
id: 'historyitems', id: 'historyitems',
label: "Logbuch", label: "Logbuch",
to: "/historyitems", to: "/historyitems",
icon: "i-heroicons-book-open", icon: "i-heroicons-book-open"
disabled: true
}, },
{ {
label: "Organisation", label: "Organisation",

View File

@@ -18,7 +18,10 @@ defineShortcuts({
'Enter': { 'Enter': {
usingInput: true, usingInput: true,
handler: () => { handler: () => {
router.push(`/incomingInvoices/show/${filteredRows.value[selectedItem.value].id}`) const invoice = filteredRows.value[selectedItem.value]
if (invoice) {
selectIncomingInvoice(invoice)
}
} }
}, },
'arrowdown': () => { 'arrowdown': () => {
@@ -146,13 +149,11 @@ const isPaid = (item) => {
} }
const selectIncomingInvoice = (invoice) => { const selectIncomingInvoice = (invoice) => {
if(invoice.state === "Vorbereitet" ) { if (invoice.state === "Gebucht") {
router.push(`/incomingInvoices/edit/${invoice.id}`)
} else {
router.push(`/incomingInvoices/show/${invoice.id}`) router.push(`/incomingInvoices/show/${invoice.id}`)
} else {
router.push(`/incomingInvoices/edit/${invoice.id}`)
} }
} }
@@ -291,4 +292,4 @@ const selectIncomingInvoice = (invoice) => {
<style scoped> <style scoped>
</style> </style>

View File

@@ -15,6 +15,11 @@ const setupPage = async () => {
const features = ref(auth.activeTenantData.features) const features = ref(auth.activeTenantData.features)
const businessInfo = ref(auth.activeTenantData.businessInfo) 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) => { const updateTenant = async (newData) => {
@@ -24,6 +29,11 @@ const updateTenant = async (newData) => {
data: newData, data: newData,
} }
}) })
if (res) {
itemInfo.value = res
auth.activeTenantData = res
}
} }
setupPage() setupPage()
@@ -63,8 +73,8 @@ setupPage()
</div> </div>
<div v-if="item.label === 'Rechnung & Kontakt'"> <div v-if="item.label === 'Rechnung & Kontakt'">
<UCard class="mt-5"> <UCard class="mt-5">
<UForm class="w-1/2"> <UForm class="w-1/2">
<UFormGroup <UFormGroup
label="Firmenname:" label="Firmenname:"
> >
@@ -90,6 +100,23 @@ setupPage()
> >
Speichern Speichern
</UButton> </UButton>
<UFormGroup
label="Kontenrahmen:"
class="mt-6"
>
<USelectMenu
v-model="accountChart"
:options="accountChartOptions"
option-attribute="label"
value-attribute="value"
/>
</UFormGroup>
<UButton
class="mt-3"
@click="updateTenant({accountChart: accountChart})"
>
Kontenrahmen speichern
</UButton>
</UForm> </UForm>
</UCard> </UCard>