// Mapping zwischen Postgres-Rows und dem Frontend-`data`-Shape. // // Konventionen: // - DB ist snake_case, Frontend camelCase. Übersetzung explizit pro Entity. // - Postgres `numeric` kommt als String via JSON-API zurück → `num()` wandelt. // - JSONB-Spalten (settings.formats, settings.ui, settings.page_margins, // protocols.participants, projects.positions, …) werden direkt in das // Frontend-Objekt gespreaded. // - Zirkuläre / verschachtelte Sub-Entities (invoice.reminders, project.linkedQuotes, // delivery_note.items) bekommen eine Liste der Parent-IDs übergeben und // filtern selbst zur richtigen Zeile. const num = (v) => v == null ? null : Number(v); // Reverse-Mapping: Settings flach → JSONB-Sub-Objekte zerlegen. // Diese Felder gehören in settings.formats: const FORMAT_KEYS = ["projectNumberFormat", "invoiceNumberFormat", "protokollNumberFormat", "pdfNameFormat"]; // in settings.page_margins: const PAGE_MARGIN_KEYS = ["pageMarginTop", "pageMarginBottom", "pageMarginLeft", "pageMarginRight"]; // in settings.ui: const UI_KEYS = ["autoPrint", "logoSize", "qrNewPage", "expenseCategories", "internalExpenseCategories"]; function pickKeys(obj, keys) { const out = {}; for (const k of keys) if (obj[k] !== undefined) out[k] = obj[k]; return out; } export const fromDB = { studio: (r) => ({ id: r.id, name: r.name, slug: r.slug }), // Settings sind eine 1:1-Tabelle + studio_roles als Sub-Liste. // JSONB-Felder werden flach in `settings` gespreaded, damit das Frontend // weiter mit `settings.projectNumberFormat`, `settings.pageMarginTop` etc. // arbeiten kann. studioSettings: (row, roles = []) => ({ setupCompleted: row.setup_completed, name: row.name, address: row.address, street: row.street, zip: row.zip, city: row.city, country: row.country, email: row.email, phone: row.phone, iban: row.iban, ibanType: row.iban_type, mwst: row.mwst_nr, mwstRate: num(row.mwst_rate), defaultHourlyRate: num(row.default_hourly_rate), defaultWochenstunden: num(row.default_wochenstunden), defaultFerienWochen: num(row.default_ferien_wochen), closedMonths: row.closed_months || [], blockMaiTag: row.block_mai_tag, protokollTypeAbbreviations: row.protokoll_type_abbr || {}, logoUrl: row.logo_url, // Pfad in Supabase Storage (statt Base64) ...(row.formats || {}), // projectNumberFormat, invoiceNumberFormat, protokollNumberFormat, pdfNameFormat ...(row.page_margins || {}), // pageMarginTop/Bottom/Left/Right ...(row.ui || {}), // autoPrint, logoSize, qrNewPage, expenseCategories, internalExpenseCategories roles: roles.map(fromDB.studioRole), }), studioRole: (r) => ({ id: r.id, label: r.label, rate: num(r.rate) }), person: (r) => ({ id: r.id, isShared: r.studio_id === null, // global = studio_id NULL + Sichtbarkeit via person_studio_links name: r.name, type: r.person_type, isAuftraggeber: r.is_auftraggeber, isPartner: r.is_partner, street: r.street, zip: r.zip, city: r.city, country: r.country, email: r.email, phone: r.phone, website: r.website, note: r.note, contacts: r.contacts || [], honorarOffers: r.honorar_offers || [], }), project: (r, allQuoteLinks = []) => ({ id: r.id, number: r.number, name: r.name, clientId: r.client_id, category: r.category, billingType: r.billing_type, hourlyRate: num(r.hourly_rate), budget: num(r.budget), budgetHours: num(r.budget_hours), status: r.status, description: r.description, startDate: r.start_date, enabledPhases: r.enabled_phases || [], positions: r.positions || [], customPhases: r.custom_phases || [], projectContacts: r.project_contacts || [], internalMembers: r.internal_members || [], linkedQuotes: allQuoteLinks .filter(l => l.project_id === r.id) .map(l => ({ quoteId: l.quote_id, role: l.role })), }), quote: (r) => ({ id: r.id, number: r.number, clientId: r.client_id, projectId: r.project_id, projectName: r.project_name, date: r.date, validUntil: r.valid_until, mode: r.mode, mwst: r.mwst, notes: r.notes, status: r.status, sia: r.sia_config, manualPhases: r.manual_phases, freeItems: r.free_items, quoteRoles: r.quote_roles, }), invoice: (r, allReminders = []) => ({ id: r.id, number: r.number, clientId: r.client_id, contactId: r.contact_id, projectId: r.project_id, quoteId: r.quote_id, date: r.date, dueDate: r.due_date, sentDate: r.sent_date, paidDate: r.paid_date, items: r.items || [], mwst: r.mwst, mwstRate: num(r.mwst_rate), notes: r.notes, status: r.status, invoiceKind: r.invoice_kind, discountType: r.discount_type, discountValue: num(r.discount_value), discountLabel: r.discount_label, entrySelections: r.entry_selections || {}, qrReference: r.qr_reference, reminders: allReminders .filter(rem => rem.invoice_id === r.id) .sort((a, b) => (a.nr || 0) - (b.nr || 0)) .map(rem => ({ nr: rem.nr, date: rem.date, sentDate: rem.sent_date, daysPast: rem.days_past, note: rem.note, })), }), expense: (r) => ({ id: r.id, employeeId: r.employee_id, projectId: r.project_id, date: r.date, category: r.category, description: r.description, amount: num(r.amount), mwstRate: num(r.mwst_rate), inclMwst: r.incl_mwst, status: r.status, receiptUrl: r.receipt_url, receiptName: r.receipt_name, lohnEntryId: r.lohn_entry_id, }), internalExpense: (r) => ({ id: r.id, date: r.date, category: r.category, description: r.description, amount: num(r.amount), mwstRate: num(r.mwst_rate), inclMwst: r.incl_mwst, recurring: r.recurring, recurringInterval: r.recurring_interval, receiptUrl: r.receipt_url, }), timeEntry: (r) => ({ id: r.id, employeeId: r.employee_id, projectId: r.project_id, phaseId: r.phase_id, positionId: r.position_id, date: r.date, minutes: r.minutes, startTime: r.start_time, endTime: r.end_time, description: r.description, createdAt: r.created_at, }), employee: (r) => ({ id: r.id, name: r.name, personalNr: r.personal_nr, pensum: r.pensum, wochenstunden: num(r.wochenstunden), ferienWochen: num(r.ferien_wochen), pkAGSatz: num(r.pk_ag_satz), ferienUebertragVorjahr: r.ferien_uebertrag_vorjahr || {}, _appUserId: r.app_user_id, active: r.active, }), absence: (r) => ({ id: r.id, employeeId: r.employee_id, type: r.type_id, date: r.date, dateFrom: r.date_from, dateTo: r.date_to, startTime: r.start_time, endTime: r.end_time, hours: r.hours, minutes: r.minutes, note: r.note, status: r.status, createdAt: r.created_at, }), vacationEntry: (r) => ({ id: r.id, employeeId: r.employee_id, dateFrom: r.date_from, dateTo: r.date_to, note: r.note, status: r.status, originalData: r.original_data, createdAt: r.created_at, }), payrollEntry: (r) => ({ id: r.id, employeeId: r.employee_id, year: r.year, month: r.month, brutto: num(r.brutto), ahv: num(r.ahv), alv: num(r.alv), bvg: num(r.bvg), nbu: num(r.nbu), ktg: num(r.ktg), quellensteuer: num(r.quellensteuer), spesen: num(r.spesen), bonus: num(r.bonus), netto: num(r.netto), status: r.status, paidAt: r.paid_at, }), overtimeClosing: (r) => ({ id: r.id, employeeId: r.employee_id, date: r.date, saldoHours: num(r.saldo_hours), }), holiday: (r) => ({ date: r.date, label: r.label, halfDay: r.half_day, }), absenceType: (r) => ({ id: r.id, label: r.label, color: r.color }), letterTemplate: (r) => ({ id: r.id, name: r.name, body: r.body }), appRole: (r) => ({ id: r.id, name: r.name, permissions: r.permissions, dashboardTemplateId: r.dashboard_template_id, }), dashboardTemplate: (r) => ({ id: r.id, name: r.name, isPublic: r.is_public, layout: r.layout, }), protocol: (r) => ({ id: r.id, number: r.number, type: r.type, location: r.location, projectId: r.project_id, projectManual: r.project_manual, participants: r.participants || [], traktanden: r.traktanden || [], nextDate: r.next_date, verteiler: r.verteiler, createdAt: r.created_at, }), deliveryNote: (r, allItems = []) => ({ id: r.id, number: r.number, date: r.date, clientId: r.client_id, projectId: r.project_id, notes: r.notes, items: allItems .filter(it => it.delivery_note_id === r.id) .sort((a, b) => (a.sort || 0) - (b.sort || 0)) .map(it => ({ id: it.id, desc: it.description, qty: num(it.qty), unit: it.unit, note: it.note, })), }), blogPost: (r) => ({ id: r.id, authorId: r.author_id, category: r.category, title: r.title, body: r.body, pinned: r.pinned, createdAt: r.created_at, }), }; // ─────────────────────────────────────────────────────────────────────────── // Frontend → DB Mapping (für save()) // ─────────────────────────────────────────────────────────────────────────── export const toDB = { studioSettings: (settings, studioId) => ({ studio_id: studioId, setup_completed: settings.setupCompleted ?? false, name: settings.name, address: settings.address, street: settings.street, zip: settings.zip, city: settings.city, country: settings.country, email: settings.email, phone: settings.phone, iban: settings.iban, iban_type: settings.ibanType, mwst_nr: settings.mwst, mwst_rate: settings.mwstRate, default_hourly_rate: settings.defaultHourlyRate, default_wochenstunden: settings.defaultWochenstunden, default_ferien_wochen: settings.defaultFerienWochen, closed_months: settings.closedMonths || [], block_mai_tag: settings.blockMaiTag, protokoll_type_abbr: settings.protokollTypeAbbreviations || {}, logo_url: settings.logoUrl, formats: pickKeys(settings, FORMAT_KEYS), page_margins: pickKeys(settings, PAGE_MARGIN_KEYS), ui: pickKeys(settings, UI_KEYS), }), studioRoles: (roles = [], studioId) => (roles || []).map((r, i) => ({ studio_id: studioId, id: r.id, label: r.label, rate: r.rate, sort: i, })), person: (p, studioId) => ({ id: p.id, // Geteilte Person bleibt global (studio_id NULL); lokale Person hängt am Studio. studio_id: p.isShared ? null : studioId, name: p.name, person_type: p.type, is_auftraggeber: !!p.isAuftraggeber, is_partner: !!p.isPartner, street: p.street, zip: p.zip, city: p.city, country: p.country, email: p.email || null, phone: p.phone, website: p.website, note: p.note, contacts: p.contacts || [], honorar_offers: p.honorar_offers || [], }), project: (p, studioId) => ({ id: p.id, studio_id: studioId, number: p.number, name: p.name, client_id: p.clientId || null, category: p.category, billing_type: p.billingType, hourly_rate: p.hourlyRate, budget: p.budget, budget_hours: p.budgetHours, status: p.status || "aktiv", description: p.description, start_date: p.startDate || null, enabled_phases: p.enabledPhases || [], positions: p.positions || [], custom_phases: p.customPhases || [], project_contacts: p.projectContacts || [], internal_members: p.internalMembers || [], }), projectQuoteLinks: (projects = []) => (projects || []).flatMap(p => (p.linkedQuotes || []).map(lq => ({ project_id: p.id, quote_id: lq.quoteId, role: lq.role || null, })) ), quote: (q, studioId) => ({ id: q.id, studio_id: studioId, number: q.number, client_id: q.clientId || null, project_id: q.projectId || null, project_name: q.projectName, date: q.date || null, valid_until: q.validUntil || null, mode: q.mode, mwst: q.mwst, notes: q.notes, status: q.status || "entwurf", sia_config: q.sia || null, manual_phases: q.manualPhases || null, free_items: q.freeItems || null, quote_roles: q.quoteRoles || null, }), invoice: (inv, studioId) => ({ id: inv.id, studio_id: studioId, number: inv.number, client_id: inv.clientId || null, contact_id: inv.contactId || null, project_id: inv.projectId || null, quote_id: inv.quoteId || null, date: inv.date || null, due_date: inv.dueDate || null, sent_date: inv.sentDate || null, paid_date: inv.paidDate || null, items: inv.items || [], mwst: inv.mwst, mwst_rate: inv.mwstRate, notes: inv.notes, status: inv.status || "entwurf", invoice_kind: inv.invoiceKind, discount_type: inv.discountType || "none", discount_value: inv.discountValue || 0, discount_label: inv.discountLabel, entry_selections: inv.entrySelections || {}, qr_reference: inv.qrReference, }), invoiceReminders: (invoices = []) => (invoices || []).flatMap(inv => (inv.reminders || []).map(rem => ({ invoice_id: inv.id, nr: rem.nr, date: rem.date, sent_date: rem.sentDate || null, days_past: rem.daysPast, note: rem.note, })) ), expense: (e, studioId) => ({ id: e.id, studio_id: studioId, employee_id: e.employeeId || null, project_id: e.projectId || null, date: e.date, category: e.category, description: e.description, amount: e.amount, mwst_rate: e.mwstRate, incl_mwst: e.inclMwst, status: e.status || "offen", receipt_url: e.receiptUrl || null, receipt_name: e.receiptName, lohn_entry_id: e.lohnEntryId || null, }), internalExpense: (e, studioId) => ({ id: e.id, studio_id: studioId, date: e.date, category: e.category, description: e.description, amount: e.amount, mwst_rate: e.mwstRate, incl_mwst: e.inclMwst, recurring: !!e.recurring, recurring_interval: e.recurringInterval || null, receipt_url: e.receiptUrl || null, }), timeEntry: (t, studioId) => ({ id: t.id, studio_id: studioId, employee_id: t.employeeId || null, project_id: t.projectId || null, phase_id: t.phaseId || null, position_id: t.positionId || null, date: t.date, minutes: t.minutes, start_time: t.startTime || null, end_time: t.endTime || null, description: t.description, }), employee: (e, studioId) => ({ id: e.id, studio_id: studioId, name: e.name, personal_nr: e.personalNr, pensum: e.pensum, wochenstunden: e.wochenstunden, ferien_wochen: e.ferienWochen, pk_ag_satz: e.pkAGSatz, ferien_uebertrag_vorjahr: e.ferienUebertragVorjahr || {}, app_user_id: e._appUserId || null, active: e.active ?? true, }), absence: (a, studioId) => ({ id: a.id, studio_id: studioId, employee_id: a.employeeId, type_id: a.type || null, date: a.date || null, date_from: a.dateFrom || null, date_to: a.dateTo || null, start_time: a.startTime || null, end_time: a.endTime || null, hours: a.hours, minutes: a.minutes, note: a.note, status: a.status || "pending", }), vacationEntry: (v, studioId) => ({ id: v.id, studio_id: studioId, employee_id: v.employeeId, date_from: v.dateFrom, date_to: v.dateTo, note: v.note, status: v.status || "pending", original_data: v.originalData || null, }), payrollEntry: (p, studioId) => ({ id: p.id, studio_id: studioId, employee_id: p.employeeId, year: p.year, month: p.month, brutto: p.brutto, ahv: p.ahv, alv: p.alv, bvg: p.bvg, nbu: p.nbu, ktg: p.ktg, quellensteuer: p.quellensteuer, spesen: p.spesen, bonus: p.bonus, netto: p.netto, status: p.status || "entwurf", paid_at: p.paidAt || null, }), overtimeClosing: (o, studioId) => ({ id: o.id, studio_id: studioId, employee_id: o.employeeId, date: o.date, saldo_hours: o.saldoHours, }), holiday: (h, studioId) => ({ studio_id: studioId, date: h.date, label: h.label, half_day: !!h.halfDay, }), absenceType: (t, studioId) => ({ studio_id: studioId, id: t.id, label: t.label, color: t.color, }), letterTemplate: (t, studioId) => ({ studio_id: studioId, id: t.id, name: t.name, body: t.body, }), appRole: (r, studioId) => ({ studio_id: studioId, id: r.id, name: r.name, permissions: r.permissions, dashboard_template_id: r.dashboardTemplateId || null, }), dashboardTemplate: (d, studioId) => ({ studio_id: studioId, id: d.id, name: d.name, is_public: d.isPublic ?? true, layout: d.layout || [], }), protocol: (p, studioId) => ({ id: p.id, studio_id: studioId, number: p.number, type: p.type, location: p.location, project_id: p.projectId || null, project_manual: p.projectManual, participants: p.participants || [], traktanden: p.traktanden || [], next_date: p.nextDate || null, verteiler: p.verteiler, }), deliveryNote: (d, studioId) => ({ id: d.id, studio_id: studioId, number: d.number, date: d.date || null, client_id: d.clientId || null, project_id: d.projectId || null, notes: d.notes, }), deliveryNoteItems: (deliveryNotes = []) => (deliveryNotes || []).flatMap(dn => (dn.items || []).map((it, i) => ({ id: it.id, delivery_note_id: dn.id, sort: i, description: it.desc, qty: it.qty, unit: it.unit, note: it.note, })) ), blogPost: (b, studioId) => ({ id: b.id, studio_id: studioId, author_id: b.authorId || null, category: b.category, title: b.title, body: b.body, pinned: !!b.pinned, }), };