From 428a002e9fe36fa285f2c21a1056bf3296aff04a Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 8 Dec 2025 12:15:20 +0100 Subject: [PATCH] Redone --- src/index.ts | 1 + src/modules/time/evaluation.service.ts | 259 +++++++++++++++--------- src/plugins/auth.ts | 164 +++++++++------- src/routes/auth/dep/user.ts | 108 ---------- src/routes/staff/time.ts | 262 ++++++++++++++----------- src/utils/helpers.ts | 96 ++++++--- src/utils/resource.config.ts | 32 ++- 7 files changed, 495 insertions(+), 427 deletions(-) delete mode 100644 src/routes/auth/dep/user.ts diff --git a/src/index.ts b/src/index.ts index fa4a204..0fde16c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,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() }) diff --git a/src/modules/time/evaluation.service.ts b/src/modules/time/evaluation.service.ts index 39b15c9..8032d32 100644 --- a/src/modules/time/evaluation.service.ts +++ b/src/modules/time/evaluation.service.ts @@ -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 - } -} \ No newline at end of file + times, + }; +} diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts index bf9648c..f4b1427 100644 --- a/src/plugins/auth.ts +++ b/src/plugins/auth.ts @@ -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 } } diff --git a/src/routes/auth/dep/user.ts b/src/routes/auth/dep/user.ts deleted file mode 100644 index 7239922..0000000 --- a/src/routes/auth/dep/user.ts +++ /dev/null @@ -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, - } - }) -} \ No newline at end of file diff --git a/src/routes/staff/time.ts b/src/routes/staff/time.ts index 3daa30c..4d00f76 100644 --- a/src/routes/staff/time.ts +++ b/src/routes/staff/time.ts @@ -1,143 +1,173 @@ -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 { user_id, ...rest } = req.body as any const userId = req.user.user_id const tenantId = req.user.tenant_id - - let dataToInsert = { + const newEntry = { tenant_id: tenantId, - user_id: user_id ? user_id : userId, - // @ts-ignore - ...req.body + user_id: user_id || userId, + ...rest } - const { data, error } = await server.supabase - .from('staff_time_entries') - .insert([dataToInsert]) - .select() - .maybeSingle() + const [created] = await server.db + .insert(stafftimeentries) + .values(newEntry) + .returning() - if (error) return reply.code(400).send({ error: error.message }) - return reply.send(data) + 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) => { - 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() - - if (error) return reply.code(400).send({ error: error.message }) - return reply.send(data) - } - ) - - // ▶ Liste aller Zeiten - server.get<{ - Querystring: { - from?: string - to?: string - type?: string - user_id?: string - } - }>('/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 }) - - // 🔒 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) - } - - // 📅 Zeitfilter - if (from) query = query.gte('started_at', from) - if (to) query = query.lte('started_at', to) - if (type) query = query.eq('type', type) - - const { data, error } = await query - if (error) return reply.code(400).send({ error: error.message }) - - return reply.send(data) }) + // ------------------------------------------------------------- + // ▶ Zeit stoppen + // ------------------------------------------------------------- + server.put("/staff/time/:id/stop", async (req, reply) => { + try { + const { id } = req.params as any + const { stopped_at } = req.body as any - // ▶ Einzelne Zeit abrufen (inkl. Connects) - server.get<{ Params: { id: string } }>( - '/staff/time/:id', - async (req, reply) => { - const { id } = req.params + const [updated] = await server.db + .update(stafftimeentries) + .set({ + stopped_at, + updated_at: new Date() + }) + .where(eq(stafftimeentries.id, id)) + .returning() - 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) + return updated + } catch (err) { + return reply.code(400).send({ error: (err as Error).message }) } - ) + }) - // ▶ Zeit bearbeiten - server.put<{ Params: { id: string }, Body: Partial }>( - '/staff/time/:id', - async (req, reply) => { - const { id } = req.params + // ------------------------------------------------------------- + // ▶ Liste aller Zeiten + // ------------------------------------------------------------- + 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 - const { data, error } = await server.supabase - .from('staff_time_entries') - .update({ ...req.body, updated_at: new Date().toISOString() }) - .eq('id', id) + 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() - .maybeSingle() + .from(stafftimeentries) + .where(where) + .orderBy(desc(stafftimeentries.started_at)) - if (error) return reply.code(400).send({ error: error.message }) - return reply.send(data) + return rows + } catch (err) { + console.error(err) + return reply.code(400).send({ error: (err as Error).message }) } - ) + }) + // ------------------------------------------------------------- + // ▶ Einzelne Zeit (inkl. Connects) + // ------------------------------------------------------------- + server.get("/staff/time/:id", async (req, reply) => { + try { + const { id } = req.params as any + + 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 }) + } + }) + + // ------------------------------------------------------------- + // ▶ Zeit bearbeiten + // ------------------------------------------------------------- + server.put("/staff/time/:id", async (req, reply) => { + try { + const { id } = req.params as any + + + const updateData = { + // @ts-ignore + ...req.body, + updated_at: new Date() + } + + const [updated] = await server.db + .update(stafftimeentries) + .set(updateData) + .where(eq(stafftimeentries.id, id)) + .returning() + + return updated + } catch (err) { + return reply.code(400).send({ error: (err as Error).message }) + } + }) + + // ------------------------------------------------------------- // ▶ 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 }) } - ) + }) } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index f484ae1..1d11c5b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -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; -} \ No newline at end of file + // Numerisch + return a < b ? -1 : 1 +} diff --git a/src/utils/resource.config.ts b/src/utils/resource.config.ts index 9c62711..4bb852a 100644 --- a/src/utils/resource.config.ts +++ b/src/utils/resource.config.ts @@ -1,12 +1,13 @@ 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"; @@ -106,9 +107,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, } } \ No newline at end of file