Compare commits

...

30 Commits

Author SHA1 Message Date
e7554fa2cc .gitea/ISSUE_TEMPLATE/feature_request.md aktualisiert
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 16s
2026-01-22 17:42:28 +00:00
7c1fabf58a .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
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 16s
2026-01-22 17:40:58 +00:00
1203b6cbd1 .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
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 16s
2026-01-15 11:39:03 +00:00
525f2906fb .gitea/ISSUE_TEMPLATE/feature_request.md aktualisiert
Some checks failed
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
2026-01-15 11:38:50 +00:00
b105382abf .gitea/ISSUE_TEMPLATE/bug_report.md aktualisiert
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 15s
2026-01-15 11:38:00 +00:00
b1cdec7d17 Merge pull request 'Added feature request template' (#62) from dev into main
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 16s
Reviewed-on: #62
2026-01-15 11:31:57 +00:00
6b9de04d83 Added feature request template
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 15s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-15 12:30:33 +01:00
f1d512b2e5 Merge pull request 'dev' (#61) from dev into main
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 16s
Reviewed-on: #61
2026-01-15 11:29:15 +00:00
529ec0c77d Added bug report template
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 15s
2026-01-15 12:27:56 +01:00
246677b750 Fixed Missing Phase on Init 2026-01-15 12:19:34 +01:00
c839714945 Fixed Failing Load #58
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 1m10s
2026-01-15 12:14:46 +01:00
8614917a05 Optimized Dockerfile for Frontend
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 19s
Build and Push Docker Images / build-frontend (push) Successful in 3m12s
2026-01-15 12:03:40 +01:00
2de80ea6ca Fixed #59
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 3m1s
Build and Push Docker Images / build-frontend (push) Successful in 5m49s
2026-01-15 11:52:07 +01:00
6f5fed0ffb Fixed Date #30
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 33s
Build and Push Docker Images / build-frontend (push) Successful in 5m54s
2026-01-14 15:51:04 +01:00
767152c535 Added Color Mode Button Fix #46
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 5m52s
2026-01-14 15:15:36 +01:00
3128893ba2 Added Error Toasts Fix #52
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 16s
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-01-14 15:10:37 +01:00
bcf460cfd5 Fix mtoLoad Customer in Contracts #56
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 15s
2026-01-14 11:15:32 +01:00
da704be925 Fix DeliveryDateType #23 #54
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 35s
Build and Push Docker Images / build-frontend (push) Successful in 17s
2026-01-14 11:06:48 +01:00
c049730599 Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 2m19s
2026-01-13 14:41:34 +01:00
0194345ed8 Renamed Anwesenheiten to Zeiten FIX #31 2026-01-13 14:41:21 +01:00
82d8cd36b9 Renamed Anwesenheiten to Zeiten FIX#31
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 19s
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-01-13 14:40:53 +01:00
66110da6c4 Removed Dev Output
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 5m53s
2026-01-13 14:24:39 +01:00
267648074c Fix #53
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 33s
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-01-13 14:24:04 +01:00
32b4c40e11 Fix #1
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 5m55s
2026-01-12 19:40:32 +01:00
f044195d86 Fix #20 2026-01-12 19:37:22 +01:00
202e20ddd5 Fix #3
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 34s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-12 19:27:46 +01:00
905f2e7bf4 Fix #47
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 6m2s
Fix #19
Fix Adresse laden bei Dokument kopieren
2026-01-12 18:33:08 +01:00
b39a52fb20 Fix #19 Ansprechpartner nicht auswählbar
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 5m58s
2026-01-11 11:03:11 +01:00
098bd02808 Fixed PageLeaveGuard.vue Dark Mode
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 6m9s
2026-01-10 20:03:45 +01:00
db21b43120 Merge pull request 'dev' (#40) from dev into main
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 16s
Reviewed-on: #40
2026-01-08 22:21:06 +00:00
20 changed files with 191 additions and 80 deletions

View File

@@ -0,0 +1,19 @@
---
name: 🐛 Bug Report
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
title: '[BUG] '
labels: Problem
assignees: ''
---
**Beschreibung**
**Reproduktion**
**Screenshots**
**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.**

View File

@@ -0,0 +1,17 @@
---
name: ✨ Feature Request
about: Schlage eine Idee für dieses Projekt vor.
title: '[FEATURE] '
labels: Funktionswunsch
assignees: ''
---
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
**Lösungsvorschlag**
**Alternativen**

View File

@@ -71,7 +71,7 @@ export const projects = pgTable("projects", {
updatedAt: timestamp("updated_at", { withTimezone: true }), updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id), updatedBy: uuid("updated_by").references(() => authUsers.id),
active_phase: text("active_phase"), active_phase: text("active_phase").default("Erstkontakt"),
}) })
export type Project = typeof projects.$inferSelect export type Project = typeof projects.$inferSelect

View File

@@ -461,10 +461,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
} }
Object.keys(createData).forEach((key) => { Object.keys(createData).forEach((key) => {
if(key.toLowerCase().includes("date")) 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) .insert(table)
.values(createData) .values(createData)
@@ -513,8 +512,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
let data = {...body, updated_at: new Date().toISOString(), updated_by: userId} 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")) { console.log(key)
if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) {
data[key] = normalizeDate(data[key]) data[key] = normalizeDate(data[key])
} }
}) })

View File

@@ -22,7 +22,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
server.post("/staff/time/event", async (req, reply) => { server.post("/staff/time/event", async (req, reply) => {
try { try {
const userId = req.user.user_id const actorId = req.user.user_id;
const tenantId = req.user.tenant_id const tenantId = req.user.tenant_id
const body = req.body as any const body = req.body as any
@@ -35,17 +35,15 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
const dataToInsert = { const dataToInsert = {
tenant_id: tenantId, tenant_id: tenantId,
user_id: userId, user_id: body.user_id,
actortype: "user", actortype: "user",
actoruser_id: userId, actoruser_id: actorId,
eventtime: normalizeDate(body.eventtime), eventtime: normalizeDate(body.eventtime),
eventtype: body.eventtype, eventtype: body.eventtype,
source: "WEB", source: "WEB",
payload: body.payload // Payload (z.B. Description) mit speichern payload: body.payload // Payload (z.B. Description) mit speichern
} }
console.log(dataToInsert)
const [created] = await server.db const [created] = await server.db
.insert(stafftimeevents) .insert(stafftimeevents)
//@ts-ignore //@ts-ignore
@@ -390,7 +388,9 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
const evaluatedUserId = targetUserId || actingUserId; const evaluatedUserId = targetUserId || actingUserId;
const startDate = new Date(from); const startDate = new Date(from);
const endDate = new Date(to); let endDateQuery = new Date(to);
endDateQuery.setDate(endDateQuery.getDate() + 1);
const endDate = endDateQuery;
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return reply.code(400).send({ error: "Ungültiges Datumsformat." }); return reply.code(400).send({ error: "Ungültiges Datumsformat." });

View File

@@ -57,6 +57,7 @@ export const resourceConfig = {
table: contracts, table: contracts,
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"], searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
numberRangeHolder: "contractNumber", numberRangeHolder: "contractNumber",
mtoLoad: ["customer"],
}, },
plants: { plants: {
table: plants, table: plants,

View File

@@ -1,15 +1,31 @@
FROM node:20-alpine # --- Stage 1: Build ---
FROM node:20-alpine AS builder
RUN mkdir -p /usr/src/nuxt-app
WORKDIR /usr/src/nuxt-app WORKDIR /usr/src/nuxt-app
COPY . .
RUN npm i # Nur Files kopieren, die für die Installation nötig sind (besseres Caching)
COPY package*.json ./
RUN npm install
# Restlichen Code kopieren und bauen
COPY . .
RUN npm run build RUN npm run build
# --- Stage 2: Runtime ---
FROM node:20-alpine AS runner
WORKDIR /usr/src/nuxt-app
# Von der Build-Stage NUR den fertigen .output Ordner kopieren
COPY --from=builder /usr/src/nuxt-app/.output ./.output
# Optional: Falls du statische Dateien aus public brauchst,
# sind diese normalerweise bereits in .output/public enthalten.
ENV NUXT_HOST=0.0.0.0 ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000 ENV NUXT_PORT=3000
ENV NODE_ENV=production
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["node", ".output/server/index.mjs"] ENTRYPOINT ["node", ".output/server/index.mjs"]

View File

@@ -55,7 +55,7 @@ const setup = async () => {
}) })
filetypes.value = await useEntities("filetags").select() filetypes.value = await useEntities("filetags").select()
documentboxes.value = await useEntities("documentboxes").select() //documentboxes.value = await useEntities("documentboxes").select()
} }
setup() setup()

View File

@@ -152,7 +152,7 @@ const links = computed(() => {
icon: "i-heroicons-user-group", icon: "i-heroicons-user-group",
children: [ children: [
... true ? [{ ... true ? [{
label: "Anwesenheiten", label: "Zeiten",
to: "/staff/time", to: "/staff/time",
icon: "i-heroicons-clock", icon: "i-heroicons-clock",
}] : [], }] : [],

View File

@@ -131,38 +131,5 @@ button {
.btn-confirm:hover { background: #dc2626; } .btn-confirm:hover { background: #dc2626; }
/* --------------------------------------------------------- */
/* DARK MODE OVERRIDES */
/* Wir nutzen :global(.dark), um auf die Klasse im <html>/<body> zuzugreifen */
/* --------------------------------------------------------- */
:global(.dark) .guard-overlay {
background: rgba(0,0,0,0.7);
}
:global(.dark) .guard-modal {
background: #1f2937; /* gray-800 */
color: #f3f4f6; /* gray-100 */
border: 1px solid #374151; /* gray-700 Border für Kontrast */
}
:global(.dark) .guard-body {
color: #9ca3af; /* gray-400 */
}
:global(.dark) .btn-cancel {
background: #374151; /* gray-700 */
color: #e5e7eb; /* gray-200 */
}
:global(.dark) .btn-cancel:hover {
background: #4b5563; /* gray-600 */
}
/* Confirm Button bleibt rot, aber evtl. leicht angepasst */
:global(.dark) .btn-confirm {
background: #b91c1c; /* red-700 (etwas dunkler für Darkmode angenehmer) */
color: #fecaca; /* red-100 Text */
}
:global(.dark) .btn-confirm:hover {
background: #991b1b;
}
</style> </style>

View File

@@ -159,7 +159,16 @@ const submit = async () => {
@input="e => form.startDate = new Date(e.target.value)" @input="e => form.startDate = new Date(e.target.value)"
/> />
</UFormGroup> </UFormGroup>
<UFormGroup label="Ende"> <UFormGroup label="Dauer (Stunden)">
<input
type="number"
step="0.25"
placeholder="z.B. 1.5"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6 px-2"
@input="e => form.endDate = dayjs(form.startDate).add(parseFloat(e.target.value), 'hour').toDate()"
/>
</UFormGroup>
<UFormGroup label="Ende" class="col-span-2">
<input <input
type="datetime-local" type="datetime-local"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6 px-2" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary-600 sm:text-sm sm:leading-6 px-2"

View File

@@ -103,7 +103,8 @@ async function onSubmit(event: FormSubmitEvent<any>) {
start: startIso, // Die eingegebene Startzeit start: startIso, // Die eingegebene Startzeit
end: endIso, // Die eingegebene Endzeit (oder null) end: endIso, // Die eingegebene Endzeit (oder null)
type: state.type, type: state.type,
description: state.description description: state.description,
user_id: props.defaultUserId
}) })
toast.add({ title: 'Zeit manuell erfasst', color: 'green' }) toast.add({ title: 'Zeit manuell erfasst', color: 'green' })

View File

@@ -91,15 +91,18 @@ export const useStaffTime = () => {
} }
// 🆕 NEU: Manuellen Eintrag erstellen (Vergangenheit oder Zeitraum) // 🆕 NEU: Manuellen Eintrag erstellen (Vergangenheit oder Zeitraum)
const createEntry = async (data: { start: string, end: string | null, type: string, description: string }) => { const createEntry = async (data: { start: string, end: string | null, type: string, description: string, user_id: string }) => {
// 1. Start Event senden // 1. Start Event senden
// Wir nutzen den dynamischen Typ (work_start, vacation_start etc.) // Wir nutzen den dynamischen Typ (work_start, vacation_start etc.)
console.log(data)
await $api('/api/staff/time/event', { await $api('/api/staff/time/event', {
method: 'POST', method: 'POST',
body: { body: {
eventtype: `${data.type}_start`, eventtype: `${data.type}_start`,
eventtime: data.start, eventtime: data.start,
payload: { description: data.description } payload: { description: data.description },
user_id: data.user_id,
} }
}) })
@@ -109,7 +112,9 @@ export const useStaffTime = () => {
method: 'POST', method: 'POST',
body: { body: {
eventtype: `${data.type}_end`, eventtype: `${data.type}_end`,
eventtime: data.end eventtime: data.end,
payload: { description: data.description },
user_id: data.user_id,
} }
}) })
} }

View File

@@ -236,11 +236,17 @@ const footerLinks = [
<template #footer> <template #footer>
<div class="flex flex-col gap-3 w-full"> <div class="flex flex-col gap-3 w-full">
<UColorModeButton />
<LabelPrinterButton/> <LabelPrinterButton/>
<!-- Footer Links --> <!-- Footer Links -->
<UDashboardSidebarLinks :links="footerLinks" /> <UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" /> <UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/> <UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div> </div>

View File

@@ -204,7 +204,8 @@ const setupPage = async () => {
processDieselPosition() processDieselPosition()
} }
} else if (route.query.loadMode === "finalInvoice") { }
else if (route.query.loadMode === "finalInvoice") {
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id)) let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
//TODO: Implement Checking for Same Customer, Contact and Project //TODO: Implement Checking for Same Customer, Contact and Project
@@ -355,6 +356,8 @@ const setupPage = async () => {
} }
await setCustomerData(null, true)
} }
@@ -459,7 +462,11 @@ const setCustomerData = async (customerId, loadOnlyAdress = false) => {
let customer = customers.value.find(i => i.id === itemInfo.value.customer) let customer = customers.value.find(i => i.id === itemInfo.value.customer)
console.log(customer) console.log(customer)
itemInfo.value.contact = null if(!loadOnlyAdress) {
itemInfo.value.contact = null
}
if (customer) { if (customer) {
itemInfo.value.address.street = customer.infoData.street itemInfo.value.address.street = customer.infoData.street
itemInfo.value.address.zip = customer.infoData.zip itemInfo.value.address.zip = customer.infoData.zip
@@ -1824,7 +1831,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
> >
<InputGroup> <InputGroup>
<USelectMenu <USelectMenu
:options="contacts.filter(i => i.customer === itemInfo.customer)" :options="contacts.filter(i => i.customer?.id === itemInfo.customer)"
option-attribute="fullName" option-attribute="fullName"
value-attribute="id" value-attribute="id"
:search-attributes="['name']" :search-attributes="['name']"
@@ -2057,7 +2064,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
> >
<InputGroup> <InputGroup>
<USelectMenu <USelectMenu
:options="plants.filter(i => i.customer === itemInfo.customer)" :options="plants.filter(i => i.customer?.id === itemInfo.customer)"
v-model="itemInfo.plant" v-model="itemInfo.plant"
value-attribute="id" value-attribute="id"
option-attribute="name" option-attribute="name"
@@ -2095,7 +2102,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
> >
<InputGroup> <InputGroup>
<USelectMenu <USelectMenu
:options="projects.filter(i => i.customer === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))" :options="projects.filter(i => i.customer?.id === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
v-model="itemInfo.project" v-model="itemInfo.project"
value-attribute="id" value-attribute="id"
option-attribute="name" option-attribute="name"
@@ -2134,7 +2141,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
> >
<InputGroup> <InputGroup>
<USelectMenu <USelectMenu
:options="contracts.filter(i => i.customer === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))" :options="contracts.filter(i => i.customer?.id === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
v-model="itemInfo.contract" v-model="itemInfo.contract"
value-attribute="id" value-attribute="id"
option-attribute="name" option-attribute="name"

View File

@@ -81,7 +81,7 @@ const openBankstatements = () => {
E-Mail E-Mail
</UButton> </UButton>
<UButton <UButton
@click="router.push(`/createDocument/edit/?linkedDocument=${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'" v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"

View File

@@ -147,7 +147,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
} else { } else {
item.state = "Entwurf" item.state = "Entwurf"
} }
const data = await useEntities('incominginvoices').update(itemInfo.value.id,item) const data = await useEntities('incominginvoices').update(itemInfo.value.id,item,true)
} }
const findIncomingInvoiceErrors = computed(() => { const findIncomingInvoiceErrors = computed(() => {

View File

@@ -119,8 +119,8 @@ async function loadWorkingTimeInfo() {
// Erstellt Query-Parameter für den neuen Backend-Endpunkt // Erstellt Query-Parameter für den neuen Backend-Endpunkt
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
from: selectedStartDay.value, from: $dayjs(selectedStartDay.value).format("YYYY-MM-DD"),
to: selectedEndDay.value, to: $dayjs(selectedEndDay.value).format("YYYY-MM-DD"),
targetUserId: evaluatedUserId.value, targetUserId: evaluatedUserId.value,
}); });

View File

@@ -1,31 +1,86 @@
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const toast = useToast()
const api = $fetch.create({ const api = $fetch.create({
baseURL: config.public.apiBase, baseURL: config.public.apiBase,
credentials: "include", credentials: "include",
async onRequest({options}) {
// Token aus Cookie holen
let token: string | null | undefined = ""
token = useCookie("token").value async onRequest({ options }) {
const token = useCookie("token").value
// Falls im Request explizit ein anderer JWT übergeben wird
if (options.context?.jwt) {
// Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden options.headers = {
if (options.context && (options.context as any).jwt) { ...options.headers,
token = (options.context as any).jwt Authorization: `Bearer ${options.context.jwt}`,
} }
} else if (token) {
if (token) {
options.headers = { options.headers = {
...options.headers, ...options.headers,
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
} }
} }
},
async onRequestError({error}) {
toast.add({
title: "Fehler",
description: "Eine Anfrage konnte nicht ausgeführt werden.",
color: 'red',
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 5000 // Bleibt 5 Sekunden sichtbar
})
},
async onResponseError({ response }) {
// Toasts nur im Client anzeigen
console.log(response)
if (!process.client) return
const status = response.status
let title = "Fehler"
let description = "Ein unerwarteter Fehler ist aufgetreten."
switch (status) {
case 400:
title = "Anfrage fehlerhaft"
description = "Die Daten konnten nicht korrekt verarbeitet werden."
break
case 401:
title = "Nicht angemeldet"
description = "Deine Sitzung ist abgelaufen oder ungültig."
// Optional: useCookie('token').value = null
break
case 403:
title = "Zugriff verweigert"
description = "Du hast keine Berechtigung für diesen Bereich."
break
case 404:
title = "Nicht gefunden"
description = "Die gesuchte Ressource wurde nicht gefunden."
break
case 500:
title = "Server-Fehler"
description = "Internes Problem. Bitte versuche es später erneut."
break
}
// Nuxt UI Toast Notification
toast.add({
title: title,
description: description,
color: 'red',
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 5000 // Bleibt 5 Sekunden sichtbar
})
} }
}) })
return { provide: { api } } return {
provide: {
api
}
}
}) })

View File

@@ -2499,8 +2499,8 @@ export const useDataStore = defineStore('data', () => {
component: vehicle, component: vehicle,
inputType: "select", inputType: "select",
selectDataType: "vehicles", selectDataType: "vehicles",
selectOptionAttribute: "licensePlate", selectOptionAttribute: "license_plate",
selectSearchAttributes: ['licensePlate'], selectSearchAttributes: ['license_plate','vin'],
}, },
{ {
key: "inventoryitem", key: "inventoryitem",