diff --git a/src/index.ts b/src/index.ts index 0fde16c..a057ed7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; @@ -113,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); diff --git a/src/routes/auth/user.ts b/src/routes/auth/user.ts new file mode 100644 index 0000000..fde299a --- /dev/null +++ b/src/routes/auth/user.ts @@ -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 } + + 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" }) + } + }) +} diff --git a/src/routes/resources/main.ts b/src/routes/resources/main.ts index bad18a7..966cea0 100644 --- a/src/routes/resources/main.ts +++ b/src/routes/resources/main.ts @@ -13,6 +13,8 @@ import { import {resourceConfig} from "../../utils/resource.config"; +import {useNextNumberRangeNumber} from "../../utils/functions"; +import {stafftimeentries} from "../../../db/schema"; // ------------------------------------------------------------- // SQL Volltextsuche auf mehreren Feldern @@ -422,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; + + 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 + + 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 + }) + } diff --git a/src/routes/tenant.ts b/src/routes/tenant.ts index 8b83203..93fa7c6 100644 --- a/src/routes/tenant.ts +++ b/src/routes/tenant.ts @@ -9,7 +9,7 @@ import { tenants } from "../../db/schema" -import { eq } from "drizzle-orm" +import {and, eq, inArray} from "drizzle-orm" export default async function tenantRoutes(server: FastifyInstance) { @@ -48,12 +48,10 @@ export default async function tenantRoutes(server: FastifyInstance) { const membership = await server.db .select() .from(authTenantUsers) - .where( - eq(authTenantUsers.user_id, req.user.user_id) - ) - .where( - eq(authTenantUsers.tenant_id, Number(tenant_id)) - ) + .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" }) @@ -120,8 +118,11 @@ export default async function tenantRoutes(server: FastifyInstance) { const profiles = await server.db .select() .from(authProfiles) - .where(eq(authProfiles.tenant_id, tenantId)) - .where(inArray(authProfiles.user_id, userIds)) + .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) @@ -192,6 +193,7 @@ export default async function tenantRoutes(server: FastifyInstance) { if (!current) return reply.code(404).send({ error: "Tenant not found" }) const updatedRanges = { + //@ts-ignore ...current.numberRanges, [numberrange]: numberRange } diff --git a/src/utils/resource.config.ts b/src/utils/resource.config.ts index 4bb852a..4aa78f1 100644 --- a/src/utils/resource.config.ts +++ b/src/utils/resource.config.ts @@ -16,12 +16,14 @@ export const resourceConfig = { 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"], @@ -30,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, @@ -43,6 +46,7 @@ export const resourceConfig = { vendors: { table: vendors, searchColumns: ["name","vendorNumber","notes","defaultPaymentType"], + numberRangeHolder: "vendorNumber", }, files: { table: files @@ -54,7 +58,8 @@ export const resourceConfig = { table: filetags }, inventoryitems: { - table: inventoryitems + table: inventoryitems, + numberRangeHolder: "articleNumber", }, inventoryitemgroups: { table: inventoryitemgroups @@ -88,6 +93,7 @@ export const resourceConfig = { spaces: { table: spaces, searchColumns: ["name","space_number","type","info_data"], + numberRangeHolder: "spaceNumber", }, ownaccounts: { table: ownaccounts, @@ -96,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,