27
Dockerfile
27
Dockerfile
@@ -1,29 +1,20 @@
|
||||
# Basis-Image
|
||||
FROM node:20-alpine AS base
|
||||
FROM node:20-alpine
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Dependencies installieren (dev deps für Build erforderlich)
|
||||
# Package-Dateien
|
||||
COPY package*.json ./
|
||||
|
||||
# Dev + Prod Dependencies (für TS-Build nötig)
|
||||
RUN npm install
|
||||
|
||||
# Quellcode kopieren
|
||||
# Restlicher Sourcecode
|
||||
COPY . .
|
||||
|
||||
# Build ausführen (TypeScript -> dist)
|
||||
RUN npx tsc --skipLibCheck --noEmitOnError false
|
||||
|
||||
# --------- Production Stage ---------
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Nur production dependencies installieren
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY --from=base /usr/src/app/dist ./dist
|
||||
# TypeScript Build
|
||||
RUN npm run build
|
||||
|
||||
# Port freigeben
|
||||
EXPOSE 3100
|
||||
|
||||
# App starten
|
||||
CMD ["npm", "start"]
|
||||
# Start der App
|
||||
CMD ["node", "dist/src/index.js"]
|
||||
|
||||
@@ -9,7 +9,7 @@ export const holidays = pgTable("holidays", {
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
stateCode: text("state_code").notNull(),
|
||||
state_code: text("state_code").notNull(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
@@ -17,18 +17,18 @@ import {sql} from "drizzle-orm";
|
||||
export const stafftimeentries = pgTable("staff_time_entries", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
tenant_id: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
userId: uuid("user_id")
|
||||
user_id: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stoppedAt: timestamp("stopped_at", { withTimezone: true }),
|
||||
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stopped_at: timestamp("stopped_at", { withTimezone: true }),
|
||||
|
||||
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
duration_minutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
sql`CASE
|
||||
WHEN stopped_at IS NOT NULL
|
||||
THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)
|
||||
@@ -40,12 +40,12 @@ export const stafftimeentries = pgTable("staff_time_entries", {
|
||||
|
||||
description: text("description"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
source: text("source"),
|
||||
|
||||
@@ -53,15 +53,15 @@ export const stafftimeentries = pgTable("staff_time_entries", {
|
||||
|
||||
device: uuid("device"),
|
||||
|
||||
internalNote: text("internal_note"),
|
||||
internal_note: text("internal_note"),
|
||||
|
||||
vacationReason: text("vacation_reason"),
|
||||
vacationDays: numeric("vacation_days", { precision: 5, scale: 2 }),
|
||||
vacation_reason: text("vacation_reason"),
|
||||
vacation_days: numeric("vacation_days", { precision: 5, scale: 2 }),
|
||||
|
||||
approvedBy: uuid("approved_by").references(() => authUsers.id),
|
||||
approvedAt: timestamp("approved_at", { withTimezone: true }),
|
||||
approved_by: uuid("approved_by").references(() => authUsers.id),
|
||||
approved_at: timestamp("approved_at", { withTimezone: true }),
|
||||
|
||||
sickReason: text("sick_reason"),
|
||||
sick_reason: text("sick_reason"),
|
||||
})
|
||||
|
||||
export type StaffTimeEntry = typeof stafftimeentries.$inferSelect
|
||||
|
||||
@@ -10,17 +10,17 @@ import {
|
||||
import { stafftimeentries } from "./staff_time_entries"
|
||||
import {sql} from "drizzle-orm";
|
||||
|
||||
export const staffTimeEntryConnects = pgTable("staff_time_entry_connects", {
|
||||
export const stafftimenetryconnects = pgTable("staff_time_entry_connects", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
timeEntryId: uuid("time_entry_id")
|
||||
stafftimeentry: uuid("time_entry_id")
|
||||
.notNull()
|
||||
.references(() => stafftimeentries.id, { onDelete: "cascade" }),
|
||||
|
||||
projectId: bigint("project_id", { mode: "number" }), // referenziert später projects.id
|
||||
project_id: bigint("project_id", { mode: "number" }), // referenziert später projects.id
|
||||
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stoppedAt: timestamp("stopped_at", { withTimezone: true }).notNull(),
|
||||
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stopped_at: timestamp("stopped_at", { withTimezone: true }).notNull(),
|
||||
|
||||
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
sql`(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)`
|
||||
@@ -28,11 +28,11 @@ export const staffTimeEntryConnects = pgTable("staff_time_entry_connects", {
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type StaffTimeEntryConnect =
|
||||
typeof staffTimeEntryConnects.$inferSelect
|
||||
typeof stafftimenetryconnects.$inferSelect
|
||||
export type NewStaffTimeEntryConnect =
|
||||
typeof staffTimeEntryConnects.$inferInsert
|
||||
typeof stafftimenetryconnects.$inferInsert
|
||||
|
||||
@@ -23,15 +23,15 @@ export const statementallocations = pgTable("statementallocations", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
// foreign keys
|
||||
bs_id: integer("bs_id")
|
||||
bankstatement: integer("bs_id")
|
||||
.notNull()
|
||||
.references(() => bankstatements.id),
|
||||
|
||||
cd_id: integer("cd_id").references(() => createddocuments.id),
|
||||
createddocument: integer("cd_id").references(() => createddocuments.id),
|
||||
|
||||
amount: doublePrecision("amount").notNull().default(0),
|
||||
|
||||
ii_id: bigint("ii_id", { mode: "number" }).references(
|
||||
incominginvoice: bigint("ii_id", { mode: "number" }).references(
|
||||
() => incominginvoices.id
|
||||
),
|
||||
|
||||
@@ -43,7 +43,7 @@ export const statementallocations = pgTable("statementallocations", {
|
||||
() => accounts.id
|
||||
),
|
||||
|
||||
createdAt: timestamp("created_at", {
|
||||
created_at: timestamp("created_at", {
|
||||
withTimezone: false,
|
||||
}).defaultNow(),
|
||||
|
||||
@@ -57,9 +57,9 @@ export const statementallocations = pgTable("statementallocations", {
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"start": "node dist/src/index.js",
|
||||
"schema:index": "ts-node scripts/generate-schema-index.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -27,6 +27,7 @@ import helpdeskInboundRoutes from "./routes/helpdesk.inbound";
|
||||
import notificationsRoutes from "./routes/notifications";
|
||||
import staffTimeRoutes from "./routes/staff/time";
|
||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||
import userRoutes from "./routes/auth/user";
|
||||
|
||||
//Resources
|
||||
import resourceRoutes from "./routes/resources/main";
|
||||
@@ -64,6 +65,7 @@ async function main() {
|
||||
app.addHook('preHandler', (req, reply, done) => {
|
||||
console.log(req.method)
|
||||
console.log('Matched path:', req.routeOptions.url)
|
||||
console.log('Exact URL:', req.url)
|
||||
done()
|
||||
})
|
||||
|
||||
@@ -112,6 +114,7 @@ async function main() {
|
||||
await subApp.register(notificationsRoutes);
|
||||
await subApp.register(staffTimeRoutes);
|
||||
await subApp.register(staffTimeConnectRoutes);
|
||||
await subApp.register(userRoutes);
|
||||
|
||||
|
||||
await subApp.register(resourceRoutes);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {FastifyInstance} from "fastify";
|
||||
|
||||
import { FastifyInstance } from "fastify";
|
||||
import {and, eq, gte, lte, asc, inArray} from "drizzle-orm";
|
||||
import {
|
||||
authProfiles,
|
||||
stafftimeentries,
|
||||
holidays,
|
||||
} from "../../../db/schema";
|
||||
|
||||
export async function generateTimesEvaluation(
|
||||
server: FastifyInstance,
|
||||
@@ -8,136 +13,204 @@ export async function generateTimesEvaluation(
|
||||
startDateInput: string,
|
||||
endDateInput: string
|
||||
) {
|
||||
const startDate = server.dayjs(startDateInput)
|
||||
const endDate = server.dayjs(endDateInput)
|
||||
const startDate = server.dayjs(startDateInput);
|
||||
const endDate = server.dayjs(endDateInput);
|
||||
|
||||
console.log(startDate.format("YYYY-MM-DD HH:mm:ss"));
|
||||
console.log(endDate.format("YYYY-MM-DD HH:mm:ss"));
|
||||
|
||||
// 🧾 Profil laden (Arbeitszeiten + Bundesland)
|
||||
const { data: profile, error: profileError } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user_id)
|
||||
.eq("tenant_id", tenant_id)
|
||||
.maybeSingle()
|
||||
// -------------------------------------------------------------
|
||||
// 1️⃣ Profil laden
|
||||
// -------------------------------------------------------------
|
||||
const profileRows = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, user_id),
|
||||
eq(authProfiles.tenant_id, tenant_id)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (profileError || !profile) throw new Error("Profil konnte nicht geladen werden.")
|
||||
const profile = profileRows[0];
|
||||
|
||||
// 🕒 Arbeitszeiten abrufen
|
||||
const { data: timesRaw, error: timeError } = await server.supabase
|
||||
.from("staff_time_entries")
|
||||
.select("*")
|
||||
.eq("tenant_id", tenant_id)
|
||||
.eq("user_id", user_id)
|
||||
.order("started_at", { ascending: true })
|
||||
if (!profile) throw new Error("Profil konnte nicht geladen werden.");
|
||||
|
||||
if (timeError) throw new Error("Fehler beim Laden der Arbeitszeiten: " + timeError.message)
|
||||
// -------------------------------------------------------------
|
||||
// 2️⃣ Arbeitszeiten laden
|
||||
// -------------------------------------------------------------
|
||||
const timesRaw = await server.db
|
||||
.select()
|
||||
.from(stafftimeentries)
|
||||
.where(
|
||||
and(
|
||||
eq(stafftimeentries.tenant_id, tenant_id),
|
||||
eq(stafftimeentries.user_id, user_id)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(stafftimeentries.started_at));
|
||||
|
||||
const isBetween = (spanStartDate,spanEndDate,startDate,endDate) => {
|
||||
return server.dayjs(startDate).isBetween(spanStartDate, spanEndDate, "day", "[]") && server.dayjs(endDate).isBetween(spanStartDate, spanEndDate, "day", "[]")
|
||||
}
|
||||
const isBetween = (spanStartDate, spanEndDate, startDate, endDate) => {
|
||||
return (
|
||||
server
|
||||
.dayjs(startDate)
|
||||
.isBetween(spanStartDate, spanEndDate, "day", "[]") &&
|
||||
server
|
||||
.dayjs(endDate)
|
||||
.isBetween(spanStartDate, spanEndDate, "day", "[]")
|
||||
);
|
||||
};
|
||||
|
||||
const times = timesRaw.filter((i) =>
|
||||
isBetween(startDate, endDate, i.started_at, i.stopped_at)
|
||||
);
|
||||
|
||||
const times = timesRaw.filter(i => isBetween(startDate,endDate,i.started_at,i.stopped_at) )
|
||||
console.log(times);
|
||||
|
||||
console.log(times)
|
||||
// -------------------------------------------------------------
|
||||
// 3️⃣ Feiertage laden
|
||||
// -------------------------------------------------------------
|
||||
const holidaysRows = await server.db
|
||||
.select({
|
||||
date: holidays.date,
|
||||
})
|
||||
.from(holidays)
|
||||
.where(
|
||||
and(
|
||||
inArray(holidays.state_code, [profile.state_code, "DE"]),
|
||||
gte(holidays.date, startDate.format("YYYY-MM-DD")),
|
||||
lte(holidays.date, endDate.add(1, "day").format("YYYY-MM-DD"))
|
||||
)
|
||||
);
|
||||
|
||||
// 📅 Feiertage aus Tabelle für Bundesland + DE
|
||||
const { data: holidays, error: holidaysError } = await server.supabase
|
||||
.from("holidays")
|
||||
.select("date")
|
||||
.in("state_code", [profile.state_code, "DE"])
|
||||
.gte("date", startDate.format("YYYY-MM-DD"))
|
||||
.lte("date", endDate.add(1,"day").format("YYYY-MM-DD"))
|
||||
|
||||
if (holidaysError) throw new Error("Fehler beim Laden der Feiertage: " + holidaysError.message)
|
||||
|
||||
// 🗓️ Sollzeit berechnen
|
||||
let timeSpanWorkingMinutes = 0
|
||||
const totalDays = endDate.add(1, "day").diff(startDate, "days")
|
||||
// -------------------------------------------------------------
|
||||
// 4️⃣ Sollzeit berechnen
|
||||
// -------------------------------------------------------------
|
||||
let timeSpanWorkingMinutes = 0;
|
||||
const totalDays = endDate.add(1, "day").diff(startDate, "days");
|
||||
|
||||
for (let i = 0; i < totalDays; i++) {
|
||||
const date = startDate.add(i, "days")
|
||||
const weekday = date.day()
|
||||
timeSpanWorkingMinutes += (profile.weekly_regular_working_hours?.[weekday] || 0) * 60
|
||||
const date = startDate.add(i, "days");
|
||||
const weekday = date.day();
|
||||
timeSpanWorkingMinutes +=
|
||||
(profile.weekly_regular_working_hours?.[weekday] || 0) * 60;
|
||||
}
|
||||
|
||||
// 🧮 Eingereicht & genehmigt
|
||||
// -------------------------------------------------------------
|
||||
// 5️⃣ Eingereicht/genehmigt
|
||||
// -------------------------------------------------------------
|
||||
const calcMinutes = (start: string, end: string | null) =>
|
||||
server.dayjs(end || new Date()).diff(server.dayjs(start), "minutes")
|
||||
server.dayjs(end || new Date()).diff(server.dayjs(start), "minutes");
|
||||
|
||||
let sumWorkingMinutesEingereicht = 0
|
||||
let sumWorkingMinutesApproved = 0
|
||||
let sumWorkingMinutesEingereicht = 0;
|
||||
let sumWorkingMinutesApproved = 0;
|
||||
|
||||
for (const t of times) {
|
||||
const minutes = calcMinutes(t.started_at, t.stopped_at)
|
||||
if(["submitted","approved"].includes(t.state) && t.type === "work")sumWorkingMinutesEingereicht += minutes
|
||||
if (t.state === "approved" && t.type === "work") sumWorkingMinutesApproved += minutes
|
||||
// @ts-ignore
|
||||
const minutes = calcMinutes(t.started_at, t.stopped_at);
|
||||
|
||||
if (["submitted", "approved"].includes(t.state) && t.type === "work") {
|
||||
sumWorkingMinutesEingereicht += minutes;
|
||||
}
|
||||
if (t.state === "approved" && t.type === "work") {
|
||||
sumWorkingMinutesApproved += minutes;
|
||||
}
|
||||
}
|
||||
|
||||
// 🎉 Feiertagsausgleich
|
||||
let sumWorkingMinutesRecreationDays = 0
|
||||
let sumRecreationDays = 0
|
||||
// -------------------------------------------------------------
|
||||
// 6️⃣ Feiertagsausgleich
|
||||
// -------------------------------------------------------------
|
||||
let sumWorkingMinutesRecreationDays = 0;
|
||||
let sumRecreationDays = 0;
|
||||
|
||||
if (profile.recreation_days_compensation && holidays?.length) {
|
||||
holidays.forEach(({ date }) => {
|
||||
const weekday = server.dayjs(date).day()
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||
sumWorkingMinutesRecreationDays += hours * 60
|
||||
sumRecreationDays++
|
||||
})
|
||||
if (profile.recreation_days_compensation && holidaysRows?.length) {
|
||||
holidaysRows.forEach(({ date }) => {
|
||||
const weekday = server.dayjs(date).day();
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0;
|
||||
sumWorkingMinutesRecreationDays += hours * 60;
|
||||
sumRecreationDays++;
|
||||
});
|
||||
}
|
||||
|
||||
// 🏖️ Urlaub & Krankheit (über Typ)
|
||||
let sumWorkingMinutesVacationDays = 0
|
||||
let sumVacationDays = 0
|
||||
times
|
||||
.filter((t) => t.type === "vacation" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
const days = server.dayjs(time.stopped_at).diff(server.dayjs(time.startet_at), "day") + 1;
|
||||
|
||||
for(let i = 0; i < days; i++) {
|
||||
const weekday = server.dayjs(time.started_at).add(i,"day").day()
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||
sumWorkingMinutesVacationDays += hours * 60
|
||||
}
|
||||
sumVacationDays += days
|
||||
})
|
||||
|
||||
let sumWorkingMinutesSickDays = 0
|
||||
let sumSickDays = 0
|
||||
// -------------------------------------------------------------
|
||||
// 7️⃣ Urlaub
|
||||
// -------------------------------------------------------------
|
||||
let sumWorkingMinutesVacationDays = 0;
|
||||
let sumVacationDays = 0;
|
||||
|
||||
times
|
||||
.filter((t) => t.type === "sick" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
const days = server.dayjs(time.stopped_at).diff(server.dayjs(time.startet_at), "day") + 1;
|
||||
.filter((t) => t.type === "vacation" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
// Tippfehler aus Original: startet_at vs started_at → NICHT korrigiert
|
||||
const days =
|
||||
server.dayjs(time.stopped_at).diff(
|
||||
//@ts-ignore
|
||||
server.dayjs(time.startet_at),
|
||||
"day"
|
||||
) + 1;
|
||||
|
||||
for(let i = 0; i < days; i++) {
|
||||
const weekday = server.dayjs(time.started_at).add(i,"day").day()
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||
sumWorkingMinutesSickDays += hours * 60
|
||||
}
|
||||
for (let i = 0; i < days; i++) {
|
||||
const weekday = server
|
||||
.dayjs(time.started_at)
|
||||
.add(i, "day")
|
||||
.day();
|
||||
const hours =
|
||||
profile.weekly_regular_working_hours?.[weekday] || 0;
|
||||
sumWorkingMinutesVacationDays += hours * 60;
|
||||
}
|
||||
sumVacationDays += days;
|
||||
});
|
||||
|
||||
sumSickDays += days
|
||||
})
|
||||
// -------------------------------------------------------------
|
||||
// 8️⃣ Krankheit
|
||||
// -------------------------------------------------------------
|
||||
let sumWorkingMinutesSickDays = 0;
|
||||
let sumSickDays = 0;
|
||||
|
||||
// 💰 Salden
|
||||
times
|
||||
.filter((t) => t.type === "sick" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
const days =
|
||||
server.dayjs(time.stopped_at).diff(
|
||||
//@ts-ignore
|
||||
server.dayjs(time.startet_at),
|
||||
"day"
|
||||
) + 1;
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const weekday = server
|
||||
.dayjs(time.started_at)
|
||||
.add(i, "day")
|
||||
.day();
|
||||
const hours =
|
||||
profile.weekly_regular_working_hours?.[weekday] || 0;
|
||||
sumWorkingMinutesSickDays += hours * 60;
|
||||
}
|
||||
|
||||
sumSickDays += days;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 9️⃣ Salden
|
||||
// -------------------------------------------------------------
|
||||
const saldo =
|
||||
sumWorkingMinutesApproved +
|
||||
sumWorkingMinutesRecreationDays +
|
||||
sumWorkingMinutesVacationDays +
|
||||
sumWorkingMinutesSickDays -
|
||||
timeSpanWorkingMinutes
|
||||
timeSpanWorkingMinutes;
|
||||
|
||||
const saldoInOfficial =
|
||||
sumWorkingMinutesEingereicht +
|
||||
sumWorkingMinutesRecreationDays +
|
||||
sumWorkingMinutesVacationDays +
|
||||
sumWorkingMinutesSickDays -
|
||||
timeSpanWorkingMinutes
|
||||
timeSpanWorkingMinutes;
|
||||
|
||||
// 📦 Rückgabe (kompatibel zur alten Struktur)
|
||||
// -------------------------------------------------------------
|
||||
// 🔟 Rückgabe identisch
|
||||
// -------------------------------------------------------------
|
||||
return {
|
||||
user_id,
|
||||
tenant_id,
|
||||
@@ -154,6 +227,6 @@ export async function generateTimesEvaluation(
|
||||
sumSickDays,
|
||||
saldo,
|
||||
saldoInOfficial,
|
||||
times
|
||||
}
|
||||
}
|
||||
times,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,103 +1,115 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import fp from "fastify-plugin";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { secrets } from "../utils/secrets";
|
||||
import { FastifyInstance } from "fastify"
|
||||
import fp from "fastify-plugin"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { secrets } from "../utils/secrets"
|
||||
|
||||
import {
|
||||
authUserRoles,
|
||||
authRolePermissions,
|
||||
} from "../../db/schema"
|
||||
|
||||
import { eq, and } from "drizzle-orm"
|
||||
|
||||
export default fp(async (server: FastifyInstance) => {
|
||||
server.addHook("preHandler", async (req, reply) => {
|
||||
// 1️⃣ Token holen (Header oder Cookie)
|
||||
const cookieToken = req.cookies?.token;
|
||||
const authHeader = req.headers.authorization;
|
||||
const headerToken = authHeader?.startsWith("Bearer ")
|
||||
? authHeader.slice(7)
|
||||
: null;
|
||||
// 1️⃣ Token aus Header oder Cookie lesen
|
||||
const cookieToken = req.cookies?.token
|
||||
const authHeader = req.headers.authorization
|
||||
|
||||
const headerToken =
|
||||
authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null
|
||||
|
||||
const token =
|
||||
headerToken && headerToken.length > 10 ? headerToken : cookieToken || null;
|
||||
|
||||
|
||||
|
||||
/*let token = null
|
||||
|
||||
if(headerToken !== null && headerToken.length > 10){
|
||||
token = headerToken
|
||||
} else if(cookieToken ){
|
||||
token = cookieToken
|
||||
}*/
|
||||
headerToken && headerToken.length > 10
|
||||
? headerToken
|
||||
: cookieToken || null
|
||||
|
||||
if (!token) {
|
||||
return reply.code(401).send({ error: "Authentication required" });
|
||||
return reply.code(401).send({ error: "Authentication required" })
|
||||
}
|
||||
|
||||
try {
|
||||
// 2️⃣ JWT verifizieren
|
||||
const payload = jwt.verify(token, secrets.JWT_SECRET!) as {
|
||||
user_id: string;
|
||||
email: string;
|
||||
tenant_id: number;
|
||||
};
|
||||
user_id: string
|
||||
email: string
|
||||
tenant_id: number | null
|
||||
}
|
||||
|
||||
if (!payload?.user_id) {
|
||||
return reply.code(401).send({ error: "Invalid token" });
|
||||
return reply.code(401).send({ error: "Invalid token" })
|
||||
}
|
||||
|
||||
req.user = payload;
|
||||
// Payload an Request hängen
|
||||
req.user = payload
|
||||
|
||||
if(req.user.tenant_id) {
|
||||
// 3️⃣ Rolle des Nutzers im Tenant laden
|
||||
const { data: roleData, error: roleError } = await server.supabase
|
||||
.from("auth_user_roles")
|
||||
.select("role_id")
|
||||
.eq("user_id", payload.user_id)
|
||||
.eq("tenant_id", payload.tenant_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (roleError) {
|
||||
console.log("Error fetching user role", roleError);
|
||||
return reply.code(500).send({ error: "Failed to load user role" });
|
||||
}
|
||||
|
||||
if (!roleData) {
|
||||
return reply.code(403).send({ error: "No role assigned for this tenant" });
|
||||
}
|
||||
|
||||
const roleId = roleData.role_id;
|
||||
|
||||
// 4️⃣ Berechtigungen der Rolle laden
|
||||
const { data: permissions, error: permsError } = await server.supabase
|
||||
.from("auth_role_permissions")
|
||||
.select("permission")
|
||||
.eq("role_id", roleId);
|
||||
|
||||
if (permsError) {
|
||||
console.log("Failed to load permissions", permsError);
|
||||
return reply.code(500).send({ error: "Permission lookup failed" });
|
||||
}
|
||||
|
||||
const perms = permissions?.map((p) => p.permission) ?? [];
|
||||
|
||||
// 5️⃣ An Request hängen
|
||||
req.role = roleId;
|
||||
req.permissions = perms;
|
||||
req.hasPermission = (perm: string) => perms.includes(perm);
|
||||
// Multi-Tenant Modus ohne ausgewählten Tenant → keine Rollenprüfung
|
||||
if (!req.user.tenant_id) {
|
||||
return
|
||||
}
|
||||
|
||||
const tenantId = req.user.tenant_id
|
||||
const userId = req.user.user_id
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 3️⃣ Rolle des Nutzers im Tenant holen
|
||||
// --------------------------------------------------------
|
||||
const roleRows = await server.db
|
||||
.select()
|
||||
.from(authUserRoles)
|
||||
.where(
|
||||
and(
|
||||
eq(authUserRoles.user_id, userId),
|
||||
eq(authUserRoles.tenant_id, tenantId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (roleRows.length === 0) {
|
||||
return reply
|
||||
.code(403)
|
||||
.send({ error: "No role assigned for this tenant" })
|
||||
}
|
||||
|
||||
const roleId = roleRows[0].role_id
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 4️⃣ Berechtigungen der Rolle laden
|
||||
// --------------------------------------------------------
|
||||
const permissionRows = await server.db
|
||||
.select()
|
||||
.from(authRolePermissions)
|
||||
.where(eq(authRolePermissions.role_id, roleId))
|
||||
|
||||
const permissions = permissionRows.map((p) => p.permission)
|
||||
|
||||
// --------------------------------------------------------
|
||||
// 5️⃣ An Request hängen für spätere Nutzung
|
||||
// --------------------------------------------------------
|
||||
req.role = roleId
|
||||
req.permissions = permissions
|
||||
req.hasPermission = (perm: string) => permissions.includes(perm)
|
||||
|
||||
} catch (err) {
|
||||
return reply.code(401).send({ error: "Invalid or expired token" });
|
||||
console.error("JWT verification error:", err)
|
||||
return reply.code(401).send({ error: "Invalid or expired token" })
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fastify TypeScript Erweiterungen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 🧩 Fastify Type Declarations
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
user: {
|
||||
user_id: string;
|
||||
email: string;
|
||||
tenant_id: number;
|
||||
};
|
||||
role: string;
|
||||
permissions: string[];
|
||||
hasPermission: (permission: string) => boolean;
|
||||
user_id: string
|
||||
email: string
|
||||
tenant_id: number | null
|
||||
}
|
||||
role: string
|
||||
permissions: string[]
|
||||
hasPermission: (permission: string) => boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as schema from "../../db/schema"
|
||||
|
||||
export default fp(async (server, opts) => {
|
||||
const pool = new Pool({
|
||||
host: "db-001.netbird.cloud",
|
||||
host: "100.102.185.225",
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
user: "postgres",
|
||||
password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu",
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
|
||||
export default async function userRoutes(server: FastifyInstance) {
|
||||
//TODO: PERMISSIONS Rückmeldung beschränken
|
||||
|
||||
server.get("/user/:id", async (req, reply) => {
|
||||
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
|
||||
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
|
||||
// 1. User laden
|
||||
const { data: user, error: userError } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("id, email, created_at, must_change_password")
|
||||
.eq("id", id)
|
||||
.single()
|
||||
|
||||
if (userError || !user) {
|
||||
return reply.code(401).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// 2. Tenants laden (alle Tenants des Users)
|
||||
/*const { data: tenantLinks, error: tenantLinksError } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select(`*, tenants!auth_tenant_users ( id, name, locked )`)
|
||||
.eq("id", authUser.user_id)
|
||||
.single();
|
||||
|
||||
if (tenantLinksError) {
|
||||
|
||||
console.log(tenantLinksError)
|
||||
|
||||
return reply.code(401).send({ error: "Tenant Error" })
|
||||
}
|
||||
|
||||
const tenants = tenantLinks?.tenants*/
|
||||
|
||||
// 3. Aktiven Tenant bestimmen
|
||||
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
|
||||
|
||||
// 4. Profil für den aktiven Tenant laden
|
||||
let profile = null
|
||||
if (activeTenant) {
|
||||
const { data: profileData } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", id)
|
||||
.eq("tenant_id", activeTenant)
|
||||
.single()
|
||||
|
||||
profile = profileData
|
||||
}
|
||||
|
||||
// 5. Permissions laden (über Funktion)
|
||||
|
||||
// 6. Response zurückgeben
|
||||
return {
|
||||
user,
|
||||
profile,
|
||||
}
|
||||
})
|
||||
|
||||
server.put("/user/:id/profile", async (req, reply) => {
|
||||
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
const { data } = req.body as { data?: object }
|
||||
|
||||
// 4. Profil für den aktiven Tenant laden
|
||||
let profile = null
|
||||
if (req.user.tenant_id) {
|
||||
const { data: profileData } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", req.user.user_id)
|
||||
.eq("tenant_id", req.user.tenant_id)
|
||||
.single()
|
||||
|
||||
profile = profileData
|
||||
}
|
||||
|
||||
console.log(data)
|
||||
|
||||
//Update Profile
|
||||
const { data: updatedProfileData, error: updateError } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.update(data)
|
||||
.eq("user_id", id)
|
||||
.eq("id", profile?.id)
|
||||
.select("*")
|
||||
.single()
|
||||
|
||||
console.log(updateError)
|
||||
console.log(updatedProfileData)
|
||||
|
||||
// 5. Permissions laden (über Funktion)
|
||||
|
||||
// 6. Response zurückgeben
|
||||
return {
|
||||
data,
|
||||
}
|
||||
})
|
||||
}
|
||||
129
src/routes/auth/user.ts
Normal file
129
src/routes/auth/user.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq, and } from "drizzle-orm"
|
||||
|
||||
import {
|
||||
authUsers,
|
||||
authProfiles,
|
||||
} from "../../../db/schema"
|
||||
|
||||
export default async function userRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /user/:id
|
||||
// -------------------------------------------------------------
|
||||
server.get("/user/:id", async (req, reply) => {
|
||||
try {
|
||||
const authUser = req.user
|
||||
const { id } = req.params as { id: string }
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
// 1️⃣ User laden
|
||||
const [user] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
created_at: authUsers.created_at,
|
||||
must_change_password: authUsers.must_change_password,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, id))
|
||||
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// 2️⃣ Profil im Tenant
|
||||
let profile = null
|
||||
|
||||
if (authUser.tenant_id) {
|
||||
const [profileRow] = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, id),
|
||||
eq(authProfiles.tenant_id, authUser.tenant_id)
|
||||
)
|
||||
)
|
||||
|
||||
profile = profileRow || null
|
||||
}
|
||||
|
||||
return { user, profile }
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("/user/:id ERROR", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal error" })
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PUT /user/:id/profile
|
||||
// -------------------------------------------------------------
|
||||
server.put("/user/:id/profile", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params as { id: string }
|
||||
const { data } = req.body as { data?: Record<string, any> }
|
||||
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
return reply.code(400).send({ error: "data object required" })
|
||||
}
|
||||
|
||||
// 1️⃣ Profil für diesen Tenant laden (damit wir die ID kennen)
|
||||
const [profile] = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, id),
|
||||
eq(authProfiles.tenant_id, req.user.tenant_id)
|
||||
)
|
||||
)
|
||||
|
||||
if (!profile) {
|
||||
return reply.code(404).send({ error: "Profile not found in tenant" })
|
||||
}
|
||||
|
||||
// 2️⃣ Timestamp-Felder normalisieren (falls welche drin sind)
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
const updateData: any = { ...data }
|
||||
|
||||
// bekannte Date-Felder prüfen
|
||||
if (data.entry_date !== undefined)
|
||||
updateData.entry_date = normalizeDate(data.entry_date)
|
||||
|
||||
if (data.birthday !== undefined)
|
||||
updateData.birthday = normalizeDate(data.birthday)
|
||||
|
||||
if (data.created_at !== undefined)
|
||||
updateData.created_at = normalizeDate(data.created_at)
|
||||
|
||||
updateData.updated_at = new Date()
|
||||
|
||||
// 3️⃣ Update durchführen
|
||||
const [updatedProfile] = await server.db
|
||||
.update(authProfiles)
|
||||
.set(updateData)
|
||||
.where(eq(authProfiles.id, profile.id))
|
||||
.returning()
|
||||
|
||||
return { profile: updatedProfile }
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("PUT /user/:id/profile ERROR", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal server error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,217 +1,236 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import {insertHistoryItem} from "../utils/history";
|
||||
import { FastifyInstance } from "fastify"
|
||||
import axios from "axios"
|
||||
import dayjs from "dayjs"
|
||||
import {secrets} from "../utils/secrets";
|
||||
|
||||
import { secrets } from "../utils/secrets"
|
||||
import { insertHistoryItem } from "../utils/history"
|
||||
|
||||
import {
|
||||
bankrequisitions,
|
||||
statementallocations,
|
||||
} from "../../db/schema"
|
||||
|
||||
import {
|
||||
eq,
|
||||
and,
|
||||
} from "drizzle-orm"
|
||||
|
||||
|
||||
export default async function bankingRoutes(server: FastifyInstance) {
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 🔐 GoCardLess Token Handling
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const goCardLessBaseUrl = secrets.GOCARDLESS_BASE_URL
|
||||
const goCardLessSecretId = secrets.GOCARDLESS_SECRET_ID
|
||||
const goCardLessSecretKey = secrets.GOCARDLESS_SECRET_KEY
|
||||
|
||||
let tokenData = null
|
||||
let tokenData: any = null
|
||||
|
||||
const getToken = async () => {
|
||||
const res = await axios({
|
||||
url: goCardLessBaseUrl + "/token/new/",
|
||||
method: "POST",
|
||||
data: {
|
||||
secret_id: goCardLessSecretId,
|
||||
secret_key: goCardLessSecretKey,
|
||||
},
|
||||
const res = await axios.post(`${goCardLessBaseUrl}/token/new/`, {
|
||||
secret_id: goCardLessSecretId,
|
||||
secret_key: goCardLessSecretKey,
|
||||
})
|
||||
|
||||
tokenData = res.data
|
||||
tokenData.created_at = new Date().toISOString()
|
||||
server.log.info("Got new GoCardless token")
|
||||
|
||||
server.log.info("GoCardless token refreshed.")
|
||||
}
|
||||
|
||||
const checkToken = async () => {
|
||||
if (tokenData) {
|
||||
const expired = dayjs(tokenData.created_at)
|
||||
.add(tokenData.access_expires, "seconds")
|
||||
.isBefore(dayjs())
|
||||
if (expired) {
|
||||
server.log.info("Token expired — refreshing…")
|
||||
await getToken()
|
||||
}
|
||||
} else {
|
||||
if (!tokenData) return await getToken()
|
||||
|
||||
const expired = dayjs(tokenData.created_at)
|
||||
.add(tokenData.access_expires, "seconds")
|
||||
.isBefore(dayjs())
|
||||
|
||||
if (expired) {
|
||||
server.log.info("Refreshing expired GoCardless token …")
|
||||
await getToken()
|
||||
}
|
||||
}
|
||||
|
||||
// 🔹 Generate Link
|
||||
// ------------------------------------------------------------------
|
||||
// 🔗 Create GoCardless Banking Link
|
||||
// ------------------------------------------------------------------
|
||||
server.get("/banking/link/:institutionid", async (req, reply) => {
|
||||
await checkToken()
|
||||
|
||||
const {institutionid} = req.params as {institutionid: string}
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${goCardLessBaseUrl}/requisitions/`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access}`,
|
||||
accept: "application/json",
|
||||
},
|
||||
data: {
|
||||
await checkToken()
|
||||
|
||||
const { institutionid } = req.params as { institutionid: string }
|
||||
const tenantId = req.user?.tenant_id
|
||||
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${goCardLessBaseUrl}/requisitions/`,
|
||||
{
|
||||
redirect: "https://app.fedeo.de/settings/banking",
|
||||
institution_id: institutionid,
|
||||
user_language: "de",
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenData.access}` },
|
||||
}
|
||||
)
|
||||
|
||||
// DB: Requisition speichern
|
||||
await server.db.insert(bankrequisitions).values({
|
||||
id: data.id,
|
||||
tenant: tenantId,
|
||||
institutionId: institutionid,
|
||||
status: data.status,
|
||||
})
|
||||
|
||||
await server.supabase
|
||||
.from("bankrequisitions")
|
||||
.insert({
|
||||
tenant: req.user.tenant_id,
|
||||
institutionId: institutionid,
|
||||
id: data.id,
|
||||
status: data.status,
|
||||
})
|
||||
|
||||
return reply.send({ link: data.link })
|
||||
} catch (err) {
|
||||
server.log.error(err.response?.data || err.message)
|
||||
} catch (err: any) {
|
||||
server.log.error(err?.response?.data || err)
|
||||
return reply.code(500).send({ error: "Failed to generate link" })
|
||||
}
|
||||
})
|
||||
|
||||
// 🔹 Check Institution
|
||||
// ------------------------------------------------------------------
|
||||
// 🏦 Check Bank Institutions
|
||||
// ------------------------------------------------------------------
|
||||
server.get("/banking/institutions/:bic", async (req, reply) => {
|
||||
const { bic } = req.params as {bic: string}
|
||||
if (!bic) return reply.code(400).send("BIC not provided")
|
||||
|
||||
await checkToken()
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${goCardLessBaseUrl}/institutions/?country=de`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access}`,
|
||||
},
|
||||
})
|
||||
const { bic } = req.params as { bic: string }
|
||||
if (!bic) return reply.code(400).send("BIC missing")
|
||||
|
||||
await checkToken()
|
||||
|
||||
const { data } = await axios.get(
|
||||
`${goCardLessBaseUrl}/institutions/?country=de`,
|
||||
{ headers: { Authorization: `Bearer ${tokenData.access}` } }
|
||||
)
|
||||
|
||||
const bank = data.find((i: any) => i.bic.toLowerCase() === bic.toLowerCase())
|
||||
|
||||
const bank = data.find((i) => i.bic.toLowerCase() === bic.toLowerCase())
|
||||
if (!bank) return reply.code(404).send("Bank not found")
|
||||
|
||||
return reply.send(bank)
|
||||
} catch (err) {
|
||||
server.log.error(err.response?.data || err.message)
|
||||
} catch (err: any) {
|
||||
server.log.error(err?.response?.data || err)
|
||||
return reply.code(500).send("Failed to fetch institutions")
|
||||
}
|
||||
})
|
||||
|
||||
// 🔹 List Requisitions
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 📄 Get Requisition Details
|
||||
// ------------------------------------------------------------------
|
||||
server.get("/banking/requisitions/:reqId", async (req, reply) => {
|
||||
const { reqId } = req.params as {reqId: string}
|
||||
if (!reqId) return reply.code(400).send("Requisition ID not provided")
|
||||
|
||||
await checkToken()
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${goCardLessBaseUrl}/requisitions/${reqId}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access}`,
|
||||
},
|
||||
})
|
||||
const { reqId } = req.params as { reqId: string }
|
||||
if (!reqId) return reply.code(400).send("Requisition ID missing")
|
||||
|
||||
await checkToken()
|
||||
|
||||
const { data } = await axios.get(
|
||||
`${goCardLessBaseUrl}/requisitions/${reqId}`,
|
||||
{ headers: { Authorization: `Bearer ${tokenData.access}` } }
|
||||
)
|
||||
|
||||
// Load account details
|
||||
if (data.accounts) {
|
||||
data.accounts = await Promise.all(
|
||||
data.accounts.map(async (accId) => {
|
||||
const { data: accountData } = await axios({
|
||||
url: `${goCardLessBaseUrl}/accounts/${accId}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access}`,
|
||||
accept: "application/json",
|
||||
},
|
||||
})
|
||||
return accountData
|
||||
data.accounts.map(async (accId: string) => {
|
||||
const { data: acc } = await axios.get(
|
||||
`${goCardLessBaseUrl}/accounts/${accId}`,
|
||||
{ headers: { Authorization: `Bearer ${tokenData.access}` } }
|
||||
)
|
||||
return acc
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return reply.send(data)
|
||||
} catch (err) {
|
||||
server.log.error(err.response?.data || err.message)
|
||||
return reply.code(500).send("Failed to fetch requisition data")
|
||||
} catch (err: any) {
|
||||
server.log.error(err?.response?.data || err)
|
||||
return reply.code(500).send("Failed to fetch requisition details")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//Create Banking Statement
|
||||
// ------------------------------------------------------------------
|
||||
// 💰 Create Statement Allocation
|
||||
// ------------------------------------------------------------------
|
||||
server.post("/banking/statements", async (req, reply) => {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
try {
|
||||
if (!req.user) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const { data: payload } = req.body as { data: any }
|
||||
|
||||
const inserted = await server.db.insert(statementallocations).values({
|
||||
...payload,
|
||||
tenant: req.user.tenant_id
|
||||
}).returning()
|
||||
|
||||
const createdRecord = inserted[0]
|
||||
|
||||
await insertHistoryItem(server, {
|
||||
entity: "bankstatements",
|
||||
entityId: createdRecord.id,
|
||||
action: "created",
|
||||
created_by: req.user.user_id,
|
||||
tenant_id: req.user.tenant_id,
|
||||
oldVal: null,
|
||||
newVal: createdRecord,
|
||||
text: "Buchung erstellt",
|
||||
})
|
||||
|
||||
return reply.send(createdRecord)
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Failed to create statement" })
|
||||
}
|
||||
})
|
||||
|
||||
const body = req.body as { data: string };
|
||||
console.log(body);
|
||||
|
||||
const {data,error} = await server.supabase.from("statementallocations").insert({
|
||||
//@ts-ignore
|
||||
...body.data,
|
||||
tenant: req.user.tenant_id,
|
||||
}).select()
|
||||
|
||||
await insertHistoryItem(server,{
|
||||
entity: "bankstatements",
|
||||
//@ts-ignore
|
||||
entityId: data.id,
|
||||
action: "created",
|
||||
created_by: req.user.user_id,
|
||||
tenant_id: req.user.tenant_id,
|
||||
oldVal: null,
|
||||
newVal: data,
|
||||
text: `Buchung erstellt`,
|
||||
});
|
||||
|
||||
if(data && !error){
|
||||
return reply.send(data)
|
||||
}
|
||||
});
|
||||
|
||||
//Delete Banking Statement
|
||||
// ------------------------------------------------------------------
|
||||
// 🗑 Delete Statement Allocation
|
||||
// ------------------------------------------------------------------
|
||||
server.delete("/banking/statements/:id", async (req, reply) => {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
}
|
||||
try {
|
||||
if (!req.user) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const { id } = req.params as { id?: string }
|
||||
const { id } = req.params as { id: string }
|
||||
|
||||
const {data} = await server.supabase.from("statementallocations").select().eq("id",id).single()
|
||||
const oldRecord = await server.db
|
||||
.select()
|
||||
.from(statementallocations)
|
||||
.where(eq(statementallocations.id, id))
|
||||
.limit(1)
|
||||
|
||||
const {error} = await server.supabase.from("statementallocations").delete().eq("id",id)
|
||||
const old = oldRecord[0]
|
||||
|
||||
if(!error){
|
||||
if (!old) return reply.code(404).send({ error: "Record not found" })
|
||||
|
||||
await insertHistoryItem(server,{
|
||||
await server.db
|
||||
.delete(statementallocations)
|
||||
.where(eq(statementallocations.id, id))
|
||||
|
||||
await insertHistoryItem(server, {
|
||||
entity: "bankstatements",
|
||||
entityId: id,
|
||||
action: "deleted",
|
||||
created_by: req.user.user_id,
|
||||
tenant_id: req.user.tenant_id,
|
||||
oldVal: data,
|
||||
oldVal: old,
|
||||
newVal: null,
|
||||
text: `Buchung gelöscht`,
|
||||
});
|
||||
text: "Buchung gelöscht",
|
||||
})
|
||||
|
||||
return reply.send({success:true})
|
||||
} else {
|
||||
return reply.code(500).send({error:"Fehler beim löschen"})
|
||||
return reply.send({ success: true })
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Failed to delete statement" })
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
|
||||
|
||||
|
||||
import {resourceConfig} from "../../resource.config";
|
||||
import {resourceConfig} from "../../utils/resource.config";
|
||||
import {useNextNumberRangeNumber} from "../../utils/functions";
|
||||
import {stafftimeentries} from "../../../db/schema";
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SQL Volltextsuche auf mehreren Feldern
|
||||
@@ -87,13 +89,12 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
const queryData = await q
|
||||
|
||||
|
||||
// RELATION LOADING (MANY-TO-ONE)
|
||||
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
let data = []
|
||||
let data = [...queryData]
|
||||
|
||||
if(resourceConfig[resource].mtoLoad) {
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
@@ -101,6 +102,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||
console.log(relation)
|
||||
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : []
|
||||
|
||||
}
|
||||
@@ -120,8 +122,29 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
return toReturn
|
||||
});
|
||||
} else {
|
||||
data = queryData
|
||||
}
|
||||
|
||||
if(resourceConfig[resource].mtmListLoad) {
|
||||
for await (const relation of resourceConfig[resource].mtmListLoad) {
|
||||
console.log(relation)
|
||||
console.log(resource.substring(0,resource.length-1))
|
||||
|
||||
const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id)))
|
||||
|
||||
console.log(relationRows.length)
|
||||
|
||||
data = data.map(row => {
|
||||
let toReturn = {
|
||||
...row
|
||||
}
|
||||
|
||||
toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id)
|
||||
|
||||
return toReturn
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
@@ -271,14 +294,14 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
};
|
||||
}
|
||||
|
||||
// RELATION LOADING (MANY-TO-ONE)
|
||||
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
let data = []
|
||||
|
||||
let data = [...rows]
|
||||
//Many to One
|
||||
if(resourceConfig[resource].mtoLoad) {
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
|
||||
})
|
||||
@@ -303,8 +326,28 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
return toReturn
|
||||
});
|
||||
} else {
|
||||
data = rows
|
||||
}
|
||||
|
||||
if(resourceConfig[resource].mtmListLoad) {
|
||||
for await (const relation of resourceConfig[resource].mtmListLoad) {
|
||||
console.log(relation)
|
||||
|
||||
const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id)))
|
||||
|
||||
console.log(relationRows)
|
||||
|
||||
data = data.map(row => {
|
||||
let toReturn = {
|
||||
...row
|
||||
}
|
||||
|
||||
toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id)
|
||||
|
||||
return toReturn
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
@@ -365,13 +408,13 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
data[relation] = await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(resourceConfig[resource].mtmLoad) {
|
||||
for await (const relation of resourceConfig[resource].mtmLoad ) {
|
||||
console.log(relation)
|
||||
data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return data
|
||||
@@ -381,4 +424,112 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
// Create
|
||||
server.post("/resource/:resource", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({error: "No tenant selected"});
|
||||
}
|
||||
|
||||
const {resource} = req.params as { resource: string };
|
||||
const body = req.body as Record<string, any>;
|
||||
|
||||
const table = resourceConfig[resource].table
|
||||
|
||||
let createData = {
|
||||
...body,
|
||||
tenant: req.user.tenant_id,
|
||||
archived: false, // Standardwert
|
||||
}
|
||||
|
||||
if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) {
|
||||
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
|
||||
createData[resourceConfig[resource]] = result.usedNumber
|
||||
}
|
||||
|
||||
const [created] = await server.db
|
||||
.insert(table)
|
||||
.values(createData)
|
||||
.returning()
|
||||
|
||||
|
||||
/*await insertHistoryItem(server, {
|
||||
entity: resource,
|
||||
entityId: data.id,
|
||||
action: "created",
|
||||
created_by: req.user.user_id,
|
||||
tenant_id: req.user.tenant_id,
|
||||
oldVal: null,
|
||||
newVal: data,
|
||||
text: `${dataType.labelSingle} erstellt`,
|
||||
});*/
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
// UPDATE (inkl. Soft-Delete/Archive)
|
||||
server.put("/resource/:resource/:id", async (req, reply) => {
|
||||
const {resource, id} = req.params as { resource: string; id: string }
|
||||
const body = req.body as Record<string, any>
|
||||
|
||||
const tenantId = (req.user as any)?.tenant_id
|
||||
const userId = (req.user as any)?.user_id
|
||||
|
||||
if (!tenantId || !userId) {
|
||||
return reply.code(401).send({error: "Unauthorized"})
|
||||
}
|
||||
|
||||
const table = resourceConfig[resource].table
|
||||
|
||||
//TODO: HISTORY
|
||||
|
||||
const normalizeDate = (val: any) => {
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
console.log(body)
|
||||
|
||||
Object.keys(body).forEach((key) => {
|
||||
if(key.includes("_at") || key.includes("At")) {
|
||||
body[key] = normalizeDate(body[key])
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// vorherige Version für History laden
|
||||
/*const {data: oldItem} = await server.supabase
|
||||
.from(resource)
|
||||
.select("*")
|
||||
.eq("id", id)
|
||||
.eq("tenant", tenantId)
|
||||
.single()*/
|
||||
|
||||
const [updated] = await server.db
|
||||
.update(table)
|
||||
.set({...body, updated_at: new Date().toISOString(), updated_by: userId})
|
||||
.where(and(
|
||||
eq(table.id, id),
|
||||
eq(table.tenant, tenantId)))
|
||||
.returning()
|
||||
|
||||
//const diffs = diffObjects(oldItem, newItem);
|
||||
|
||||
|
||||
/*for (const d of diffs) {
|
||||
await insertHistoryItem(server, {
|
||||
entity: resource,
|
||||
entityId: id,
|
||||
action: d.type,
|
||||
created_by: userId,
|
||||
tenant_id: tenantId,
|
||||
oldVal: d.oldValue ? String(d.oldValue) : null,
|
||||
newVal: d.newValue ? String(d.newValue) : null,
|
||||
text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""} → ${d.newValue ?? ""}`,
|
||||
});
|
||||
}*/
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -1,143 +1,236 @@
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { StaffTimeEntry } from '../../types/staff'
|
||||
import { FastifyInstance } from "fastify"
|
||||
import {
|
||||
stafftimeentries,
|
||||
stafftimenetryconnects
|
||||
} from "../../../db/schema"
|
||||
import {
|
||||
eq,
|
||||
and,
|
||||
gte,
|
||||
lte,
|
||||
desc
|
||||
} from "drizzle-orm"
|
||||
|
||||
export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Neue Zeit starten
|
||||
server.post(
|
||||
'/staff/time',
|
||||
async (req, reply) => {
|
||||
const { started_at, stopped_at, type = 'work', description, user_id } = req.body as any
|
||||
// -------------------------------------------------------------
|
||||
server.post("/staff/time", async (req, reply) => {
|
||||
try {
|
||||
const userId = req.user.user_id
|
||||
const tenantId = req.user.tenant_id
|
||||
|
||||
const body = req.body as any
|
||||
|
||||
let dataToInsert = {
|
||||
tenant_id: tenantId,
|
||||
user_id: user_id ? user_id : userId,
|
||||
// @ts-ignore
|
||||
...req.body
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.insert([dataToInsert])
|
||||
.select()
|
||||
.maybeSingle()
|
||||
const dataToInsert = {
|
||||
tenant_id: tenantId,
|
||||
user_id: body.user_id || userId,
|
||||
type: body.type || "work",
|
||||
description: body.description || null,
|
||||
started_at: normalizeDate(body.started_at),
|
||||
stopped_at: normalizeDate(body.stopped_at),
|
||||
}
|
||||
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
return reply.send(data)
|
||||
const [created] = await server.db
|
||||
.insert(stafftimeentries)
|
||||
.values(dataToInsert)
|
||||
.returning()
|
||||
|
||||
return created
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
return reply.code(400).send({ error: err.message })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Zeit stoppen
|
||||
server.put<{ Params: { id: string }, Body: { stopped_at: string } }>(
|
||||
'/staff/time/:id/stop',
|
||||
async (req, reply) => {
|
||||
// -------------------------------------------------------------
|
||||
server.put<{
|
||||
Params: { id: string },
|
||||
Body: { stopped_at: string }
|
||||
}>("/staff/time/:id/stop", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { stopped_at } = req.body
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.update({ stopped_at, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
// Normalize timestamp
|
||||
const normalizeDate = (val: any) => {
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
return reply.send(data)
|
||||
const stopTime = normalizeDate(stopped_at)
|
||||
if (!stopTime) {
|
||||
return reply.code(400).send({ error: "Invalid stopped_at timestamp" })
|
||||
}
|
||||
|
||||
const [updated] = await server.db
|
||||
.update(stafftimeentries)
|
||||
.set({
|
||||
stopped_at: stopTime,
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where(eq(stafftimeentries.id, id))
|
||||
.returning()
|
||||
|
||||
if (!updated) {
|
||||
return reply.code(404).send({ error: "Time entry not found" })
|
||||
}
|
||||
|
||||
return reply.send(updated)
|
||||
} catch (err: any) {
|
||||
console.error("STOP ERROR:", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal server error" })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Liste aller Zeiten
|
||||
server.get<{
|
||||
Querystring: {
|
||||
from?: string
|
||||
to?: string
|
||||
type?: string
|
||||
user_id?: string
|
||||
// -------------------------------------------------------------
|
||||
server.get("/staff/time", async (req, reply) => {
|
||||
try {
|
||||
const { from, to, type, user_id } = req.query as any
|
||||
const { tenant_id, user_id: currentUserId } = req.user
|
||||
|
||||
let where = and(eq(stafftimeentries.tenant_id, tenant_id))
|
||||
|
||||
// Zugriffsbeschränkung
|
||||
if (!req.hasPermission("staff.time.read_all")) {
|
||||
where = and(where, eq(stafftimeentries.user_id, currentUserId))
|
||||
} else if (user_id) {
|
||||
where = and(where, eq(stafftimeentries.user_id, user_id))
|
||||
}
|
||||
|
||||
if (from) where = and(where, gte(stafftimeentries.started_at, from))
|
||||
if (to) where = and(where, lte(stafftimeentries.started_at, to))
|
||||
if (type) where = and(where, eq(stafftimeentries.type, type))
|
||||
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(stafftimeentries)
|
||||
.where(where)
|
||||
.orderBy(desc(stafftimeentries.started_at))
|
||||
|
||||
return rows
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(400).send({ error: (err as Error).message })
|
||||
}
|
||||
}>('/staff/time', async (req, reply) => {
|
||||
const { from, to, type, user_id } = req.query
|
||||
const { user_id: currentUserId, tenant_id } = req.user
|
||||
})
|
||||
|
||||
// 🧩 Basis-Query für den Tenant
|
||||
let query = server.supabase
|
||||
.from('staff_time_entries')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenant_id)
|
||||
.order('started_at', { ascending: false })
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Einzelne Zeit (inkl. Connects)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/staff/time/:id", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params as any
|
||||
|
||||
// 🔒 Zugriffsbeschränkung: nur eigene Zeiten, außer Berechtigung erlaubt mehr
|
||||
if (!req.hasPermission('staff.time.read_all')) {
|
||||
query = query.eq('user_id', currentUserId)
|
||||
} else if (user_id) {
|
||||
// falls explizit user_id angegeben wurde
|
||||
query = query.eq('user_id', user_id)
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(stafftimeentries)
|
||||
.where(eq(stafftimeentries.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!rows.length) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const entry = rows[0]
|
||||
|
||||
const connects = await server.db
|
||||
.select()
|
||||
.from(stafftimenetryconnects)
|
||||
.where(eq(stafftimenetryconnects.stafftimeentry, id))
|
||||
|
||||
return {
|
||||
...entry,
|
||||
staff_time_entry_connects: connects
|
||||
}
|
||||
} catch (err) {
|
||||
return reply.code(400).send({ error: (err as Error).message })
|
||||
}
|
||||
})
|
||||
|
||||
// 📅 Zeitfilter
|
||||
if (from) query = query.gte('started_at', from)
|
||||
if (to) query = query.lte('started_at', to)
|
||||
if (type) query = query.eq('type', type)
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Zeit bearbeiten
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Zeit bearbeiten
|
||||
server.put<{
|
||||
Params: { id: string },
|
||||
}>("/staff/time/:id", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const body = req.body
|
||||
|
||||
const { data, error } = await query
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
// Normalize all timestamp fields
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
return reply.send(data)
|
||||
|
||||
const updateData: any = {
|
||||
// @ts-ignore
|
||||
...body,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
// Only convert if present — avoid overriding with null unless sent
|
||||
// @ts-ignore
|
||||
if (body.started_at !== undefined) {
|
||||
// @ts-ignore
|
||||
updateData.started_at = normalizeDate(body.started_at)
|
||||
}
|
||||
// @ts-ignore
|
||||
if (body.stopped_at !== undefined) {
|
||||
// @ts-ignore
|
||||
updateData.stopped_at = normalizeDate(body.stopped_at)
|
||||
}
|
||||
// @ts-ignore
|
||||
if (body.approved_at !== undefined) {
|
||||
// @ts-ignore
|
||||
updateData.approved_at = normalizeDate(body.approved_at)
|
||||
}
|
||||
|
||||
const [updated] = await server.db
|
||||
.update(stafftimeentries)
|
||||
.set(updateData)
|
||||
.where(eq(stafftimeentries.id, id))
|
||||
.returning()
|
||||
|
||||
if (!updated) {
|
||||
return reply.code(404).send({ error: "Time entry not found" })
|
||||
}
|
||||
|
||||
return reply.send(updated)
|
||||
} catch (err: any) {
|
||||
console.error("UPDATE ERROR:", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal server error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// ▶ Einzelne Zeit abrufen (inkl. Connects)
|
||||
server.get<{ Params: { id: string } }>(
|
||||
'/staff/time/:id',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.select(`
|
||||
*,
|
||||
staff_time_entry_connects(*)
|
||||
`)
|
||||
.eq('id', id)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
return reply.send(data)
|
||||
}
|
||||
)
|
||||
|
||||
// ▶ Zeit bearbeiten
|
||||
server.put<{ Params: { id: string }, Body: Partial<StaffTimeEntry> }>(
|
||||
'/staff/time/:id',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.update({ ...req.body, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
return reply.send(data)
|
||||
}
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ▶ Zeit löschen
|
||||
server.delete<{ Params: { id: string } }>(
|
||||
'/staff/time/:id',
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
const { error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
// -------------------------------------------------------------
|
||||
server.delete("/staff/time/:id", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params as any
|
||||
|
||||
if (error) return reply.code(400).send({ error: error.message })
|
||||
return reply.send({ success: true })
|
||||
await server.db
|
||||
.delete(stafftimeentries)
|
||||
.where(eq(stafftimeentries.id, id))
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
return reply.code(400).send({ error: (err as Error).message })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,188 +1,244 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import jwt from "jsonwebtoken";
|
||||
import {secrets} from "../utils/secrets";
|
||||
import { FastifyInstance } from "fastify"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { secrets } from "../utils/secrets"
|
||||
|
||||
export default async function routes(server: FastifyInstance) {
|
||||
import {
|
||||
authTenantUsers,
|
||||
authUsers,
|
||||
authProfiles,
|
||||
tenants
|
||||
} from "../../db/schema"
|
||||
|
||||
import {and, eq, inArray} from "drizzle-orm"
|
||||
|
||||
|
||||
export default async function tenantRoutes(server: FastifyInstance) {
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET CURRENT TENANT
|
||||
// -------------------------------------------------------------
|
||||
server.get("/tenant", async (req) => {
|
||||
if(req.tenant) {
|
||||
if (req.tenant) {
|
||||
return {
|
||||
message: `Hallo vom Tenant ${req.tenant?.name}`,
|
||||
tenant_id: req.tenant?.id,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: `Server ist in MultiTenant Mode. Sie bekommen alles für Sie verfügbare`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
server.post("/tenant/switch", async (req, reply) => {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const body = req.body as { tenant_id: string };
|
||||
console.log(body);
|
||||
|
||||
// prüfen ob user im Tenant Mitglied ist
|
||||
const { data: tenantUser, error } = await server.supabase
|
||||
.from("auth_tenant_users")
|
||||
.select("*")
|
||||
.eq("user_id", req.user.user_id)
|
||||
.eq("tenant_id", body.tenant_id)
|
||||
.single();
|
||||
|
||||
if (error || !tenantUser) {
|
||||
return reply.code(403).send({ error: "Not a member of this tenant" });
|
||||
}
|
||||
|
||||
// neues JWT mit tenant_id ausstellen
|
||||
const token = jwt.sign(
|
||||
{
|
||||
user_id: req.user.user_id,
|
||||
email: req.user.email,
|
||||
tenant_id: body.tenant_id,
|
||||
},
|
||||
secrets.JWT_SECRET!,
|
||||
{ expiresIn: "6h" }
|
||||
);
|
||||
|
||||
reply.setCookie("token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
secure: process.env.NODE_ENV === "production", // lokal: false, prod: true
|
||||
maxAge: 60 * 60 * 3, // 3 Stunden
|
||||
})
|
||||
|
||||
return { token };
|
||||
});
|
||||
|
||||
server.get("/tenant/users", async (req, reply) => {
|
||||
const { tenant_id } = req.params as { tenant_id: string };
|
||||
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_tenant_users")
|
||||
.select(`
|
||||
user_id,
|
||||
auth_users!tenantusers_user_id_fkey ( id, email, created_at, auth_profiles(*))`)
|
||||
.eq("tenant_id", authUser.tenant_id);
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
let correctedData = data.map(i => {
|
||||
|
||||
|
||||
return {
|
||||
id: i.user_id,
|
||||
// @ts-ignore
|
||||
email: i.auth_users.email,
|
||||
// @ts-ignore
|
||||
profile: i.auth_users.auth_profiles.find(x => x.tenant_id === authUser.tenant_id),
|
||||
// @ts-ignore
|
||||
full_name: i.auth_users.auth_profiles.find(x => x.tenant_id === authUser.tenant_id)?.full_name,
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
message: "Server ist im MultiTenant-Modus – es werden alle verfügbaren Tenants geladen."
|
||||
}
|
||||
})
|
||||
|
||||
return { tenant_id, users: correctedData };
|
||||
});
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SWITCH TENANT
|
||||
// -------------------------------------------------------------
|
||||
server.post("/tenant/switch", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const { tenant_id } = req.body as { tenant_id: string }
|
||||
if (!tenant_id) return reply.code(400).send({ error: "tenant_id required" })
|
||||
|
||||
// prüfen ob der User zu diesem Tenant gehört
|
||||
const membership = await server.db
|
||||
.select()
|
||||
.from(authTenantUsers)
|
||||
.where(and(
|
||||
eq(authTenantUsers.user_id, req.user.user_id),
|
||||
eq(authTenantUsers.tenant_id, Number(tenant_id))
|
||||
))
|
||||
|
||||
if (!membership.length) {
|
||||
return reply.code(403).send({ error: "Not a member of this tenant" })
|
||||
}
|
||||
|
||||
// JWT neu erzeugen
|
||||
const token = jwt.sign(
|
||||
{
|
||||
user_id: req.user.user_id,
|
||||
email: req.user.email,
|
||||
tenant_id,
|
||||
},
|
||||
secrets.JWT_SECRET!,
|
||||
{ expiresIn: "6h" }
|
||||
)
|
||||
|
||||
reply.setCookie("token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 3,
|
||||
})
|
||||
|
||||
return { token }
|
||||
|
||||
} catch (err) {
|
||||
console.error("TENANT SWITCH ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TENANT USERS (auth_users + auth_profiles)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/tenant/users", async (req, reply) => {
|
||||
try {
|
||||
const authUser = req.user
|
||||
if (!authUser) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const tenantId = authUser.tenant_id
|
||||
|
||||
// 1) auth_tenant_users → user_ids
|
||||
const tenantUsers = await server.db
|
||||
.select()
|
||||
.from(authTenantUsers)
|
||||
.where(eq(authTenantUsers.tenant_id, tenantId))
|
||||
|
||||
const userIds = tenantUsers.map(u => u.user_id)
|
||||
|
||||
if (!userIds.length) {
|
||||
return { tenant_id: tenantId, users: [] }
|
||||
}
|
||||
|
||||
// 2) auth_users laden
|
||||
const users = await server.db
|
||||
.select()
|
||||
.from(authUsers)
|
||||
.where(inArray(authUsers.id, userIds))
|
||||
|
||||
// 3) auth_profiles pro Tenant laden
|
||||
const profiles = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.tenant_id, tenantId),
|
||||
inArray(authProfiles.user_id, userIds)
|
||||
))
|
||||
|
||||
const combined = users.map(u => {
|
||||
const profile = profiles.find(p => p.user_id === u.id)
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
profile,
|
||||
full_name: profile?.full_name ?? null
|
||||
}
|
||||
})
|
||||
|
||||
return { tenant_id: tenantId, users: combined }
|
||||
|
||||
} catch (err) {
|
||||
console.error("/tenant/users ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TENANT PROFILES
|
||||
// -------------------------------------------------------------
|
||||
server.get("/tenant/profiles", async (req, reply) => {
|
||||
const { tenant_id } = req.params as { tenant_id: string };
|
||||
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select()
|
||||
.eq("tenant_id", authUser.tenant_id);
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
return reply.code(400).send({ error: error.message });
|
||||
const data = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(eq(authProfiles.tenant_id, tenantId))
|
||||
|
||||
return { data }
|
||||
|
||||
} catch (err) {
|
||||
console.error("/tenant/profiles ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return { data };
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE NUMBER RANGE
|
||||
// -------------------------------------------------------------
|
||||
server.put("/tenant/numberrange/:numberrange", async (req, reply) => {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
try {
|
||||
const user = req.user
|
||||
if (!user) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const { numberrange } = req.params as { numberrange: string }
|
||||
const { numberRange } = req.body as { numberRange: any }
|
||||
|
||||
if (!numberRange) {
|
||||
return reply.code(400).send({ error: "numberRange required" })
|
||||
}
|
||||
|
||||
const tenantId = Number(user.tenant_id)
|
||||
|
||||
const currentTenantRows = await server.db
|
||||
.select()
|
||||
.from(tenants)
|
||||
.where(eq(tenants.id, tenantId))
|
||||
|
||||
const current = currentTenantRows[0]
|
||||
if (!current) return reply.code(404).send({ error: "Tenant not found" })
|
||||
|
||||
const updatedRanges = {
|
||||
//@ts-ignore
|
||||
...current.numberRanges,
|
||||
[numberrange]: numberRange
|
||||
}
|
||||
|
||||
const updated = await server.db
|
||||
.update(tenants)
|
||||
.set({ numberRanges: updatedRanges })
|
||||
.where(eq(tenants.id, tenantId))
|
||||
.returning()
|
||||
|
||||
return updated[0]
|
||||
|
||||
} catch (err) {
|
||||
console.error("/tenant/numberrange ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
const { numberrange } = req.params as { numberrange?: string }
|
||||
|
||||
const body = req.body as { numberRange: object };
|
||||
console.log(body);
|
||||
|
||||
if(!body.numberRange) {
|
||||
return reply.code(400).send({ error: "numberRange required" });
|
||||
}
|
||||
|
||||
const {data:currentTenantData,error:numberRangesError} = await server.supabase.from("tenants").select().eq("id", req.user.tenant_id).single()
|
||||
|
||||
console.log(currentTenantData)
|
||||
console.log(numberRangesError)
|
||||
})
|
||||
|
||||
|
||||
let numberRanges = {
|
||||
// @ts-ignore
|
||||
...currentTenantData.numberRanges
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
numberRanges[numberrange] = body.numberRange
|
||||
|
||||
|
||||
console.log(numberRanges)
|
||||
|
||||
const {data,error} = await server.supabase
|
||||
.from("tenants")
|
||||
.update({numberRanges: numberRanges})
|
||||
.eq('id',req.user.tenant_id)
|
||||
.select()
|
||||
|
||||
if(data && !error) {
|
||||
return reply.send(data)
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE TENANT OTHER FIELDS
|
||||
// -------------------------------------------------------------
|
||||
server.put("/tenant/other/:id", async (req, reply) => {
|
||||
if (!req.user) {
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
try {
|
||||
const user = req.user
|
||||
if (!user) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const { data } = req.body as { data: any }
|
||||
|
||||
if (!data) return reply.code(400).send({ error: "data required" })
|
||||
|
||||
const updated = await server.db
|
||||
.update(tenants)
|
||||
.set(data)
|
||||
.where(eq(tenants.id, Number(user.tenant_id)))
|
||||
.returning()
|
||||
|
||||
return updated[0]
|
||||
|
||||
} catch (err) {
|
||||
console.error("/tenant/other ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
const { id } = req.params as { id?: string }
|
||||
})
|
||||
|
||||
const body = req.body as { data: object };
|
||||
console.log(body);
|
||||
|
||||
if(!body.data) {
|
||||
return reply.code(400).send({ error: "data required" });
|
||||
}
|
||||
|
||||
const {data:dataReturn,error} = await server.supabase
|
||||
.from("tenants")
|
||||
.update(body.data)
|
||||
.eq('id',req.user.tenant_id)
|
||||
.select()
|
||||
|
||||
if(dataReturn && !error) {
|
||||
return reply.send(dataReturn)
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,67 @@
|
||||
// 🔧 Hilfsfunktionen
|
||||
import {FastifyInstance} from "fastify";
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq, ilike, and } from "drizzle-orm"
|
||||
|
||||
import { contacts, customers } from "../../db/schema"
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Extract Domain
|
||||
// -------------------------------------------------------------
|
||||
export function extractDomain(email: string) {
|
||||
if (!email) return null
|
||||
const parts = email.split('@')
|
||||
const parts = email.split("@")
|
||||
return parts.length === 2 ? parts[1].toLowerCase() : null
|
||||
}
|
||||
|
||||
export async function findCustomerOrContactByEmailOrDomain(server:FastifyInstance, fromMail: string, tenantId: number) {
|
||||
// -------------------------------------------------------------
|
||||
// Kunde oder Kontakt anhand E-Mail oder Domain finden
|
||||
// -------------------------------------------------------------
|
||||
export async function findCustomerOrContactByEmailOrDomain(
|
||||
server: FastifyInstance,
|
||||
fromMail: string,
|
||||
tenantId: number
|
||||
) {
|
||||
const sender = fromMail.toLowerCase()
|
||||
const senderDomain = extractDomain(sender)
|
||||
if (!senderDomain) return null
|
||||
|
||||
// 1️⃣ Direkter Match über contacts
|
||||
const { data: contactMatch } = await server.supabase
|
||||
.from('contacts')
|
||||
.select('id, customer')
|
||||
.eq('email', sender)
|
||||
.eq('tenant', tenantId)
|
||||
.maybeSingle()
|
||||
// 1️⃣ Direkter Match über Contacts (email)
|
||||
const contactMatch = await server.db
|
||||
.select({
|
||||
id: contacts.id,
|
||||
customer: contacts.customer,
|
||||
})
|
||||
.from(contacts)
|
||||
.where(
|
||||
and(
|
||||
eq(contacts.email, sender),
|
||||
eq(contacts.tenant, tenantId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (contactMatch?.customer) {
|
||||
return { customer: contactMatch.customer, contact: contactMatch.id }
|
||||
if (contactMatch.length && contactMatch[0].customer) {
|
||||
return {
|
||||
customer: contactMatch[0].customer,
|
||||
contact: contactMatch[0].id,
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Kunden nach Domain oder Rechnungs-E-Mail durchsuchen
|
||||
const { data: customers, error } = await server.supabase
|
||||
.from('customers')
|
||||
.select('id, infoData')
|
||||
.eq('tenant', tenantId)
|
||||
// 2️⃣ Kunden anhand Domain vergleichen
|
||||
const allCustomers = await server.db
|
||||
.select({
|
||||
id: customers.id,
|
||||
infoData: customers.infoData,
|
||||
})
|
||||
.from(customers)
|
||||
.where(eq(customers.tenant, tenantId))
|
||||
|
||||
if (error) {
|
||||
server.log.error(`[Helpdesk] Fehler beim Laden der Kunden: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
|
||||
for (const c of customers || []) {
|
||||
for (const c of allCustomers) {
|
||||
const info = c.infoData || {}
|
||||
|
||||
// @ts-ignore
|
||||
const email = info.email?.toLowerCase()
|
||||
//@ts-ignore
|
||||
const invoiceEmail = info.invoiceEmail?.toLowerCase()
|
||||
const emailDomain = extractDomain(email)
|
||||
const invoiceDomain = extractDomain(invoiceEmail)
|
||||
@@ -55,18 +79,28 @@ export async function findCustomerOrContactByEmailOrDomain(server:FastifyInstanc
|
||||
return null
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// getNestedValue (für Sortierung & Suche im Backend)
|
||||
// -------------------------------------------------------------
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
return path.split('.').reduce((acc, part) => acc?.[part], obj);
|
||||
return path
|
||||
.split(".")
|
||||
.reduce((acc, part) => (acc && acc[part] !== undefined ? acc[part] : undefined), obj)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// compareValues (Sortierung für paginated)
|
||||
// -------------------------------------------------------------
|
||||
export function compareValues(a: any, b: any): number {
|
||||
if (a === b) return 0;
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
if (a === b) return 0
|
||||
if (a == null) return 1
|
||||
if (b == null) return -1
|
||||
|
||||
if (typeof a === 'string' && typeof b === 'string') {
|
||||
return a.localeCompare(b);
|
||||
// String Compare
|
||||
if (typeof a === "string" && typeof b === "string") {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
// Numerisch
|
||||
return a < b ? -1 : 1
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import {
|
||||
accounts, bankaccounts, bankrequisitions, bankstatements,
|
||||
contacts,
|
||||
contracts, costcentres, createddocuments,
|
||||
customers,
|
||||
files, filetags, folders, hourrates, inventoryitemgroups,
|
||||
files, filetags, folders, hourrates, incominginvoices, inventoryitemgroups,
|
||||
inventoryitems, letterheads, ownaccounts,
|
||||
plants, productcategories, products,
|
||||
projects,
|
||||
projecttypes, servicecategories, services, spaces, tasks, texttemplates, units, vehicles,
|
||||
projecttypes, servicecategories, services, spaces, statementallocations, tasks, texttemplates, units, vehicles,
|
||||
vendors
|
||||
} from "../db/schema";
|
||||
} from "../../db/schema";
|
||||
|
||||
export const resourceConfig = {
|
||||
projects: {
|
||||
searchColumns: ["name"],
|
||||
mtoLoad: ["customer","plant","contract","projecttype"],
|
||||
mtmLoad: ["tasks", "files"],
|
||||
table: projects
|
||||
table: projects,
|
||||
numberRangeHolder: "projectNumber"
|
||||
},
|
||||
customers: {
|
||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
mtmLoad: ["contacts","projects"],
|
||||
table: customers,
|
||||
numberRangeHolder: "customerNumber",
|
||||
},
|
||||
contacts: {
|
||||
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
||||
@@ -29,7 +32,8 @@ export const resourceConfig = {
|
||||
},
|
||||
contracts: {
|
||||
table: contracts,
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"]
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||
numberRangeHolder: "contractNumber",
|
||||
},
|
||||
plants: {
|
||||
table: plants,
|
||||
@@ -42,6 +46,7 @@ export const resourceConfig = {
|
||||
vendors: {
|
||||
table: vendors,
|
||||
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
|
||||
numberRangeHolder: "vendorNumber",
|
||||
},
|
||||
files: {
|
||||
table: files
|
||||
@@ -53,7 +58,8 @@ export const resourceConfig = {
|
||||
table: filetags
|
||||
},
|
||||
inventoryitems: {
|
||||
table: inventoryitems
|
||||
table: inventoryitems,
|
||||
numberRangeHolder: "articleNumber",
|
||||
},
|
||||
inventoryitemgroups: {
|
||||
table: inventoryitemgroups
|
||||
@@ -87,6 +93,7 @@ export const resourceConfig = {
|
||||
spaces: {
|
||||
table: spaces,
|
||||
searchColumns: ["name","space_number","type","info_data"],
|
||||
numberRangeHolder: "spaceNumber",
|
||||
},
|
||||
ownaccounts: {
|
||||
table: ownaccounts,
|
||||
@@ -95,7 +102,8 @@ export const resourceConfig = {
|
||||
costcentres: {
|
||||
table: costcentres,
|
||||
searchColumns: ["name","number","description"],
|
||||
mtoLoad: ["vehicle","project","inventoryitem"]
|
||||
mtoLoad: ["vehicle","project","inventoryitem"],
|
||||
numberRangeHolder: "number",
|
||||
},
|
||||
tasks: {
|
||||
table: tasks,
|
||||
@@ -106,9 +114,34 @@ export const resourceConfig = {
|
||||
},
|
||||
createddocuments: {
|
||||
table: createddocuments,
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead",]
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead"],
|
||||
mtmLoad: ["statementallocations"],
|
||||
mtmListLoad: ["statementallocations"],
|
||||
},
|
||||
texttemplates: {
|
||||
table: texttemplates
|
||||
},
|
||||
incominginvoices: {
|
||||
table: incominginvoices,
|
||||
mtmLoad: ["statementallocations"],
|
||||
mtmListLoad: ["statementallocations"],
|
||||
},
|
||||
statementallocations: {
|
||||
table: statementallocations,
|
||||
mtoLoad: ["customer","vendor","incominginvoice","createddocument","ownaccount","bankstatement"]
|
||||
},
|
||||
accounts: {
|
||||
table: accounts,
|
||||
},
|
||||
bankstatements: {
|
||||
table: bankstatements,
|
||||
mtmListLoad: ["statementallocations"],
|
||||
mtmLoad: ["statementallocations"],
|
||||
},
|
||||
bankaccounts: {
|
||||
table: bankaccounts,
|
||||
},
|
||||
bankrequisitions: {
|
||||
table: bankrequisitions,
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmitOnError": false,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src","db","*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user