Compare commits

..

54 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
b35c991634 Fixed PageLeaveGuard.vue Dark Mode
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-10 19:49:28 +01:00
dc376894be Fixed LabelPrinter Dark Mode 2026-01-10 19:46:16 +01:00
90788f22da Fixed displayOpenBalances.vue 2026-01-10 19:43:09 +01:00
d901ebe365 Fix #41
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 5m56s
Redesign Textvorlagen
2026-01-10 19:01:40 +01:00
7f6ba99328 Added Schnellauswahl für die Exporte
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 5m57s
2026-01-10 18:00:12 +01:00
8afdf06c8e Fixed TS #39
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 16s
2026-01-10 17:38:59 +01:00
e1205a8de5 Fixed DATEV Export #39
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 30s
Build and Push Docker Images / build-frontend (push) Successful in 15s
2026-01-10 17:34:02 +01:00
08da93b6c3 Fixed DATEV Export #39
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 31s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-10 17:30:02 +01:00
b0ace924d4 Fix Total Amount #37
Some checks failed
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-01-10 17:25:12 +01:00
593118c181 Changed Reamdme
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-08 23:23:13 +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
67d2a05ac4 #37 open for testing
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 6m17s
2026-01-08 22:54:55 +01:00
b95d539907 Fix Search and Option Display #38 2026-01-08 22:51:18 +01:00
76b363fdaf fix #29 Corrected User ID
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 16s
2026-01-08 12:26:13 +01:00
2cb0d9b607 Fixed Workflow API URL
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-08 11:28:56 +01:00
037a10e93e Fixed DB
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 30s
Build and Push Docker Images / build-frontend (push) Successful in 16s
2026-01-08 11:17:51 +01:00
c36b9aa872 Fixed DB
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-08 11:13:29 +01:00
14a9435a5a Fixed DB
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 28s
Build and Push Docker Images / build-frontend (push) Successful in 15s
2026-01-08 11:11:57 +01:00
8126c2d3f4 Fixed DB
Some checks failed
Build and Push Docker Images / build-backend (push) Failing after 28s
Build and Push Docker Images / build-frontend (push) Successful in 15s
2026-01-08 11:10:02 +01:00
a892b7a6e4 Fixed DB
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-08 10:55:48 +01:00
be7b219569 Merge remote-tracking branch 'origin/dev' into dev
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 2m17s
2026-01-06 21:13:04 +01:00
998d725528 Fixed Login
Allowed Workflows to be public #22
2026-01-06 21:12:51 +01:00
17cd3dc3a3 Fixed Login
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
Allowed Workflows to be public
2026-01-06 21:12:05 +01:00
1d9488b64d Removed Alert Message
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 5m33s
2026-01-06 15:21:09 +01:00
9fb520d8c3 Removed Capacitor 2026-01-06 15:20:26 +01:00
79 changed files with 1450 additions and 2372 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**

110
README.md
View File

@@ -1 +1,109 @@
TEST
# Docker Compose Setup
## ENV Vars
- DOMAIN
- PDF_LICENSE
- DB_PASS
- DB_USER
- CONTACT_EMAIL
## Docker Compose File
~~~
services:
frontend:
image: git.federspiel.tech/flfeders/fedeo/frontend:main
restart: always
environment:
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3000"
# Middlewares
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
# Web Entrypoint
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
backend:
image: git.federspiel.tech/flfeders/fedeo/backend:main
restart: always
environment:
- INFISICAL_CLIENT_ID=
- INFISICAL_CLIENT_SECRET=
- NODE_ENV=production
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.port=3100"
# Middlewares
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
# Web Entrypoint
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend.entrypoints=web"
# Web Secure Entrypoint
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
# db:
# image: postgres
# restart: always
# shm_size: 128mb
# environment:
# POSTGRES_PASSWORD:
# POSTGRES_USER:
# POSTGRES_DB:
# volumes:
# - ./pg-data:/var/lib/postgresql/data
# ports:
# - "5432:5432"
traefik:
image: traefik:v2.11
restart: unless-stopped
container_name: traefik
command:
- "--api.insecure=false"
- "--api.dashboard=false"
- "--api.debug=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=traefik"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secured.address=:443"
- "--accesslog=true"
- "--accesslog.filepath=/logs/access.log"
- "--accesslog.bufferingsize=5000"
- "--accesslog.fields.defaultMode=keep"
- "--accesslog.fields.headers.defaultMode=keep"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
ports:
- 80:80
- 443:443
volumes:
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/logs:/logs"
networks:
- traefik
networks:
traefik:
external: false
~~~

View File

@@ -1,10 +1,13 @@
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import {secrets} from "../src/utils/secrets";
import * as schema from "./schema"
const pool = new Pool({
export const pool = new Pool({
connectionString: secrets.DATABASE_URL,
max: 10, // je nach Last
})
export const db = drizzle(pool)
export const db = drizzle(pool , {schema})

View File

@@ -71,7 +71,7 @@ export const projects = pgTable("projects", {
updatedAt: timestamp("updated_at", { withTimezone: true }),
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

View File

@@ -6,6 +6,6 @@ export default defineConfig({
schema: "./db/schema",
out: "./db/migrations",
dbCredentials: {
url: secrets.DATABASE_URL || process.env.DATABASE_URL,
url: secrets.DATABASE_URL,
},
})

View File

@@ -146,6 +146,7 @@ async function main() {
app.ready(async () => {
try {
console.log("Testing DB Connection:")
const result = await app.db.execute("SELECT NOW()");
console.log("✓ DB connection OK: " + JSON.stringify(result.rows[0]));
} catch (err) {

View File

@@ -9,7 +9,7 @@ export default fp(async (server: FastifyInstance) => {
"http://localhost:3001", // dein Nuxt-Frontend
"http://127.0.0.1:3000", // dein Nuxt-Frontend
"http://192.168.1.227:3001", // dein Nuxt-Frontend
"http://192.168.1.227:3000", // dein Nuxt-Frontend
"http://192.168.1.113:3000", // dein Nuxt-Frontend
"https://beta.fedeo.de", // dein Nuxt-Frontend
"https://app.fedeo.de", // dein Nuxt-Frontend
"capacitor://localhost", // dein Nuxt-Frontend

View File

@@ -1,20 +1,17 @@
import fp from "fastify-plugin"
import {drizzle, NodePgDatabase} from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import * as schema from "../../db/schema"
import {secrets} from "../utils/secrets";
import { Pool } from "pg"
export default fp(async (server, opts) => {
const pool = new Pool({
host: "100.102.185.225",
port: Number(process.env.DB_PORT || 5432),
user: "postgres",
password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu",
database: "fedeo",
ssl: process.env.DB_DISABLE_SSL === "true" ? false : undefined,
connectionString: secrets.DATABASE_URL,
max: 10, // je nach Last
})
// Drizzle instance
const db = drizzle(pool, { schema })
const db = drizzle(pool , {schema})
// Dekorieren -> überall server.db
server.decorate("db", db)
@@ -24,7 +21,7 @@ export default fp(async (server, opts) => {
await pool.end()
})
server.log.info("Drizzle database connected")
console.log("Drizzle database connected")
})
declare module "fastify" {

View File

@@ -11,58 +11,64 @@ import {secrets} from "../utils/secrets";
import {createSEPAExport} from "../utils/export/sepa";
const createDatevExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => {
console.log(startDate,endDate,beraternr,mandantennr)
try {
console.log(startDate,endDate,beraternr,mandantennr)
// 1) ZIP erzeugen
const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr)
console.log("ZIP created")
console.log(buffer)
// 1) ZIP erzeugen
const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr)
console.log("ZIP created")
console.log(buffer)
// 2) Dateiname & Key festlegen
const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip`
console.log(fileKey)
// 2) Dateiname & Key festlegen
const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip`
console.log(fileKey)
// 3) In S3 hochladen
await s3.send(
new PutObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: fileKey,
Body: buffer,
ContentType: "application/zip",
})
)
// 3) In S3 hochladen
await s3.send(
new PutObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: fileKey,
Body: buffer,
ContentType: "application/zip",
})
)
// 4) Presigned URL erzeugen (24h gültig)
const url = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: fileKey,
}),
{ expiresIn: 60 * 60 * 24 }
)
// 4) Presigned URL erzeugen (24h gültig)
const url = await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: fileKey,
}),
{ expiresIn: 60 * 60 * 24 }
)
console.log(url)
console.log(url)
// 5) In Supabase-DB speichern
const { data, error } = await server.supabase
.from("exports")
.insert([
{
tenant_id: req.user.tenant_id,
start_date: startDate,
end_date: endDate,
valid_until: dayjs().add(24,"hours").toISOString(),
file_path: fileKey,
url: url,
created_at: new Date().toISOString(),
},
])
.select()
.single()
console.log(data)
console.log(error)
} catch (error) {
console.log(error)
}
// 5) In Supabase-DB speichern
const { data, error } = await server.supabase
.from("exports")
.insert([
{
tenant_id: req.user.tenant_id,
start_date: startDate,
end_date: endDate,
valid_until: dayjs().add(24,"hours").toISOString(),
file_path: fileKey,
url: url,
created_at: new Date().toISOString(),
},
])
.select()
.single()
console.log(data)
console.log(error)
}

View File

@@ -461,10 +461,9 @@ export default async function resourceRoutes(server: FastifyInstance) {
}
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
.insert(table)
.values(createData)
@@ -513,8 +512,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
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) => {
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])
}
})

View File

@@ -22,7 +22,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
server.post("/staff/time/event", async (req, reply) => {
try {
const userId = req.user.user_id
const actorId = req.user.user_id;
const tenantId = req.user.tenant_id
const body = req.body as any
@@ -35,17 +35,15 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
const dataToInsert = {
tenant_id: tenantId,
user_id: userId,
user_id: body.user_id,
actortype: "user",
actoruser_id: userId,
actoruser_id: actorId,
eventtime: normalizeDate(body.eventtime),
eventtype: body.eventtype,
source: "WEB",
payload: body.payload // Payload (z.B. Description) mit speichern
}
console.log(dataToInsert)
const [created] = await server.db
.insert(stafftimeevents)
//@ts-ignore
@@ -62,17 +60,17 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
// 🆕 POST /staff/time/edit (Bearbeiten durch Invalidieren + Neu erstellen)
server.post("/staff/time/edit", async (req, reply) => {
try {
const userId = req.user.user_id;
// 1. Der "Actor" ist der, der gerade eingeloggt ist (z.B. Manager)
const actorId = req.user.user_id;
const tenantId = req.user.tenant_id;
// Wir erwarten das komplette Paket für die Änderung
const {
originalEventIds, // Array der IDs, die "gelöscht" werden sollen (Start ID, End ID)
newStart, // ISO String
newEnd, // ISO String
newType, // z.B. 'work', 'vacation'
originalEventIds,
newStart,
newEnd,
newType,
description,
reason // Warum wurde geändert? (Audit)
reason
} = req.body as {
originalEventIds: string[],
newStart: string,
@@ -86,41 +84,66 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
return reply.code(400).send({ error: "Keine Events zum Bearbeiten angegeben." });
}
// 1. Transaction starten (damit alles oder nichts passiert)
// -----------------------------------------------------------
// SCHRITT A: Den eigentlichen Besitzer (Mitarbeiter) ermitteln
// -----------------------------------------------------------
// Wir holen uns das erste Event aus der Liste, um zu sehen, wem es gehört.
const existingEvents = await server.db
.select({
user_id: stafftimeevents.user_id,
tenant_id: stafftimeevents.tenant_id
})
.from(stafftimeevents)
.where(and(
eq(stafftimeevents.id, originalEventIds[0]),
eq(stafftimeevents.tenant_id, tenantId) // Sicherheitscheck: Nur im eigenen Tenant
))
.limit(1);
if (existingEvents.length === 0) {
return reply.code(404).send({ error: "Ursprüngliches Event nicht gefunden oder Zugriff verweigert." });
}
// Das ist der Mitarbeiter, dem die Zeit gehört
const targetUserId = existingEvents[0].user_id;
// -----------------------------------------------------------
// SCHRITT B: Transaktion durchführen
// -----------------------------------------------------------
await server.db.transaction(async (tx) => {
// A. INVALIDIEREN (Die alten Events "löschen")
// Wir erstellen für jedes alte Event ein 'invalidated' Event
// 1. INVALIDIEREN
// Wir nutzen 'targetUserId' als Besitzer des Events, aber 'actorId' als Auslöser
const invalidations = originalEventIds.map(id => ({
tenant_id: tenantId,
user_id: userId, // Gehört dem Mitarbeiter
user_id: targetUserId, // <--- WICHTIG: Gehört dem Mitarbeiter
actortype: "user",
actoruser_id: userId, // Wer hat geändert?
actoruser_id: actorId, // <--- WICHTIG: Geändert durch Manager/Self
eventtime: new Date(),
eventtype: "invalidated", // <--- NEUER TYP: Muss in loadValidEvents gefiltert werden!
eventtype: "invalidated",
source: "WEB",
related_event_id: id, // Zeigt auf das alte Event
related_event_id: id,
metadata: {
reason: reason || "Bearbeitung",
replaced_by_edit: true
}
}));
// Batch Insert
// @ts-ignore
await tx.insert(stafftimeevents).values(invalidations);
// B. NEU ERSTELLEN (Die korrigierten Events anlegen)
// 2. NEU ERSTELLEN
// Start Event
// @ts-ignore
await tx.insert(stafftimeevents).values({
tenant_id: tenantId,
user_id: userId,
user_id: targetUserId, // <--- Gehört dem Mitarbeiter
actortype: "user",
actoruser_id: userId,
actoruser_id: actorId, // <--- Erstellt durch Manager/Self
eventtime: new Date(newStart),
eventtype: `${newType}_start`, // z.B. work_start
eventtype: `${newType}_start`,
source: "WEB",
payload: { description: description || "" }
});
@@ -130,11 +153,11 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
// @ts-ignore
await tx.insert(stafftimeevents).values({
tenant_id: tenantId,
user_id: userId,
user_id: targetUserId, // <--- Gehört dem Mitarbeiter
actortype: "user",
actoruser_id: userId,
actoruser_id: actorId, // <--- Erstellt durch Manager/Self
eventtime: new Date(newEnd),
eventtype: `${newType}_end`, // z.B. work_end
eventtype: `${newType}_end`,
source: "WEB"
});
}
@@ -365,7 +388,9 @@ export default async function staffTimeRoutes(server: FastifyInstance) {
const evaluatedUserId = targetUserId || actingUserId;
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())) {
return reply.code(400).send({ error: "Ungültiges Datumsformat." });

View File

@@ -1,327 +1,389 @@
import xmlbuilder from "xmlbuilder";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween.js"
import {BlobWriter, Data64URIReader, TextReader, TextWriter, ZipWriter} from "@zip.js/zip.js";
import {FastifyInstance} from "fastify";
import {GetObjectCommand} from "@aws-sdk/client-s3";
import {s3} from "../s3";
import {secrets} from "../secrets";
dayjs.extend(isBetween)
import isBetween from "dayjs/plugin/isBetween.js";
import { BlobWriter, Data64URIReader, TextReader, ZipWriter } from "@zip.js/zip.js";
import { FastifyInstance } from "fastify";
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { s3 } from "../s3";
import { secrets } from "../secrets";
const getCreatedDocumentTotal = (item) => {
let totalNet = 0
let total19 = 0
let total7 = 0
// Drizzle Core Imports
import { eq, and, inArray, gte, lte, asc, aliasedTable } from "drizzle-orm";
item.rows.forEach(row => {
if(!['pagebreak','title','text'].includes(row.mode)){
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3)
totalNet = totalNet + Number(rowPrice)
// Tabellen Imports (keine Relations nötig!)
import {
statementallocations,
createddocuments,
incominginvoices,
accounts,
files,
customers,
vendors,
bankaccounts,
bankstatements,
ownaccounts
} from "../../../db/schema";
if(row.taxPercent === 19) {
// @ts-ignore
total19 = total19 + Number(rowPrice * 0.19)
} else if(row.taxPercent === 7) {
// @ts-ignore
total7 = total7 + Number(rowPrice * 0.07)
}
dayjs.extend(isBetween);
// ---------------------------------------------------------
// HELPER FUNCTIONS (Unverändert)
// ---------------------------------------------------------
const getCreatedDocumentTotal = (item: any) => {
let totalNet = 0;
let total19:number = 0;
let total7:number = 0;
const rows = Array.isArray(item.rows) ? item.rows : [];
rows.forEach((row: any) => {
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)).toFixed(3);
totalNet = totalNet + Number(rowPrice);
if (row.taxPercent === 19) total19 += Number(rowPrice) * Number(0.19);
else if (row.taxPercent === 7) total7 += Number(rowPrice) * Number(0.07);
}
})
let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
});
return {
totalNet: totalNet,
total19: total19,
total7: total7,
totalGross: totalGross,
}
}
totalNet, total19, total7,
totalGross: Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
};
};
const escapeString = (str) => {
const escapeString = (str: string | null | undefined) => {
return (str || "").replaceAll("\n", "").replaceAll(";", "").replaceAll(/\r/g, "").replaceAll(/"/g, "").replaceAll(/ü/g, "ue").replaceAll(/ä/g, "ae").replaceAll(/ö/g, "oe");
};
str = (str ||"")
.replaceAll("\n","")
.replaceAll(";","")
.replaceAll(/\r/g,"")
.replaceAll(/"/g,"")
.replaceAll(/ü/g,"ue")
.replaceAll(/ä/g,"ae")
.replaceAll(/ö/g,"oe")
return str
}
const displayCurrency = (input: number, onlyAbs = false) => {
return (onlyAbs ? Math.abs(input) : input).toFixed(2).replace(".", ",");
};
const displayCurrency = (input, onlyAbs = false) => {
// ---------------------------------------------------------
// MAIN EXPORT FUNCTION
// ---------------------------------------------------------
if(onlyAbs) {
return Math.abs(input).toFixed(2).replace(".",",")
} else {
return input.toFixed(2).replace(".",",")
}
}
export async function buildExportZip(server: FastifyInstance, tenant: number, startDate: string, endDate: string, beraternr: string, mandantennr: string): Promise<Buffer> {
export async function buildExportZip(
server: FastifyInstance,
tenantId: number,
startDate: string,
endDate: string,
beraternr: string,
mandantennr: string
): Promise<Buffer> {
try {
const zipFileWriter = new BlobWriter()
const zipWriter = new ZipWriter(zipFileWriter)
const zipFileWriter = new BlobWriter();
const zipWriter = new ZipWriter(zipFileWriter);
// Header Infos
const dateNowStr = dayjs().format("YYYYMMDDHHmmssSSS");
const startDateFmt = dayjs(startDate).format("YYYYMMDD");
const endDateFmt = dayjs(endDate).format("YYYYMMDD");
let header = `"EXTF";700;21;"Buchungsstapel";13;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`;
let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto`;
//Basic Information
// ---------------------------------------------------------
// 1. DATEN LADEN (CORE API SELECT & JOIN)
// ---------------------------------------------------------
let header = `"EXTF";700;21;"Buchungsstapel";13;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
// --- A) Created Documents ---
// Wir brauchen das Dokument und den Kunden dazu
const cdRaw = await server.db.select({
doc: createddocuments,
customer: customers
})
.from(createddocuments)
.leftJoin(customers, eq(createddocuments.customer, customers.id))
.where(and(
eq(createddocuments.tenant, tenantId),
inArray(createddocuments.type, ["invoices", "advanceInvoices", "cancellationInvoices"]),
eq(createddocuments.state, "Gebucht"),
eq(createddocuments.archived, false),
gte(createddocuments.documentDate, startDate),
lte(createddocuments.documentDate, endDate)
));
let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto`
// Mapping: Flat Result -> Nested Object (damit der Rest des Codes gleich bleiben kann)
const createddocumentsList = cdRaw.map(r => ({
...r.doc,
customer: r.customer
}));
//Get Bookings
const {data:statementallocationsRaw,error: statementallocationsError} = await server.supabase.from("statementallocations").select('*, account(*), bs_id(*, account(*)), cd_id(*,customer(*)), ii_id(*, vendor(*)), vendor(*), customer(*), ownaccount(*)').eq("tenant", tenant);
let {data:createddocumentsRaw,error: createddocumentsError} = await server.supabase.from("createddocuments").select('*,customer(*)').eq("tenant", tenant).in("type",["invoices","advanceInvoices","cancellationInvoices"]).eq("state","Gebucht").eq("archived",false)
let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant).eq("state","Gebucht").eq("archived",false)
const {data:accounts} = await server.supabase.from("accounts").select()
const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single()
// --- B) Incoming Invoices ---
// Wir brauchen die Rechnung und den Lieferanten
const iiRaw = await server.db.select({
inv: incominginvoices,
vendor: vendors
})
.from(incominginvoices)
.leftJoin(vendors, eq(incominginvoices.vendor, vendors.id))
.where(and(
eq(incominginvoices.tenant, tenantId),
eq(incominginvoices.state, "Gebucht"),
eq(incominginvoices.archived, false),
gte(incominginvoices.date, startDate),
lte(incominginvoices.date, endDate)
));
let createddocuments = createddocumentsRaw.filter(i => dayjs(i.documentDate).isBetween(startDate,endDate,"day","[]"))
let incominginvoices = incominginvoicesRaw.filter(i => dayjs(i.date).isBetween(startDate,endDate,"day","[]"))
let statementallocations = statementallocationsRaw.filter(i => dayjs(i.bs_id.date).isBetween(startDate,endDate,"day","[]"))
const incominginvoicesList = iiRaw.map(r => ({
...r.inv,
vendor: r.vendor
}));
// --- C) Statement Allocations ---
// Das ist der komplexeste Teil. Wir müssen Tabellen aliasen, da wir z.B. Customers doppelt joinen könnten
// (Einmal via CreatedDocument, einmal direkt an der Allocation).
const {data:filesCreateddocuments, error: filesErrorCD} = await server.supabase.from("files").select().eq("tenant",tenant).or(`createddocument.in.(${createddocuments.map(i => i.id).join(",")})`)
const {data:filesIncomingInvoices, error: filesErrorII} = await server.supabase.from("files").select().eq("tenant",tenant).or(`incominginvoice.in.(${incominginvoices.map(i => i.id).join(",")})`)
const CdCustomer = aliasedTable(customers, "cd_customer");
const IiVendor = aliasedTable(vendors, "ii_vendor");
const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => {
const allocRaw = await server.db.select({
allocation: statementallocations,
bs: bankstatements,
ba: bankaccounts,
cd: createddocuments,
cd_cust: CdCustomer,
ii: incominginvoices,
ii_vend: IiVendor,
acc: accounts,
direct_vend: vendors, // Direkte Zuordnung an Kreditor
direct_cust: customers, // Direkte Zuordnung an Debitor
own: ownaccounts
})
.from(statementallocations)
// JOIN 1: Bankstatement (Pflicht, für Datum Filter)
.innerJoin(bankstatements, eq(statementallocations.bankstatement, bankstatements.id))
// JOIN 2: Bankaccount (für DATEV Nummer)
.leftJoin(bankaccounts, eq(bankstatements.account, bankaccounts.id))
console.log(filePath)
// JOIN 3: Ausgangsrechnung & deren Kunde
.leftJoin(createddocuments, eq(statementallocations.createddocument, createddocuments.id))
.leftJoin(CdCustomer, eq(createddocuments.customer, CdCustomer.id))
const command = new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: filePath,
})
// JOIN 4: Eingangsrechnung & deren Lieferant
.leftJoin(incominginvoices, eq(statementallocations.incominginvoice, incominginvoices.id))
.leftJoin(IiVendor, eq(incominginvoices.vendor, IiVendor.id))
const { Body, ContentType } = await s3.send(command)
// JOIN 5: Direkte Zuordnungen
.leftJoin(accounts, eq(statementallocations.account, accounts.id))
.leftJoin(vendors, eq(statementallocations.vendor, vendors.id))
.leftJoin(customers, eq(statementallocations.customer, customers.id))
.leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id))
const chunks: any[] = []
// @ts-ignore
for await (const chunk of Body) {
chunks.push(chunk)
.where(and(
eq(statementallocations.tenant, tenantId),
eq(statementallocations.archived, false),
// Datum Filter direkt auf dem Bankstatement
gte(bankstatements.date, startDate),
lte(bankstatements.date, endDate)
));
// Mapping: Wir bauen das komplexe Objekt nach, das die CSV Logik erwartet
const statementallocationsList = allocRaw.map(r => ({
...r.allocation,
bankstatement: {
...r.bs,
account: r.ba // Nesting für bs.account.datevNumber
},
createddocument: r.cd ? {
...r.cd,
customer: r.cd_cust
} : null,
incominginvoice: r.ii ? {
...r.ii,
vendor: r.ii_vend
} : null,
account: r.acc,
vendor: r.direct_vend,
customer: r.direct_cust,
ownaccount: r.own
}));
// --- D) Stammdaten Accounts ---
const accountsList = await server.db.select().from(accounts);
// ---------------------------------------------------------
// 2. FILES LADEN
// ---------------------------------------------------------
// IDs sammeln für IN (...) Abfragen
const cdIds = createddocumentsList.map(i => i.id);
const iiIds = incominginvoicesList.map(i => i.id);
let filesCreateddocuments: any[] = [];
if (cdIds.length > 0) {
filesCreateddocuments = await server.db.select().from(files).where(and(
eq(files.tenant, tenantId),
inArray(files.createddocument, cdIds),
eq(files.archived, false)
));
}
let filesIncomingInvoices: any[] = [];
if (iiIds.length > 0) {
filesIncomingInvoices = await server.db.select().from(files).where(and(
eq(files.tenant, tenantId),
inArray(files.incominginvoice, iiIds),
eq(files.archived, false)
));
}
// ---------------------------------------------------------
// 3. DOWNLOAD & ZIP
// ---------------------------------------------------------
const downloadFile = async (filePath: string, fileId: string) => {
try {
const command = new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: filePath,
});
const { Body } = await s3.send(command);
if (!Body) return;
const chunks: any[] = [];
// @ts-ignore
for await (const chunk of Body) chunks.push(chunk);
const buffer = Buffer.concat(chunks);
const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}`;
const dataURLReader = new Data64URIReader(dataURL);
const ext = filePath.includes('.') ? filePath.split(".").pop() : "pdf";
await zipWriter.add(`${fileId}.${ext}`, dataURLReader);
} catch (e) {
console.error(`Error downloading file ${fileId}`, e);
}
const buffer = Buffer.concat(chunks)
const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}`
const dataURLReader = new Data64URIReader(dataURL)
await zipWriter.add(`${fileId}.${downloadFilePath.split(".").pop()}`, dataURLReader)
//await fs.writeFile(`./output/${fileId}.${downloadFilePath.split(".").pop()}`, buffer, () => {});
console.log(`File added to Zip`);
};
for (const file of filesCreateddocuments) {
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
}
for (const file of filesIncomingInvoices) {
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
}
for (const file of filesCreateddocuments) if(file.path) await downloadFile(file.path, file.id);
for (const file of filesIncomingInvoices) if(file.path) await downloadFile(file.path, file.id);
let bookingLines = []
// ---------------------------------------------------------
// 4. CSV GENERIERUNG (Logic ist gleich geblieben)
// ---------------------------------------------------------
createddocuments.forEach(createddocument => {
let bookingLines: string[] = [];
let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id);
// AR
createddocumentsList.forEach(cd => {
let file = filesCreateddocuments.find(i => i.createddocument === cd.id);
let total = 0;
let typeString = "";
let total = 0
let typeString = ""
if(createddocument.type === "invoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
console.log()
if(createddocument.usedAdvanceInvoices.length > 0){
createddocument.usedAdvanceInvoices.forEach(usedAdvanceInvoice => {
total -= getCreatedDocumentTotal(createddocumentsRaw.find(i => i.id === usedAdvanceInvoice)).totalGross
})
}
console.log(total)
typeString = "AR"
} else if(createddocument.type === "advanceInvoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
typeString = "ARAbschlag"
} else if(createddocument.type === "cancellationInvoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
typeString = "ARStorno"
if(cd.type === "invoices") {
total = getCreatedDocumentTotal(cd).totalGross;
typeString = "AR";
} else if(cd.type === "advanceInvoices") {
total = getCreatedDocumentTotal(cd).totalGross;
typeString = "ARAbschlag";
} else if(cd.type === "cancellationInvoices") {
total = getCreatedDocumentTotal(cd).totalGross;
typeString = "ARStorno";
}
let shSelector = "S"
if(Math.sign(total) === 1) {
shSelector = "S"
} else if (Math.sign(total) === -1) {
shSelector = "H"
let shSelector = Math.sign(total) === -1 ? "H" : "S";
const cust = cd.customer; // durch Mapping verfügbar
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};8400;"";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`${typeString} ${cd.documentNumber} - ${cust?.name || ""}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${cust?.name || ""}";"Kundennummer";"${cust?.customerNumber || ""}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
});
// ER
incominginvoicesList.forEach(ii => {
const accs = ii.accounts as any[] || [];
accs.forEach(account => {
let file = filesIncomingInvoices.find(i => i.incominginvoice === ii.id);
let accountData = accountsList.find(i => i.id === account.account);
if (!accountData) return;
let buschluessel = "9";
if(account.taxType === '19') buschluessel = "9";
else if(account.taxType === 'null') buschluessel = "";
else if(account.taxType === '7') buschluessel = "8";
else if(account.taxType === '19I') buschluessel = "19";
else if(account.taxType === '7I') buschluessel = "18";
else buschluessel = "-";
let amountGross = account.amountGross ? account.amountGross : (account.amountNet || 0) + (account.amountTax || 0);
let shSelector = Math.sign(amountGross) === -1 ? "H" : "S";
let text = `ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59);
const vend = ii.vendor; // durch Mapping verfügbar
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
});
});
// Bank
statementallocationsList.forEach(alloc => {
const bs = alloc.bankstatement; // durch Mapping verfügbar
if(!bs) return;
let shSelector = Math.sign(alloc.amount) === -1 ? "H" : "S";
// @ts-ignore
let datevKonto = bs.account?.datevNumber || "";
let dateVal = dayjs(bs.date).format("DDMM");
let dateFull = dayjs(bs.date).format("DD.MM.YYYY");
let bsText = escapeString(bs.text);
if(alloc.createddocument && alloc.createddocument.customer) {
const cd = alloc.createddocument;
const cust = cd.customer;
bookingLines.push(`${displayCurrency(alloc.amount,true)};"H";;;;;${cust?.customerNumber};${datevKonto};"3";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`ZE${alloc.description}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${cust?.name}";"Kundennummer";"${cust?.customerNumber}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
} else if(alloc.incominginvoice && alloc.incominginvoice.vendor) {
const ii = alloc.incominginvoice;
const vend = ii.vendor;
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend?.vendorNumber};"";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${`ZA${alloc.description} ${bsText} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${vend?.name}";"Kundennummer";"${vend?.vendorNumber}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
} else if(alloc.account) {
const acc = alloc.account;
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${acc.number};"";${dateVal};"";;;"${`${vorzeichen} ${acc.number} - ${escapeString(acc.label)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${bs.credName || ''}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
} else if(alloc.vendor) {
const vend = alloc.vendor;
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend.vendorNumber};"";${dateVal};"";;;"${`${vorzeichen} ${vend.vendorNumber} - ${escapeString(vend.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${vend.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
} else if(alloc.customer) {
const cust = alloc.customer;
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${cust.customerNumber};"";${dateVal};"";;;"${`${vorzeichen} ${cust.customerNumber} - ${escapeString(cust.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${cust.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
} else if(alloc.ownaccount) {
const own = alloc.ownaccount;
let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";
bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${own.number};"";${dateVal};"";;;"${`${vorzeichen} ${own.number} - ${escapeString(own.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${own.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
}
});
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${createddocument.customer.customerNumber};8400;"";${dayjs(createddocument.documentDate).format("DDMM")};"${createddocument.documentNumber}";;;"${`${typeString} ${createddocument.documentNumber} - ${createddocument.customer.name}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${createddocument.customer.name}";"Kundennummer";"${createddocument.customer.customerNumber}";"Belegnummer";"${createddocument.documentNumber}";"Leistungsdatum";"${dayjs(createddocument.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(createddocument.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
// ---------------------------------------------------------
// 5. STAMMDATEN CSV
// ---------------------------------------------------------
const csvString = `${header}\n${colHeaders}\n` + bookingLines.join("\n") + "\n";
await zipWriter.add(
`EXTF_Buchungsstapel_von_${startDateFmt}_bis_${endDateFmt}.csv`,
new TextReader(csvString)
);
})
const headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`;
const colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist`;
incominginvoices.forEach(incominginvoice => {
console.log(incominginvoice.id);
incominginvoice.accounts.forEach(account => {
const customersList = await server.db.select().from(customers).where(and(eq(customers.tenant, tenantId), eq(customers.active, true))).orderBy(asc(customers.customerNumber));
const vendorsList = await server.db.select().from(vendors).where(and(eq(vendors.tenant, tenantId), eq(vendors.archived, false))).orderBy(asc(vendors.vendorNumber));
let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id);
let bookinglinesStammdaten: string[] = [];
customersList.forEach(c => {
const info = c.infoData as any || {};
bookinglinesStammdaten.push(`${c.customerNumber};"${c.isCompany ? (c.name || "").substring(0,48): ''}";;"${!c.isCompany ? (c.lastname ? c.lastname : c.name) : ''}";"${!c.isCompany ? (c.firstname ? c.firstname : '') : ''}";;${c.isCompany ? 2 : 1};;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`);
});
let accountData = accounts.find(i => i.id === account.account)
let buschluessel: string = "9"
if(account.taxType === '19'){
buschluessel = "9"
} else if(account.taxType === 'null') {
buschluessel = ""
} else if(account.taxType === '7') {
buschluessel = "8"
} else if(account.taxType === '19I') {
buschluessel = "19"
} else if(account.taxType === '7I') {
buschluessel = "18"
} else {
buschluessel = "-"
}
let shSelector = "S"
let amountGross = account.amountGross ? account.amountGross : account.amountNet + account.amountTax
if(Math.sign(amountGross) === 1) {
shSelector = "S"
} else if(Math.sign(amountGross) === -1) {
shSelector = "H"
}
let text = `ER ${incominginvoice.reference}: ${escapeString(incominginvoice.description)}`.substring(0,59)
console.log(incominginvoice)
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${incominginvoice.vendor.vendorNumber};"${buschluessel}";${dayjs(incominginvoice.date).format("DDMM")};"${incominginvoice.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${incominginvoice.vendor.name}";"Kundennummer";"${incominginvoice.vendor.vendorNumber}";"Belegnummer";"${incominginvoice.reference}";"Leistungsdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
})
})
statementallocations.forEach(statementallocation => {
let shSelector = "S"
if(Math.sign(statementallocation.amount) === 1) {
shSelector = "S"
} else if(Math.sign(statementallocation.amount) === -1) {
shSelector = "H"
}
if(statementallocation.cd_id) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"H";;;;;${statementallocation.cd_id.customer.customerNumber};${statementallocation.bs_id.account.datevNumber};"3";${dayjs(statementallocation.cd_id.documentDate).format("DDMM")};"${statementallocation.cd_id.documentNumber}";;;"${`ZE${statementallocation.description}${escapeString(statementallocation.bs_id.text)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.cd_id.customer.name}";"Kundennummer";"${statementallocation.cd_id.customer.customerNumber}";"Belegnummer";"${statementallocation.cd_id.documentNumber}";"Leistungsdatum";"${dayjs(statementallocation.cd_id.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.cd_id.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.ii_id) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ii_id.vendor.vendorNumber};"";${dayjs(statementallocation.ii_id.date).format("DDMM")};"${statementallocation.ii_id.reference}";;;"${`ZA${statementallocation.description} ${escapeString(statementallocation.bs_id.text)} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ii_id.vendor.name}";"Kundennummer";"${statementallocation.ii_id.vendor.vendorNumber}";"Belegnummer";"${statementallocation.ii_id.reference}";"Leistungsdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.account) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.account.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.account.number} - ${escapeString(statementallocation.account.label)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.bs_id.credName}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.vendor) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.vendor.vendorNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.vendor.vendorNumber} - ${escapeString(statementallocation.vendor.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.vendor.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.customer) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.customer.customerNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.customer.customerNumber} - ${escapeString(statementallocation.customer.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.customer.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.ownaccount) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ownaccount.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.ownaccount.number} - ${escapeString(statementallocation.ownaccount.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ownaccount.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
}
})
let csvString = `${header}\n${colHeaders}\n`;
bookingLines.forEach(line => {
csvString += `${line}\n`;
})
const buchungsstapelReader = new TextReader(csvString)
await zipWriter.add(`EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, buchungsstapelReader)
/*fs.writeFile(`output/EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvString, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
// Kreditoren/Debitoren
let headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
let colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist`
const {data:customers} = await server.supabase.from("customers").select().eq("tenant",tenant).order("customerNumber")
const {data:vendors} = await server.supabase.from("vendors").select().eq("tenant",tenant).order("vendorNumber")
let bookinglinesStammdaten = []
customers.forEach(customer => {
bookinglinesStammdaten.push(`${customer.customerNumber};"${customer.isCompany ? customer.name.substring(0,48): ''}";;"${!customer.isCompany ? (customer.lastname ? customer.lastname : customer.name) : ''}";"${!customer.isCompany ? (customer.firstname ? customer.firstname : '') : ''}";;${customer.isCompany ? 2 : 1};;;;;;;;"STR";"${customer.infoData.street ? customer.infoData.street : ''}";;"${customer.infoData.zip ? customer.infoData.zip : ''}";"${customer.infoData.city ? customer.infoData.city : ''}";;;"${customer.infoData.special ? customer.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
})
vendors.forEach(vendor => {
bookinglinesStammdaten.push(`${vendor.vendorNumber};"${vendor.name.substring(0,48)}";;;;;2;;;;;;;;"STR";"${vendor.infoData.street ? vendor.infoData.street : ''}";;"${vendor.infoData.zip ? vendor.infoData.zip : ''}";"${vendor.infoData.city ? vendor.infoData.city : ''}";;;"${vendor.infoData.special ? vendor.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
})
let csvStringStammdaten = `${headerStammdaten}\n${colHeadersStammdaten}\n`;
bookinglinesStammdaten.forEach(line => {
csvStringStammdaten += `${line}\n`;
})
const stammdatenReader = new TextReader(csvStringStammdaten)
await zipWriter.add(`EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, stammdatenReader)
/*fs.writeFile(`output/EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringStammdaten, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
//Sachkonten
let headerSachkonten = `"EXTF";700;20;"Kontenbeschriftungen";3;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Sachkonten";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
let colHeadersSachkonten = `Konto;Kontenbeschriftung;Sprach-ID;Kontenbeschriftung lang`
const {data:bankaccounts} = await server.supabase.from("bankaccounts").select().eq("tenant",tenant).order("datevNumber")
let bookinglinesSachkonten = []
bankaccounts.forEach(bankaccount => {
bookinglinesSachkonten.push(`${bankaccount.datevNumber};"${bankaccount.name}";"de-DE";`)
})
let csvStringSachkonten = `${headerSachkonten}\n${colHeadersSachkonten}\n`;
bookinglinesSachkonten.forEach(line => {
csvStringSachkonten += `${line}\n`;
})
const sachkontenReader = new TextReader(csvStringSachkonten)
await zipWriter.add(`EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, sachkontenReader)
/*fs.writeFile(`output/EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringSachkonten, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
vendorsList.forEach(v => {
const info = v.infoData as any || {};
bookinglinesStammdaten.push(`${v.vendorNumber};"${(v.name || "").substring(0,48)}";;;;;2;;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`);
});
await zipWriter.add(
`EXTF_Stammdaten_von_${startDateFmt}_bis_${endDateFmt}.csv`,
new TextReader(`${headerStammdaten}\n${colHeadersStammdaten}\n` + bookinglinesStammdaten.join("\n") + "\n")
);
// ---------------------------------------------------------
// 6. XML METADATA
// ---------------------------------------------------------
let obj = {
archive: {
'@version':"5.0",
@@ -333,56 +395,34 @@ export async function buildExportZip(server: FastifyInstance, tenant: number, st
date: dayjs().format("YYYY-MM-DDTHH:mm:ss")
},
content: {
document: []
document: [] as any[]
}
}
}
};
filesCreateddocuments.forEach(file => {
const addXmlDoc = (file: any) => {
if(!file.path) return;
const ext = file.path.includes('.') ? file.path.split(".").pop() : "pdf";
obj.archive.content.document.push({
"@guid": file.id,
extension: {
"@xsi:type":"File",
"@name":`${file.id}.pdf`
"@name":`${file.id}.${ext}`
}
})
})
});
};
filesIncomingInvoices.forEach(file => {
obj.archive.content.document.push({
"@guid": file.id,
extension: {
"@xsi:type":"File",
"@name":`${file.id}.pdf`
}
})
})
filesCreateddocuments.forEach(addXmlDoc);
filesIncomingInvoices.forEach(addXmlDoc);
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
const doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true});
await zipWriter.add(`document.xml`, new TextReader(doc.end({pretty: true})));
//console.log(doc.end({pretty: true}));
const arrayBuffer = await (await zipWriter.close()).arrayBuffer();
return Buffer.from(arrayBuffer);
const documentsReader = new TextReader(doc.end({pretty: true}))
await zipWriter.add(`document.xml`, documentsReader)
/*function toBuffer(arrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
return buffer;
}*/
const arrayBuffer = await (await zipWriter.close()).arrayBuffer()
return Buffer.from(arrayBuffer)
} catch(error) {
console.log(error)
console.error("DATEV Export Error:", error);
throw error;
}
}

View File

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

View File

@@ -4,9 +4,58 @@ services:
web:
image: reg.federspiel.software/fedeo/software:beta
restart: always
environment:
- INFISICAL_CLIENT_ID=abc
- INFISICAL_CLIENT_SECRET=abc
backend:
image: reg.federspiel.software/fedeo/backend:main
restart: always
environment:
- NUXT_PUBLIC_API_BASE=
- NUXT_PUBLIC_PDF_LICENSE=
db:
image: postgres
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: abc
POSTGRES_USER: sandelcom
POSTGRES_DB: sensorfy
volumes:
- ./pg-data:/var/lib/postgresql/data
ports:
- "5432:5432"
traefik:
image: traefik:v2.2
restart: unless-stopped
container_name: traefik
command:
- "--api.insecure=false"
- "--api.dashboard=true"
- "--api.debug=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=traefik"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secured.address=:443"
- "--accesslog=true"
- "--accesslog.filepath=/logs/access.log"
- "--accesslog.bufferingsize=5000"
- "--accesslog.fields.defaultMode=keep"
- "--accesslog.fields.headers.defaultMode=keep"
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" # <== Enable TLS-ALPN-01 to generate and renew ACME certs
- "--certificatesresolvers.mytlschallenge.acme.email=info@sandelcom.de" # <== Setting email for certs
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json" # <== Defining acme file to store cert information
ports:
- 80:80
- 8080:8080
- 443:443
volumes:
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/logs:/logs"
labels:
#### Labels define the behavior and rules of the traefik proxy for this container ####
- "traefik.enable=true" # <== Enable traefik on itself to view dashboard and assign subdomain to view it
- "traefik.http.routers.api.rule=Host(`srv1.drinkingteam.de`)" # <== Setting the domain for the dashboard
- "traefik.http.routers.api.service=api@internal" # <== Enabling the api to be a service to access

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
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
# --- 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_PORT=3000
ENV NODE_ENV=production
EXPOSE 3000
ENTRYPOINT ["node", ".output/server/index.mjs"]
ENTRYPOINT ["node", ".output/server/index.mjs"]

View File

@@ -3,17 +3,7 @@ import * as Sentry from "@sentry/browser"
/*watch(viewport.breakpoint, (newBreakpoint, oldBreakpoint) => {
console.log('Breakpoint updated:', oldBreakpoint, '->', newBreakpoint)
})*/
const platform = ref('default')
const setup = async () => {
if(await useCapacitor().getIsPhone()) {
platform.value = "mobile"
}
const dev = process.dev
console.log(dev)
}
@@ -21,10 +11,10 @@ setup()
Sentry.init({
/*Sentry.init({
dsn: "https://62e62ff08e1a438591fe5eb4dd9de244@glitchtip.federspiel.software/3",
tracesSampleRate: 0.01,
});
});*/
@@ -40,12 +30,12 @@ useHead({
lang: 'de'
},
script: [
{
/*{
defer: true,
src: "/umami.js",
"data-website-id":"2a9782fa-2fdf-4434-981d-93592d39edef",
"data-host-url":"https://umami.federspiel.software"
}
}*/
]
})
@@ -61,8 +51,7 @@ useSeoMeta({
<NuxtLayout>
<NuxtPage/>
</NuxtLayout>
<UNotifications :class="platform === 'mobile' ? ['mb-14'] : []"/>
<UNotifications/>
<USlideovers />
<UModals/>
</div>

View File

@@ -1,16 +0,0 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'software.federspiel.fedeo',
appName: 'FEDEO',
webDir: 'dist',
ios: {
handleApplicationNotifications: false
},
/*server: {
url: "http://192.168.1.226:3000",
cleartext: true
}*/
};
export default config;

View File

@@ -55,7 +55,7 @@ const setup = async () => {
})
filetypes.value = await useEntities("filetags").select()
documentboxes.value = await useEntities("documentboxes").select()
//documentboxes.value = await useEntities("documentboxes").select()
}
setup()
@@ -171,7 +171,7 @@ const moveFile = async () => {
</template>
<div class="flex flex-row">
<div :class="useCapacitor().getIsNative() ? ['w-full'] : ['w-1/3']">
<div :class="false ? ['w-full'] : ['w-1/3']">
<PDFViewer
v-if="props.documentData.id && props.documentData.path.toLowerCase().includes('pdf')"
:file-id="props.documentData.id" />
@@ -183,7 +183,7 @@ const moveFile = async () => {
v-else
/>
</div>
<div class="w-2/3 p-5" v-if="!useCapacitor().getIsNative()">
<div class="w-2/3 p-5" v-if="!false">
<UButtonGroup>
<ArchiveButton
color="rose"

View File

@@ -26,7 +26,6 @@ const platform = ref("default")
const setup = async () => {
if(await useCapacitor().getIsPhone()) platform.value = "mobile"
if(props.type && props.elementId){
items.value = await useNuxtApp().$api(`/api/resource/${props.type}/${props.elementId}/history`)

View File

@@ -37,7 +37,7 @@ const handleClick = async () => {
<UButton
:icon="labelPrinter.connected ? 'i-heroicons-printer' : 'i-heroicons-printer'"
:color="labelPrinter.connected ? 'green' : 'gray'"
:color="labelPrinter.connected ? 'green' : ''"
variant="soft"
class="w-full justify-start"
:loading="labelPrinter.connectLoading"

View File

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

View File

@@ -2,22 +2,19 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
// Wir erwarten eine Prop, die sagt, ob geschützt werden soll
const props = defineProps<{
when: boolean // z.B. true, wenn Formular dirty ist
when: boolean
}>()
const showModal = ref(false)
const pendingNext = ref<null | ((val?: boolean) => void)>(null)
// --- 1. Interne Navigation (Nuxt) ---
// --- 1. Interne Navigation ---
onBeforeRouteLeave((to, from, next) => {
if (!props.when) {
next()
return
}
// Navigation pausieren & Modal zeigen
pendingNext.value = next
showModal.value = true
})
@@ -29,10 +26,10 @@ const confirmLeave = () => {
const cancelLeave = () => {
showModal.value = false
// Navigation wird implizit abgebrochen
// Navigation abbrechen (pendingNext verfällt)
}
// --- 2. Externe Navigation (Browser Tab schließen) ---
// --- 2. Browser Tab schließen ---
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (props.when) {
e.preventDefault()
@@ -50,12 +47,12 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
<div class="guard-modal">
<div class="guard-header">
<slot name="title">Seite wirklich verlassen?</slot>
<slot name="title">Seite verlassen?</slot>
</div>
<div class="guard-body">
<slot>
Du hast ungespeicherte Änderungen. Diese gehen verloren, wenn du die Seite verlässt.
Du hast ungespeicherte Änderungen. Wenn du die Seite verlässt, gehen diese verloren.
</slot>
</div>
@@ -64,7 +61,7 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
Nein, bleiben
</button>
<button @click="confirmLeave" class="btn-confirm">
Ja, verlassen
Ja, verwerfen
</button>
</div>
@@ -74,25 +71,65 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
</template>
<style scoped>
/* Basis-Styling - passe dies an dein Design System an */
/* --- Layout & Animation --- */
.guard-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
position: fixed; inset: 0;
background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
z-index: 9999; backdrop-filter: blur(2px);
z-index: 9999; backdrop-filter: blur(4px);
transition: opacity 0.2s;
}
.guard-modal {
background: white; padding: 24px; border-radius: 12px;
width: 90%; max-width: 400px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
font-family: sans-serif;
}
.guard-header { font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem; }
.guard-body { margin-bottom: 1.5rem; color: #4a5568; }
.guard-actions { display: flex; justify-content: flex-end; gap: 12px; }
/* Buttons */
button { padding: 8px 16px; border-radius: 6px; cursor: pointer; border: none; font-weight: 600;}
.btn-cancel { background: #edf2f7; color: #2d3748; }
.btn-cancel:hover { background: #e2e8f0; }
.btn-confirm { background: #e53e3e; color: white; }
.btn-confirm:hover { background: #c53030; }
.guard-modal {
width: 90%; max-width: 420px;
border-radius: 12px;
padding: 24px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
}
.guard-header {
font-size: 1.125rem; font-weight: 600; margin-bottom: 0.75rem;
}
.guard-body {
margin-bottom: 1.5rem; font-size: 0.95rem; line-height: 1.5;
}
.guard-actions {
display: flex; justify-content: flex-end; gap: 12px;
}
/* --- Buttons Basis --- */
button {
padding: 8px 16px; border-radius: 6px; cursor: pointer; border: none; font-weight: 500; font-size: 0.9rem;
transition: background-color 0.2s;
}
/* --------------------------------------------------------- */
/* LIGHT MODE (Default) */
/* --------------------------------------------------------- */
.guard-modal {
background: white;
color: #1f2937; /* gray-800 */
}
.guard-body {
color: #4b5563; /* gray-600 */
}
.btn-cancel {
background: #f3f4f6; /* gray-100 */
color: #374151; /* gray-700 */
}
.btn-cancel:hover { background: #e5e7eb; }
.btn-confirm {
background: #ef4444; /* red-500 */
color: white;
}
.btn-confirm:hover { background: #dc2626; }
</style>

View File

@@ -159,7 +159,16 @@ const submit = async () => {
@input="e => form.startDate = new Date(e.target.value)"
/>
</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
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"

View File

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

View File

@@ -42,9 +42,7 @@ const setupPage = async () => {
})
draftInvoicesCount.value = draftDocuments.length
countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet").length
countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet" && !i.archived).length
}
setupPage()
@@ -78,10 +76,10 @@ setupPage()
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk / 0,00</td>
</tr>
<tr>
<td class="break-all">ToDo Eingangsrechnungsrechnungen:</td>
<td class="break-all">Vorbereitete Eingangsrechnungen:</td>
<td
v-if="countPreparedOpenIncomingInvoices > 0"
class="text-orange-500 font-bold text-nowrap"
class="text-orange-500 font-bold text-wrap"
>{{countPreparedOpenIncomingInvoices}} Stk </td>
<td v-else class="text-primary-500 font-bold text-no-wrap">0 Stk</td>
</tr>

View File

@@ -15,14 +15,8 @@ const props = defineProps({
*/
async function openLink(link) {
if (link.external) {
if (useCapacitor().getIsNative()) {
await Browser.open({
url: link.to,
presentationStyle: "popover",
})
} else {
window.open(link.to, "_blank")
}
window.open(link.to, "_blank")
} else {
return navigateTo(link.to)
}

View File

@@ -13,11 +13,9 @@ const platform = ref("default")
const setupPage = async () => {
runningTimeInfo.value = (await supabase.from("times").select().eq("profile", profileStore.activeProfile.id).is("endDate", null).single()).data || {}
projects.value = (await useSupabaseSelect("projects"))
//projects.value = (await useSupabaseSelect("projects"))
if(await useCapacitor().getIsPhone()) {
platform.value = "mobile"
}
}

View File

@@ -1,32 +0,0 @@
import {Capacitor} from "@capacitor/core";
import {Device} from "@capacitor/device";
import {Network} from "@capacitor/network";
const override = false
export const useCapacitor = () => {
const getPlatform = () => {
return Capacitor.getPlatform()
}
const getDeviceInfo = async () => {
return await Device.getInfo()
}
const getIsPhone = async () => {
let deviceInfo = await useCapacitor().getDeviceInfo()
return override || deviceInfo.model.toLowerCase().includes('iphone')
}
const getIsNative = () => {
return override || Capacitor.isNativePlatform()
}
const getNetworkStatus = async () => {
return await Network.getStatus()
}
return {getPlatform, getDeviceInfo, getNetworkStatus, getIsPhone, getIsNative}
}

View File

@@ -91,15 +91,18 @@ export const useStaffTime = () => {
}
// 🆕 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
// Wir nutzen den dynamischen Typ (work_start, vacation_start etc.)
console.log(data)
await $api('/api/staff/time/event', {
method: 'POST',
body: {
eventtype: `${data.type}_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',
body: {
eventtype: `${data.type}_end`,
eventtime: data.end
eventtime: data.end,
payload: { description: data.description },
user_id: data.user_id,
}
})
}

View File

@@ -1,13 +0,0 @@
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
# Generated Config files
App/App/capacitor.config.json
App/App/config.xml

View File

@@ -1,641 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
7E144E961F6CA2C63512098E /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DF9D76ED77CB578563C2573 /* Pods_OneSignalNotificationServiceExtension.framework */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
D5A301A42D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D5A3019D2D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
D5A301A22D970BAC002A22E9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
proxyType = 1;
remoteGlobalIDString = D5A3019C2D970BAC002A22E9;
remoteInfo = OneSignalNotificationServiceExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
D5A301A92D970BAC002A22E9 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
D5A301A42D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
37F7155EDCE8C061367E30A9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
4DF9D76ED77CB578563C2573 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
6CB294319AEF8406BACB8AC1 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
D5A301982D970B67002A22E9 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
D5A3019D2D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
D5A301A52D970BAC002A22E9 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = D5A3019C2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
D5A3019E2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
D5A301A52D970BAC002A22E9 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */,
);
path = OneSignalNotificationServiceExtension;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D5A3019A2D970BAC002A22E9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7E144E961F6CA2C63512098E /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
4DF9D76ED77CB578563C2573 /* Pods_OneSignalNotificationServiceExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
D5A3019E2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
D5A3019D2D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
D5A301982D970B67002A22E9 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
37F7155EDCE8C061367E30A9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */,
6CB294319AEF8406BACB8AC1 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
D5A301A92D970BAC002A22E9 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
D5A301A32D970BAC002A22E9 /* PBXTargetDependency */,
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
D5A3019C2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = D5A301A62D970BAC002A22E9 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
buildPhases = (
D76E39AEACB5B9B2BDC681BF /* [CP] Check Pods Manifest.lock */,
D5A301992D970BAC002A22E9 /* Sources */,
D5A3019A2D970BAC002A22E9 /* Frameworks */,
D5A3019B2D970BAC002A22E9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
D5A3019E2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */,
);
name = OneSignalNotificationServiceExtension;
productName = OneSignalNotificationServiceExtension;
productReference = D5A3019D2D970BAC002A22E9 /* OneSignalNotificationServiceExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
D5A3019C2D970BAC002A22E9 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
preferredProjectObjectVersion = 77;
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
D5A3019C2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D5A3019B2D970BAC002A22E9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
D76E39AEACB5B9B2BDC681BF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-OneSignalNotificationServiceExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D5A301992D970BAC002A22E9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
D5A301A32D970BAC002A22E9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D5A3019C2D970BAC002A22E9 /* OneSignalNotificationServiceExtension */;
targetProxy = D5A301A22D970BAC002A22E9 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
D5A301A72D970BAC002A22E9 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 37F7155EDCE8C061367E30A9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D5A301A82D970BAC002A22E9 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6CB294319AEF8406BACB8AC1 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GMCGQ8KK2P;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = software.federspiel.fedeo.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D5A301A62D970BAC002A22E9 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D5A301A72D970BAC002A22E9 /* Debug */,
D5A301A82D970BAC002A22E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:App.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.software.federspiel.fedeo.onesignal</string>
</array>
</dict>
</plist>

View File

@@ -1,49 +0,0 @@
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,14 +0,0 @@
{
"images": [
{
"idiom": "universal",
"size": "1024x1024",
"filename": "AppIcon-512@2x.png",
"platform": "ios"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,56 +0,0 @@
{
"images": [
{
"idiom": "universal",
"filename": "Default@1x~universal~anyany.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "Default@2x~universal~anyany.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "Default@3x~universal~anyany.png",
"scale": "3x"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "universal",
"scale": "1x",
"filename": "Default@1x~universal~anyany-dark.png"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "universal",
"scale": "2x",
"filename": "Default@2x~universal~anyany-dark.png"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"idiom": "universal",
"scale": "3x",
"filename": "Default@3x~universal~anyany-dark.png"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</imageView>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
</dependencies>
<scenes>
<!--Bridge View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>de</string>
<key>CFBundleDisplayName</key>
<string>FEDEO</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fedeo</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationAlwaysUsageDescription</key>
<string>One Signal Notifications</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>One Signal Notifications</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View File

@@ -1,35 +0,0 @@
import UserNotifications
import OneSignalExtension
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var receivedRequest: UNNotificationRequest!
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.receivedRequest = request
self.contentHandler = contentHandler
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
/* DEBUGGING: Uncomment the 2 lines below to check this extension is executing
Note, this extension only runs when mutable-content is set
Setting an attachment or action buttons automatically adds this */
// print("Running NotificationServiceExtension")
// bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
contentHandler(bestAttemptContent)
}
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.software.federspiel.fedeo.onesignal</string>
</array>
</dict>
</plist>

View File

@@ -1,41 +0,0 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'
pod 'CordovaPluginsStatic', :path => '../capacitor-cordova-ios-plugins'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
# iOS Deployment Target erzwingen
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
# Alle Warnungen auf inherited setzen, falls Pods Dinge überschreiben
config.build_settings['CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER'] = '$(inherited)'
end
end
end
target 'OneSignalNotificationServiceExtension' do
pod 'OneSignalXCFramework', '>= 5.0', '< 6.0'
end

View File

@@ -3,7 +3,6 @@
import MainNav from "~/components/MainNav.vue";
import dayjs from "dayjs";
import {useCapacitor} from "../composables/useCapacitor.js";
import GlobalMessages from "~/components/GlobalMessages.vue";
import TenantDropdown from "~/components/TenantDropdown.vue";
import LabelPrinterButton from "~/components/LabelPrinterButton.vue";
@@ -228,13 +227,6 @@ const footerLinks = [
</UDashboardNavbar>
<UDashboardSidebar id="sidebar">
<!-- <template #header>
<UDashboardSearchButton label="Suche..."/>
</template>-->
<!--
<GlobalMessages/>
-->
<MainNav/>
@@ -244,11 +236,17 @@ const footerLinks = [
<template #footer>
<div class="flex flex-col gap-3 w-full">
<UColorModeButton />
<LabelPrinterButton/>
<!-- Footer Links -->
<UDashboardSidebarLinks :links="footerLinks" />
<UDivider class="sticky bottom-0" />
<UserDropdown style="margin-bottom: env(safe-area-inset-bottom, 10px) !important;"/>
</div>
@@ -266,11 +264,6 @@ const footerLinks = [
<HelpSlideover/>
<!--<NotificationsSlideover />
<ClientOnly>
<LazyUDashboardSearch :groups="groups" hide-color-mode/>
</ClientOnly>-->
</UDashboardLayout>
</div>

View File

@@ -1,275 +0,0 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"
import dayjs from "dayjs";
import {useAuthStore} from "~/stores/auth.js";
const route = useRoute()
const auth = useAuthStore()
const month = dayjs().format("MM")
const hideNav = ref(false)
let lastScrollY = 0
let scrollElement = null
let returnTimer = null
const SHOW_DELAY = 1000 // 1 Sekunden
function showNavAfterDelay() {
clearTimeout(returnTimer)
returnTimer = setTimeout(() => {
hideNav.value = false
}, SHOW_DELAY)
}
const handleScroll = () => {
const current = scrollElement.scrollTop
// Runter scrollen -> verstecken
if (current > lastScrollY + 10) {
hideNav.value = true
showNavAfterDelay()
}
// Hoch scrollen -> sofort zeigen
if (current < lastScrollY - 10) {
hideNav.value = false
clearTimeout(returnTimer)
}
lastScrollY = current
}
onMounted(() => {
scrollElement = document.querySelector('.mobile-scroll-area')
if (scrollElement) {
scrollElement.addEventListener('scroll', handleScroll)
}
})
onBeforeUnmount(() => {
if (scrollElement) scrollElement.removeEventListener('scroll', handleScroll)
clearTimeout(returnTimer)
})
</script>
<template>
<div v-if="!auth.loading">
<div v-if="auth.activeTenantData?.locked === 'maintenance_tenant'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Dieser FEDEO Mandant wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut oder verwende einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
</UCard>
</UContainer>
</div>
<div v-else-if="auth.activeTenantData?.locked === 'maintenance'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-exclamation-triangle-solid" class="w-16 h-16 text-yellow-500" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Wartungsarbeiten
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
FEDEO wird derzeit gewartet. Bitte versuche es in einigen Minuten erneut.
</p>
</UCard>
</UContainer>
</div>
<div v-else-if="auth.activeTenantData?.locked === 'no_subscription'">
<UContainer class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
<UCard class="max-w-lg text-center p-10">
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class=" mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="mx-auto my-10"
v-else
/>
<div class="flex justify-center mb-6">
<UIcon name="i-heroicons-credit-card" class="w-16 h-16 text-red-600" />
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Kein Aktives Abonnement für diesen Mandant.
</h1>
<p class="text-gray-600 dark:text-gray-300 mb-8">
Bitte wenden Sie sich an den FEDEO Support um ein Abonnement zu erhalten oder verwenden Sie einen anderen Mandanten.
</p>
<div class="mx-auto text-left flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
{{tenant.name}}
<UButton
:disabled="tenant.locked"
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
</UCard>
</UContainer>
</div>
<UDashboardLayout class="safearea" v-else>
<UDashboardPage style="height: 90vh">
<UDashboardPanel grow>
<slot />
</UDashboardPanel>
</UDashboardPage>
<!-- Modernisierte Mobile Navigation -->
<nav
:class="[
'fixed bottom-0 left-0 right-0 z-50', // ← bottom-0 hinzugefügt!
'h-[70px] bg-white/80 dark:bg-gray-950/80 backdrop-blur-xl',
'border-t border-gray-200 dark:border-gray-800',
'flex justify-around items-center pt-2 pb-[max(env(safe-area-inset-bottom),0.5rem)]',
'transition-transform duration-300 ease-in-out',
hideNav ? 'translate-y-full' : 'translate-y-0'
]"
>
<UButton
icon="i-heroicons-home"
to="/mobile/"
variant="ghost"
:color="route.fullPath === '/mobile' ? 'primary' : 'gray'"
class="nav-btn"
/>
<UButton
icon="i-heroicons-clipboard-document-check"
to="/standardEntity/tasks"
variant="ghost"
:color="route.fullPath === '/standardEntity/tasks' ? 'primary' : 'gray'"
class="nav-btn"
/>
<UButton
icon="i-heroicons-rectangle-stack"
to="/standardEntity/projects"
variant="ghost"
:color="route.fullPath === '/standardEntity/projects' ? 'primary' : 'gray'"
class="nav-btn"
/>
<UButton
icon="i-heroicons-bars-4"
to="/mobile/menu"
variant="ghost"
:color="route.fullPath === '/mobile/menu' ? 'primary' : 'gray'"
class="nav-btn"
/>
</nav>
</UDashboardLayout>
</div>
<div
v-else
class="flex flex-col"
>
<UColorModeImage
light="/Logo_Hell_Weihnachten.png"
dark="/Logo_Dunkel_Weihnachten.png"
class="w-1/3 mx-auto my-10"
v-if="month === '12'"
/>
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
class="w-1/3 mx-auto my-10"
v-else
/>
<div v-if="!auth.activeTenant && auth.tenants?.length > 0 " class="w-full mx-auto text-center">
<!-- Tenant Selection -->
<h3 class="text-center font-bold text-2xl mb-5">Kein Aktiver Mandant. <br>Bitte wählen Sie ein Mandant.</h3>
<div class="mx-auto w-5/6 flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wählen</UButton>
</div>
<UButton
variant="outline"
color="rose"
@click="auth.logout()"
>Abmelden</UButton>
</div>
<div v-else>
<UProgress animation="carousel" class="w-3/4 mx-auto mt-10" />Test
{{auth.tenants}}
<UButton
variant="outline"
color="rose"
@click="auth.logout()"
>Abmelden</UButton>
</div>
</div>
</template>
<style scoped>
.nav-btn {
@apply w-12 h-12 flex justify-center items-center rounded-xl active:scale-95 transition;
}
</style>

View File

@@ -1,20 +1,42 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const auth = useAuthStore()
console.log(auth)
// DEBUG: Was sieht die Middleware wirklich?
console.log("🔒 Middleware Check auf:", to.path)
console.log("👤 User Status:", auth.user ? "Eingeloggt" : "Gast")
if (auth.loading) return
// 1. Ausnahme für Workflows
// WICHTIG: Prüfen ob to.path wirklich mit /workflows beginnt
if (to.path.startsWith('/workflows')) {
console.log("✅ Zugriff erlaubt (Public Route)")
return
}
if (auth.loading) {
console.log("⏳ Auth lädt noch...")
return
}
// Wenn nicht eingeloggt → auf /login (außer er will schon dahin)
if (!auth.user && !["/login", "/password-reset"].includes(to.path)) {
// 2. Wenn nicht eingeloggt
if (!auth.user) {
// Erlaube Zugriff auf Login/Reset Seiten
if (["/login", "/password-reset"].includes(to.path)) {
return
}
console.log("⛔ Blocked: Not logged in - Redirect to /login")
return navigateTo("/login")
}
// Wenn eingeloggt → von /login auf /dashboard umleiten
if (auth.user && !auth.user?.must_change_password && to.path === "/login") {
// 3. Wenn eingeloggt
if (to.path === "/login") {
if (auth.user.must_change_password) {
return navigateTo("/password-change")
}
return navigateTo("/")
} else if(auth.user && auth.user.must_change_password && to.path !== "/password-change") {
}
if (auth.user.must_change_password && to.path !== "/password-change") {
return navigateTo("/password-change")
}
})

View File

@@ -1,9 +0,0 @@
export default defineNuxtRouteMiddleware(async (to, _from) => {
const router = useRouter()
console.log(useCapacitor().getIsNative())
if(useCapacitor().getIsNative() && _from.path !== '/mobile') {
return router.push('/mobile')
}
})

View File

@@ -204,7 +204,8 @@ const setupPage = async () => {
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))
//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)
console.log(customer)
itemInfo.value.contact = null
if(!loadOnlyAdress) {
itemInfo.value.contact = null
}
if (customer) {
itemInfo.value.address.street = customer.infoData.street
itemInfo.value.address.zip = customer.infoData.zip
@@ -1824,7 +1831,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup>
<USelectMenu
:options="contacts.filter(i => i.customer === itemInfo.customer)"
:options="contacts.filter(i => i.customer?.id === itemInfo.customer)"
option-attribute="fullName"
value-attribute="id"
:search-attributes="['name']"
@@ -2057,7 +2064,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup>
<USelectMenu
:options="plants.filter(i => i.customer === itemInfo.customer)"
:options="plants.filter(i => i.customer?.id === itemInfo.customer)"
v-model="itemInfo.plant"
value-attribute="id"
option-attribute="name"
@@ -2095,7 +2102,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup>
<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"
value-attribute="id"
option-attribute="name"
@@ -2134,7 +2141,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
>
<InputGroup>
<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"
value-attribute="id"
option-attribute="name"

View File

@@ -81,7 +81,7 @@ const openBankstatements = () => {
E-Mail
</UButton>
<UButton
@click="router.push(`/createDocument/edit/?linkedDocument=${itemInfo.id}&loadMode=storno`)"
@click="router.push(`/createDocument/edit/?createddocument=${itemInfo.id}&loadMode=storno`)"
variant="outline"
color="rose"
v-if="itemInfo.type === 'invoices' || itemInfo.type === 'advanceInvoices'"

View File

@@ -1,5 +1,42 @@
<script setup lang="ts">
import dayjs from "dayjs"
// --- Helper für Schnellauswahl ---
// Setzt ein spezifisches Quartal (Q1-Q4), optional mit Jahresangabe
const setQuarter = (quarter: number, year: number = dayjs().year()) => {
const startMonth = (quarter - 1) * 3 // Q1=0 (Jan), Q2=3 (Apr), etc.
// .toDate() ist wichtig für den DatePicker
createExportData.value.start_date = dayjs().year(year).month(startMonth).startOf('month').toDate()
createExportData.value.end_date = dayjs().year(year).month(startMonth + 2).endOf('month').toDate()
}
// Berechnet automatisch das vorherige Quartal (inkl. Jahreswechsel, falls wir in Q1 sind)
const setLastQuarter = () => {
const currentMonth = dayjs().month() // 0 bis 11
const currentQuarter = Math.floor(currentMonth / 3) + 1 // 1 bis 4
if (currentQuarter === 1) {
// Wenn wir in Q1 sind, ist das letzte Quartal Q4 des Vorjahres
setQuarter(4, dayjs().year() - 1)
} else {
// Sonst einfach das vorherige Quartal im aktuellen Jahr
setQuarter(currentQuarter - 1)
}
}
const setMonthPreset = (type: 'current' | 'last') => {
let date = dayjs()
if (type === 'last') {
date = date.subtract(1, 'month')
}
createExportData.value.start_date = date.startOf('month').toDate()
createExportData.value.end_date = date.endOf('month').toDate()
}
// ---------------------------------
const exports = ref([])
const auth = useAuthStore()
@@ -46,8 +83,6 @@ const createExport = async () => {
}
}
</script>
<template>
@@ -63,28 +98,16 @@ const createExport = async () => {
>+ SEPA</UButton>
</template>
</UDashboardNavbar>
<UTable
:rows="exports"
:columns="[
{
key: 'created_at',
label: 'Erstellt am',
},{
key: 'start_date',
label: 'Start',
},{
key: 'end_date',
label: 'Ende',
},{
key: 'valid_until',
label: 'Gültig bis',
},{
key: 'type',
label: 'Typ',
},{
key: 'download',
label: 'Download',
},
{ key: 'created_at', label: 'Erstellt am' },
{ key: 'start_date', label: 'Start' },
{ key: 'end_date', label: 'Ende' },
{ key: 'valid_until', label: 'Gültig bis' },
{ key: 'type', label: 'Typ' },
{ key: 'download', label: 'Download' },
]"
>
<template #created_at-data="{row}">
@@ -100,11 +123,7 @@ const createExport = async () => {
{{dayjs(row.valid_until).format("DD.MM.YYYY HH:mm")}}
</template>
<template #download-data="{row}">
<UButton
@click="downloadFile(row)"
>
Download
</UButton>
<UButton @click="downloadFile(row)">Download</UButton>
</template>
</UTable>
@@ -114,46 +133,78 @@ const createExport = async () => {
Export erstellen
</template>
<UFormGroup
label="Start:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.start_date ? dayjs(createExportData.start_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="mx-auto"
/>
<div class="mb-6 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Schnellauswahl</div>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<div class="flex flex-col gap-3">
<div class="flex gap-2 items-center">
<span class="text-xs text-gray-400 w-16">Monat:</span>
<UButton size="2xs" color="white" variant="solid" @click="setMonthPreset('last')">Letzter</UButton>
<UButton size="2xs" color="white" variant="solid" @click="setMonthPreset('current')">Aktuell</UButton>
</div>
<UFormGroup
label="Ende:"
>
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.end_date ? dayjs(createExportData.end_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="mx-auto"
/>
<div class="flex gap-2 items-center">
<span class="text-xs text-gray-400 w-16">Quartal:</span>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UButton
size="2xs"
color="white"
variant="solid"
@click="setLastQuarter()"
class="mr-2 border-r border-gray-200 dark:border-gray-600 pr-3"
>
Letztes
</UButton>
<UButton
v-for="q in 4"
:key="q"
size="2xs"
color="white"
variant="solid"
@click="setQuarter(q)"
>
Q{{ q }}
</UButton>
</div>
</div>
</div>
<div class="flex gap-4">
<UFormGroup label="Start:" class="flex-1">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.start_date ? dayjs(createExportData.start_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="w-full justify-start"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.start_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
<UFormGroup label="Ende:" class="flex-1">
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="createExportData.end_date ? dayjs(createExportData.end_date).format('DD.MM.YYYY') : 'Datum auswählen'"
variant="outline"
class="w-full justify-start"
/>
<template #panel="{ close }">
<LazyDatePicker v-model="createExportData.end_date" @close="close" />
</template>
</UPopover>
</UFormGroup>
</div>
<template #footer>
<UButton
@click="createExport"
>
Erstellen
</UButton>
<div class="flex justify-end">
<UButton @click="createExport">
Erstellen
</UButton>
</div>
</template>
</UCard>
@@ -162,5 +213,4 @@ const createExport = async () => {
</template>
<style scoped>
</style>
</style>

View File

@@ -117,14 +117,17 @@ const totalCalculated = computed(() => {
if(account.taxType === "19" && account.amountTax) {
totalAmount19Tax += account.amountTax
} else if(account.taxType === "7" && account.amountTax) {
totalAmount7Tax += account.amountTax
}
})
totalGross = Number(totalNet + totalAmount19Tax)
totalGross = Number(totalNet + totalAmount19Tax + totalAmount7Tax)
return {
totalNet,
totalAmount19Tax,
totalAmount7Tax,
totalGross
}
@@ -144,7 +147,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
} else {
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(() => {
@@ -392,6 +395,10 @@ const findIncomingInvoiceErrors = computed(() => {
<td>Gesamt exkl. Steuer: </td>
<td class="text-right">{{totalCalculated.totalNet.toFixed(2).replace(".",",")}} €</td>
</tr>
<tr>
<td>7% Steuer: </td>
<td class="text-right">{{totalCalculated.totalAmount7Tax.toFixed(2).replace(".",",")}} €</td>
</tr>
<tr>
<td>19% Steuer: </td>
<td class="text-right">{{totalCalculated.totalAmount19Tax.toFixed(2).replace(".",",")}} €</td>
@@ -441,7 +448,7 @@ const findIncomingInvoiceErrors = computed(() => {
value-attribute="id"
searchable
:disabled="mode === 'show'"
:search-attributes="['label']"
:search-attributes="['label','name','description','number']"
searchable-placeholder="Suche..."
v-model="item.costCentre"
class="flex-auto"
@@ -450,10 +457,10 @@ const findIncomingInvoiceErrors = computed(() => {
{{costcentres.find(i => i.id === item.costCentre) ? costcentres.find(i => i.id === item.costCentre).name : "Keine Kostenstelle ausgewählt" }}
</template>
<template #option="{option}">
<span v-if="option.vehicle">Fahrzeug - {{option.name}}</span>
<span v-else-if="option.project">Projekt - {{option.name}}</span>
<span v-else-if="option.inventoryitem">Inventarartikel - {{option.name}}</span>
<span v-else>{{option.name}}</span>
<span v-if="option.vehicle">{{option.number}} - Fahrzeug - {{option.name}}</span>
<span v-else-if="option.project">{{option.number}} - Projekt - {{option.name}}</span>
<span v-else-if="option.inventoryitem">{{option.number}} - Inventarartikel - {{option.name}}</span>
<span v-else>{{option.number}} - {{option.name}}</span>
</template>
</USelectMenu>

View File

@@ -80,9 +80,6 @@
import Nimbot from "~/components/nimbot.vue";
import LabelPrintModal from "~/components/LabelPrintModal.vue";
definePageMeta({
middleware: 'redirect-to-mobile-index'
})
const modal = useModal();

View File

@@ -7,19 +7,14 @@ const auth = useAuthStore()
const toast = useToast()
const router = useRouter()
const platformIsNative = useCapacitor().getIsNative()
const doLogin = async (data:any) => {
try {
await auth.login(data.email, data.password)
// Weiterleiten nach erfolgreichem Login
toast.add({title:"Einloggen erfolgreich"})
if(platformIsNative) {
await router.push("/mobile")
} else {
await router.push("/")
}
await router.push("/")
} catch (err: any) {
toast.add({title:"Zugangsdaten falsch. Bitte überprüfen Sie Ihre Eingaben",color:"rose"})
}
@@ -27,22 +22,13 @@ const doLogin = async (data:any) => {
</script>
<template>
<UCard class="max-w-sm w-full mx-auto mt-5" v-if="!platformIsNative">
<UCard class="max-w-sm w-full mx-auto mt-5">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
/>
<UAlert
title="Achtung"
description="Es wurden alle Benutzerkonten zurückgesetzt. Bitte fordert über Passwort vergessen ein neues Passwort an."
color="rose"
variant="outline"
class="my-5"
>
</UAlert>
<UAuthForm
title="Login"
description="Geben Sie Ihre Anmeldedaten ein um Zugriff auf Ihren Account zu erhalten."
@@ -68,7 +54,7 @@ const doLogin = async (data:any) => {
</template>
</UAuthForm>
</UCard>
<div v-else class="mt-20 m-2 p-2">
<!-- <div v-else class="mt-20 m-2 p-2">
<UColorModeImage
light="/Logo.png"
dark="/Logo_Dark.png"
@@ -98,5 +84,5 @@ const doLogin = async (data:any) => {
<NuxtLink to="/password-reset" class="text-primary font-medium">Passwort vergessen?</NuxtLink>
</template>
</UAuthForm>
</div>
</div>-->
</template>

View File

@@ -1,74 +0,0 @@
<script setup>
definePageMeta({
layout: 'mobile'
})
const auth = useAuthStore()
const pinnedLinks = computed(() => {
return (auth.profile?.pinned_on_navigation || [])
.map((pin) => {
if (pin.type === "external") {
return {
label: pin.label,
to: pin.link,
icon: pin.icon,
external: true,
}
} else if (pin.type === "standardEntity") {
return {
label: pin.label,
to: `/standardEntity/${pin.datatype}/show/${pin.id}`,
icon: pin.icon,
external: false,
}
}
})
.filter(Boolean)
})
</script>
<template>
<UDashboardPanelContent>
<UPageGrid>
<UDashboardCard>
<display-welcome/>
</UDashboardCard>
<UDashboardCard
title="Aufgaben"
>
<display-open-tasks/>
</UDashboardCard>
<!--<UDashboardCard
title="Anwesenheit"
>
<display-running-working-time/>
</UDashboardCard>
<UDashboardCard
title="Zeit"
>
<display-running-time/>
</UDashboardCard>
<UDashboardCard
title="Buchhaltung"
v-if="profileStore.ownTenant.features.accounting"
>
<display-open-balances/>
</UDashboardCard>-->
<UDashboardCard
title="Projekte"
>
<display-projects-in-phases/>
</UDashboardCard>
<display-pinnend-links :links="pinnedLinks"/>
</UPageGrid>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -1,81 +0,0 @@
<script setup>
definePageMeta({
layout: 'mobile',
})
const auth = useAuthStore()
</script>
<template>
<UDashboardPanelContent>
<UDivider class="mb-3">Weiteres</UDivider>
<UButton
class="w-full my-1"
to="/staff/time"
icon="i-heroicons-clock"
>
Zeiten
</UButton>
<!-- <UButton
class="w-full my-1"
to="/standardEntity/absencerequests"
icon="i-heroicons-document-text"
>
Abwesenheiten
</UButton>-->
<UButton
class="w-full my-1"
to="/standardEntity/customers"
icon="i-heroicons-user-group"
>
Kunden
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/vendors"
icon="i-heroicons-truck"
>
Lieferanten
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/contacts"
icon="i-heroicons-user-group"
>
Ansprechpartner
</UButton>
<UButton
class="w-full my-1"
to="/standardEntity/plants"
icon="i-heroicons-clipboard-document"
>
Objekte
</UButton>
<UButton
class="w-full my-1"
@click="auth.logout()"
color="rose"
variant="outline"
>
Abmelden
</UButton>
<UDivider class="my-5">Unternehmen wechseln</UDivider>
<div class="w-full flex flex-row justify-between my-3" v-for="tenant in auth.tenants">
<span class="text-left">{{tenant.name}}</span>
<UButton
@click="auth.switchTenant(tenant.id)"
>Wechseln</UButton>
</div>
</UDashboardPanelContent>
</template>
<style scoped>
</style>

View File

@@ -1,220 +1,352 @@
<script setup>
import { ref, onMounted } from 'vue'
const dataStore = useDataStore()
const toast = useToast()
defineShortcuts({
'+': () => {
editTemplateModalOpen.value = true
}
})
// useEntities Initialisierung für 'texttemplates'
const { select, create, update } = useEntities("texttemplates")
// --- State ---
const editTemplateModalOpen = ref(false)
const itemInfo = ref({})
const texttemplates = ref([])
const loading = ref(true)
const isSaving = ref(false)
const textareaRef = ref(null)
const setup = async () => {
texttemplates.value = (await useEntities("texttemplates").select()).filter(i => !i.archived)
loading.value = false
}
setup()
// Tabelle Expand State
const expand = ref({
openedRows: [],
row: {}
})
// --- Variablen Definitionen ---
const variableDefinitions = [
{ key: '{{vorname}}', label: 'Vorname', desc: 'Vorname des Kunden' },
{ key: '{{nachname}}', label: 'Nachname', desc: 'Nachname des Kunden' },
{ key: '{{anrede}}', label: 'Anrede', desc: 'Formelle Anrede' },
{ key: '{{titel}}', label: 'Titel', desc: 'Titel des Kunden' },
{ key: '{{zahlungsziel_in_tagen}}', label: 'Zahlungsziel', desc: 'In Tagen' },
{ key: '{{lohnkosten}}', label: 'Lohnkosten', desc: 'Ausgewiesene Lohnkosten' },
]
// --- Shortcuts ---
defineShortcuts({
'+': () => openModal()
})
// --- Data Fetching ---
const refreshData = async () => {
loading.value = true
try {
// select() filtert bereits archivierte Einträge, wenn dataType.isArchivable true ist
texttemplates.value = await select()
} catch (e) {
toast.add({ title: 'Fehler beim Laden', description: e.message, color: 'rose' })
} finally {
loading.value = false
}
}
// Initialer Load
onMounted(() => {
refreshData()
})
// --- Actions ---
const openModal = (item = null) => {
if (item) {
// Deep Copy um Reaktivitätsprobleme beim Abbrechen zu vermeiden
itemInfo.value = JSON.parse(JSON.stringify(item))
} else {
// Reset für Erstellen
itemInfo.value = {
name: '',
documentType: 'offer', // Default
pos: 'startText',
text: '',
default: false
}
}
editTemplateModalOpen.value = true
}
const insertVariable = (variableKey) => {
itemInfo.value.text = (itemInfo.value.text || '') + variableKey + ' '
}
const handleCreate = async () => {
isSaving.value = true
try {
// create(payload, noRedirect) -> Wir setzen noRedirect auf true
await create(itemInfo.value, true)
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
editTemplateModalOpen.value = false
await refreshData()
} catch (e) {
toast.add({ title: 'Fehler', description: 'Konnte nicht erstellt werden.', color: 'rose' })
} finally {
isSaving.value = false
}
}
const handleUpdate = async () => {
isSaving.value = true
try {
// update(id, payload, noRedirect) -> Wir setzen noRedirect auf true
await update(itemInfo.value.id, itemInfo.value, true)
// Hinweis: Erfolgs-Toast kommt bereits aus useEntities
editTemplateModalOpen.value = false
await refreshData()
} catch (e) {
toast.add({title: 'Fehler', description: 'Konnte nicht gespeichert werden.', color: 'rose'})
} finally {
isSaving.value = false
}
}
const handleArchive = async (row) => {
try {
// Wir nutzen update mit archived: true und noRedirect, um auf der Seite zu bleiben
await update(row.id, {archived: true}, true)
await refreshData()
} catch (e) {
toast.add({title: 'Fehler', description: 'Konnte nicht archiviert werden.', color: 'rose'})
}
}
// Helper für Labels (falls dataStore noch lädt oder Key fehlt)
const getDocLabel = (type) => {
return dataStore.documentTypesForCreation?.[type]?.label || type
}
</script>
<template>
<UDashboardNavbar
title="Text Vorlagen"
>
<UDashboardNavbar title="Text Vorlagen">
<template #right>
<UButton
@click="editTemplateModalOpen = true, itemInfo = {}"
icon="i-heroicons-plus"
@click="openModal()"
color="primary"
variant="solid"
>
+ Erstellen
Erstellen
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UCard class="mx-5">
<template #header>
Variablen
</template>
<table>
<tr>
<th class="text-left">Variable</th>
<th class="text-left">Beschreibung</th>
</tr>
<tr>
<td>vorname</td>
<td>Vorname</td>
</tr>
<tr>
<td>nachname</td>
<td>Nachname</td>
</tr>
<tr>
<td>zahlungsziel_in_tagen</td>
<td>Zahlungsziel in Tagen</td>
</tr>
<tr>
<td>lohnkosten</td>
<td>Lohnkosten Verkauf</td>
</tr>
<tr>
<td>titel</td>
<td>Titel</td>
</tr>
<tr>
<td>anrede</td>
<td>Anrede</td>
</tr>
</table>
</UCard>
<UDashboardPanelContent>
<UAlert
icon="i-heroicons-information-circle"
color="primary"
variant="soft"
title="Platzhalter nutzen"
description="Nutzen Sie die Variablen im Editor, um dynamische Inhalte (wie Kundennamen) automatisch einzufügen."
class="mb-4 mx-5 mt-2"
/>
<UTable
class="mt-3"
:rows="texttemplates"
:loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
:loading="loading"
v-model:expand="expand" :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Textvorlagen anzuzeigen' }"
:columns="[{key:'name',label:'Name'},{key:'documentType',label:'Dokumententyp'},{key:'default',label:'Standard'},{key:'pos',label:'Position'}]"
v-model:expand="expand"
:empty-state="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
:columns="[
{ key: 'name', label: 'Bezeichnung' },
{ key: 'documentType', label: 'Verwendung' },
{ key: 'pos', label: 'Position' },
{ key: 'default', label: 'Standard' },
{ key: 'actions', label: '' }
]"
>
<template #documentType-data="{row}">
{{dataStore.documentTypesForCreation[row.documentType].label}}
<template #name-data="{ row }">
<span class="font-medium text-gray-900 dark:text-white">{{ row.name }}</span>
</template>
<template #default-data="{row}">
{{row.default ? "Ja" : "Nein"}}
<template #documentType-data="{ row }">
<UBadge color="gray" variant="soft">
{{ getDocLabel(row.documentType) }}
</UBadge>
</template>
<template #pos-data="{row}">
<span v-if="row.pos === 'startText'">Einleitung</span>
<span v-else-if="row.pos === 'endText'">Endtext</span>
<template #pos-data="{ row }">
<div class="flex items-center gap-2">
<UIcon
:name="row.pos === 'startText' ? 'i-heroicons-bars-arrow-down' : 'i-heroicons-bars-arrow-up'"
class="w-4 h-4 text-gray-500"
/>
<span>{{ row.pos === 'startText' ? 'Einleitung' : 'Endtext' }}</span>
</div>
</template>
<template #default-data="{ row }">
<UIcon v-if="row.default" name="i-heroicons-check-circle-20-solid" class="text-green-500"/>
<span v-else class="text-gray-400">-</span>
</template>
<template #actions-data="{ row }">
<UButton color="gray" variant="ghost" icon="i-heroicons-pencil-square" @click="openModal(row)"/>
</template>
<template #expand="{ row }">
<div class="p-4">
<p class="text-2xl">{{dataStore.documentTypesForCreation[row.documentType].label}}</p>
<p class="text-xl mt-3">{{row.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
<p class="text-justify mt-3">{{row.text}}</p>
<UButton
class="mt-3 mr-3"
@click="itemInfo = row;
editTemplateModalOpen = true"
variant="outline"
>Bearbeiten</UButton>
<ButtonWithConfirm
color="rose"
variant="outline"
@confirmed="dataStore.updateItem('texttemplates',{id: row.id,archived: true}),
setup"
>
<template #button>
Archivieren
</template>
<template #header>
<span class="text-md dark:text-whitetext-black font-bold">Archivieren bestätigen</span>
</template>
Möchten Sie diesen Ausgangsbeleg wirklich archivieren?
</ButtonWithConfirm>
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-b-lg border-t border-gray-200 dark:border-gray-700">
<div class="mb-4">
<h4 class="text-sm font-bold uppercase text-gray-500 mb-1">Vorschau</h4>
<p class="text-gray-800 dark:text-gray-200 whitespace-pre-line p-3 bg-white dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700 text-sm">
{{ row.text }}
</p>
</div>
<div class="flex justify-end gap-3">
<ButtonWithConfirm
color="rose"
variant="soft"
icon="i-heroicons-archive-box"
@confirmed="handleArchive(row)"
>
<template #button>Archivieren</template>
<template #header>
<span class="font-bold">Wirklich archivieren?</span>
</template>
Die Vorlage "{{ row.name }}" wird archiviert.
</ButtonWithConfirm>
<UButton
icon="i-heroicons-pencil"
@click="openModal(row)"
>
Bearbeiten
</UButton>
</div>
</div>
</template>
</UTable>
<!-- <div class="w-3/4 mx-auto mt-5">
<UCard
v-for="template in dataStore.texttemplates"
class="mb-3"
>
<p class="text-2xl">{{dataStore.documentTypesForCreation[template.documentType].label}}</p>
<p class="text-xl">{{template.pos === 'startText' ? 'Einleitung' : 'Ende'}}</p>
<p class="text-justify">{{template.text}}</p>
<UButton
@click="itemInfo = template;
editTemplateModalOpen = true"
icon="i-heroicons-pencil-solid"
variant="outline"
/>
</UCard>
</div>-->
</UDashboardPanelContent>
<UModal
v-model="editTemplateModalOpen"
>
<UCard class="h-full">
<UModal v-model="editTemplateModalOpen" :ui="{ width: 'sm:max-w-4xl' }">
<UCard>
<template #header>
{{itemInfo.id ? 'Vorlage bearbeiten' : 'Vorlage erstellen'}}
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">
{{ itemInfo.id ? 'Vorlage bearbeiten' : 'Neue Vorlage erstellen' }}
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark" @click="editTemplateModalOpen = false"/>
</div>
</template>
<UForm class="h-full">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<UFormGroup
label="Name:"
>
<UInput
v-model="itemInfo.name"
/>
</UFormGroup>
<UFormGroup
label="Dokumententyp:"
>
<div class="lg:col-span-2 space-y-4">
<UFormGroup label="Bezeichnung" required>
<UInput v-model="itemInfo.name" placeholder="z.B. Standard Angebotstext" icon="i-heroicons-tag"/>
</UFormGroup>
<USelectMenu
:options="Object.keys(dataStore.documentTypesForCreation).filter(i => i !== 'serialInvoices').map(i => {
return {
label: dataStore.documentTypesForCreation[i].label,
key: i
}
})"
option-attribute="label"
value-attribute="key"
v-model="itemInfo.documentType"
/>
</UFormGroup>
<UFormGroup
label="Position:"
>
<USelectMenu
:options="[{label:'Einleitung',key: 'startText'},{label:'Ende',key: 'endText'}]"
option-attribute="label"
value-attribute="key"
v-model="itemInfo.pos"
/>
</UFormGroup>
<UFormGroup
label="Text:"
>
<UTextarea
v-model="itemInfo.text"
/>
</UFormGroup>
</UForm>
<div class="grid grid-cols-2 gap-4">
<UFormGroup label="Dokumententyp" required>
<USelectMenu
v-model="itemInfo.documentType"
:options="Object.keys(dataStore.documentTypesForCreation || {})
.filter(i => i !== 'serialInvoices')
.map(i => ({ label: dataStore.documentTypesForCreation[i].label, key: i }))"
option-attribute="label"
value-attribute="key"
/>
</UFormGroup>
<UFormGroup label="Position" required>
<USelectMenu
v-model="itemInfo.pos"
:options="[
{ label: 'Einleitung (Oben)', key: 'startText' },
{ label: 'Endtext (Unten)', key: 'endText' }
]"
option-attribute="label"
value-attribute="key"
/>
</UFormGroup>
</div>
<UFormGroup label="Text Inhalt" required help="Klicken Sie rechts auf eine Variable, um sie einzufügen.">
<UTextarea
ref="textareaRef"
v-model="itemInfo.text"
:rows="10"
placeholder="Sehr geehrte Damen und Herren..."
class="font-mono text-sm"
/>
</UFormGroup>
<UCheckbox v-model="itemInfo.default" label="Als Standard für diesen Typ verwenden"/>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 h-fit">
<h4 class="font-semibold mb-3 flex items-center gap-2">
<UIcon name="i-heroicons-variable"/>
Variablen
</h4>
<p class="text-xs text-gray-500 mb-4">
Diese Platzhalter werden beim Erstellen des Dokuments ersetzt.
</p>
<div class="flex flex-col gap-2">
<button
v-for="v in variableDefinitions"
:key="v.key"
@click="insertVariable(v.key)"
class="group flex items-center justify-between p-2 rounded hover:bg-white dark:hover:bg-gray-700 border border-transparent hover:border-gray-200 dark:hover:border-gray-600 transition-colors text-left"
type="button"
>
<div>
<code
class="text-xs font-bold text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-950/50 px-1 py-0.5 rounded">{{
v.key
}}</code>
<div class="text-xs text-gray-500 mt-0.5">{{ v.desc }}</div>
</div>
<UIcon name="i-heroicons-plus-circle" class="w-5 h-5 text-gray-300 group-hover:text-primary-500"/>
</button>
</div>
</div>
</div>
<!-- TODO: Update und Create -->
<template #footer>
<UButton
@click="dataStore.createNewItem('texttemplates',itemInfo);
editTemplateModalOpen = false"
v-if="!itemInfo.id"
>Erstellen</UButton>
<UButton
@click="dataStore.updateItem('texttemplates',itemInfo);
editTemplateModalOpen = false"
v-if="itemInfo.id"
>Speichern</UButton>
<div class="flex justify-end gap-3">
<UButton color="gray" variant="ghost" @click="editTemplateModalOpen = false">
Abbrechen
</UButton>
<UButton
v-if="!itemInfo.id"
color="primary"
:loading="isSaving"
@click="handleCreate"
icon="i-heroicons-plus"
>
Erstellen
</UButton>
<UButton
v-else
color="primary"
:loading="isSaving"
@click="handleUpdate"
icon="i-heroicons-check"
>
Speichern
</UButton>
</div>
</template>
</UCard>
</UModal>
</template>
<style scoped>
</style>

View File

@@ -24,7 +24,7 @@ const workingTimeInfo = ref<{
} | null; // Neue Struktur für die Zusammenfassung
} | null>(null)
const platformIsNative = ref(useCapacitor().getIsNative())
const platformIsNative = ref(false)
const selectedPresetRange = ref("Dieser Monat bis heute")
const selectedStartDay = ref("")
@@ -119,8 +119,8 @@ async function loadWorkingTimeInfo() {
// Erstellt Query-Parameter für den neuen Backend-Endpunkt
const queryParams = new URLSearchParams({
from: selectedStartDay.value,
to: selectedEndDay.value,
from: $dayjs(selectedStartDay.value).format("YYYY-MM-DD"),
to: $dayjs(selectedEndDay.value).format("YYYY-MM-DD"),
targetUserId: evaluatedUserId.value,
});

View File

@@ -14,7 +14,7 @@ const toast = useToast()
const { $dayjs } = useNuxtApp()
// MOBILE DETECTION
const platformIsNative = useCapacitor().getIsNative()
const platformIsNative = false
// STATE
const loading = ref(false)

View File

@@ -1,7 +1,5 @@
<script setup>
import {setPageLayout} from "#app";
import {useCapacitor} from "~/composables/useCapacitor.js";
const route = useRoute()
const dataStore = useDataStore()
@@ -9,7 +7,7 @@ const api = useNuxtApp().$api
const type = route.params.type
const platform = await useCapacitor().getIsNative() ? "mobile" : "default"
const platform = "default"
const dataType = dataStore.dataTypes[route.params.type]

View File

@@ -7,7 +7,7 @@ import {setPageLayout} from "#app";
const { has } = usePermission()
const platformIsNative = useCapacitor().getIsNative()
const platformIsNative = false
defineShortcuts({
'/': () => {

View File

@@ -39,10 +39,7 @@ const setup = async () => {
times.value = await useSupabaseSelect("times","*, profile(*), project(id, name)")
projects.value = await useSupabaseSelect("projects","*")
if(await useCapacitor().getIsPhone()) {
platform.value = "mobile"
setPageLayout("mobile")
}
runningTimeInfo.value = (await supabase
.from("times")

View File

@@ -4,7 +4,7 @@ definePageMeta({
middleware: [], // Keine Auth-Checks durch Nuxt
auth: false // Falls du das nuxt-auth Modul nutzt
})
const config = useRuntimeConfig()
const route = useRoute()
const token = route.params.token
const { $api } = useNuxtApp() // Dein Fetch-Wrapper
@@ -27,7 +27,7 @@ const loadContext = async () => {
// Abruf an dein Fastify Backend
// Pfad evtl. anpassen, wenn du Proxy nutzt
const res = await $fetch(`http://localhost:3100/workflows/context/${token}`, { headers })
const res = await $fetch(`${config.public.apiBase}/workflows/context/${token}`, { headers })
context.value = res
status.value = 'ready'

View File

@@ -1,35 +1,86 @@
import {Preferences} from "@capacitor/preferences";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const toast = useToast()
const api = $fetch.create({
baseURL: config.public.apiBase,
credentials: "include",
async onRequest({options}) {
// Token aus Cookie holen
let token: string | null | undefined = ""
if (useCapacitor().getIsNative()) {
const {value} = await Preferences.get({key: 'token'});
token = value
} else {
token = useCookie("token").value
}
async onRequest({ options }) {
const token = useCookie("token").value
// Falls im Request explizit ein anderer JWT übergeben wird → diesen verwenden
if (options.context && (options.context as any).jwt) {
token = (options.context as any).jwt
}
if (token) {
// Falls im Request explizit ein anderer JWT übergeben wird
if (options.context?.jwt) {
options.headers = {
...options.headers,
Authorization: `Bearer ${options.context.jwt}`,
}
} else if (token) {
options.headers = {
...options.headers,
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

@@ -16,32 +16,36 @@ export const useAuthStore = defineStore("auth", {
actions: {
async persist(token) {
if(useCapacitor().getIsNative()) {
console.log("On Native")
await Preferences.set({
key:"token",
value: token,
})
useCookie("token").value = token // persistieren
} else {
console.log("On Web")
useCookie("token").value = token // persistieren
console.log("On Web")
useCookie("token").value = token // persistieren
}
},
async initStore() {
console.log("Auth initStore")
await this.fetchMe()
// 1. Check: Haben wir überhaupt ein Token?
const token = useCookie("token").value
/*if (!token) {
// Kein Token -> Wir sind fertig, User ist Gast.
this.user = null
this.loading = false
return
}*/
// 2. Token existiert -> Versuche User zu laden
await this.fetchMe(token)
// Wenn fetchMe fertig ist (egal ob Erfolg oder Fehler), ladebalken weg
// Optional: Wenn eingeloggt, leite zur Home, falls gewünscht
if(this.activeTenant > 0) {
this.loading = false
if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
// Hier vorsichtig sein: Nicht navigieren, wenn der User auf eine Deep-Link URL will!
// navigateTo("/") <-- Das würde ich hier evtl. sogar weglassen und der Middleware überlassen
}
},
@@ -51,14 +55,11 @@ export const useAuthStore = defineStore("auth", {
const tempStore = useTempStore()
if(this.profile.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(this.profile?.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(this.activeTenant > 0) {
this.loading = false
if(useCapacitor().getIsNative()) {
navigateTo("/mobile")
} else {
navigateTo("/")
}
navigateTo("/")
}
},
@@ -69,12 +70,34 @@ export const useAuthStore = defineStore("auth", {
method: "POST",
body: { email, password }
})
console.log("Token: " + token)
// 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird
const tokenCookie = useCookie("token")
tokenCookie.value = token
// 2. User Daten laden
await this.fetchMe(token)
console.log(this.user)
// 3. WICHTIG: Jetzt explizit weiterleiten!
// Prüfen, ob der User geladen wurde
if (this.user) {
// Falls Passwort-Änderung erzwungen wird (passend zu deiner Middleware)
if (this.user.must_change_password) {
return navigateTo("/password-change")
}
// Normaler Login -> Dashboard
return navigateTo("/")
}
} catch (e) {
console.log("login error:" + e)
// Hier könnte man noch eine Fehlermeldung im UI anzeigen
}
},
async logout() {
@@ -82,34 +105,44 @@ export const useAuthStore = defineStore("auth", {
try {
await useNuxtApp().$api("/auth/logout", { method: "POST" })
} catch (e) {
console.error("Logout fehlgeschlagen:", e)
console.error("Logout API fehlgeschlagen (egal):", e)
}
// State resetten
this.resetState()
// Token löschen
useCookie("token").value = null
// Nur beim expliziten Logout navigieren wir
navigateTo("/login")
},
resetState() {
this.user = null
this.permissions = []
this.profile = null
this.activeTenant = null
this.tenants = []
if(useCapacitor().getIsNative()) {
await Preferences.remove({ key: 'token' });
useCookie("token").value = null
} else {
useCookie("token").value = null
}
navigateTo("/login")
this.activeTenantData = null
},
async fetchMe(jwt= null) {
console.log("Auth fetchMe")
const tempStore = useTempStore()
// Token aus Argument oder Cookie holen
const tokenToUse = jwt || useCookie("token").value
try {
const me = await useNuxtApp().$api("/api/me", {
headers: { Authorization: `Bearer ${jwt}`,
context: {
jwt
}}
headers: {
Authorization: `Bearer ${tokenToUse}`,
context: { jwt: tokenToUse }
}
})
// ... (Deine Logik für tenants, sorting etc. bleibt gleich) ...
console.log(me)
this.user = me.user
this.permissions = me.permissions
@@ -122,19 +155,24 @@ export const useAuthStore = defineStore("auth", {
this.profile = me.profile
if(this.profile.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(this.profile?.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(me.activeTenant > 0) {
this.activeTenant = me.activeTenant
this.activeTenantData = me.tenants.find(i => i.id === me.activeTenant)
} else {
}
console.log(this)
} catch (err: any) {
if (err?.response?.status === 401) this.logout()
// WICHTIG: Hier NICHT this.logout() aufrufen, weil das navigiert!
console.log("fetchMe failed (Invalid Token or Network)", err)
// Stattdessen nur den State sauber machen und Token löschen
this.resetState()
useCookie("token").value = null
// Wir werfen den Fehler nicht weiter, damit initStore normal durchläuft
}
},
@@ -149,15 +187,10 @@ export const useAuthStore = defineStore("auth", {
const {token} = res
if(useCapacitor().getIsNative()) {
await Preferences.set({
key:"token",
value: token,
})
} else {
useCookie("token").value = token // persistieren
}
useCookie("token").value = token // persistieren
await this.init(token)
},

View File

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

View File

@@ -40,7 +40,7 @@ export const useProfileStore = defineStore('profile', () => {
let profiles = (await supabase.from("profiles").select("*, role(*)")).data
let activeProfileConnection = profileconnections.find(i => i.active)
if(activeProfileConnection) {
if(!await useCapacitor().getIsPhone()) {
if(!false) {
toast.add({title: 'Aktives Profil ausgewählt'})
}