27b1057cd4
Rapport ist jetzt dual: lokal (wie bisher) ODER Cloud auf eigenem Supabase-Server. Beide Modi haben dieselben Funktionen, Cloud zusätzlich Multi-User + Live-Sync. Storage-Architektur - src/storage/adapter.js: einheitliche Promise-API, LocalStorage- und SupabaseAdapter - src/storage/migrations.js: applyMigrations als reine Funktion, für beide Backends - Konfig-driven: VITE_SUPABASE_URL im Production-Build → automatisch Cloud-Modus Postgres-Schema (supabase/migrations/0001–0010) - 29 Tabellen, multi-tenant via studio_id + Row-Level-Security - Audit-Spalten (created_by/updated_by/at) + Trigger - Seed-Trigger pro neuem Studio (Rollen, Templates, Absenz-Typen) - Realtime-Publication für Live-Sync - RPCs: ensure_profile, create_studio_with_admin (mit Personen-Sharing), list_studios, load_persons_for_studio, attach_user_to_studio Cloud-Features (App) - BackendChoice.jsx als Erst-Screen «Lokal oder Cloud» - CloudSetup.jsx: 3-Schritt-Wizard für Erst-Einrichtung - Login.jsx: Modus-Switcher + Server-URL + Studio-Dropdown + Passwort-Vergessen - ResetPassword.jsx: empfängt Mail-Link-Klick via PASSWORD_RECOVERY-Event - Realtime: Änderungen zwischen Browsern ohne Reload sichtbar - Settings → System: Cloud-Verbindung, Studio-Switcher, weiteres Studio anlegen - Settings → Team: Mitarbeiter via Email einladen (Admin-Aktion) - Personen-Sharing: bei neuem Studio Personen aus anderen Studios übernehmen - Reload-Resume: studio_id in sessionStorage, kein erneuter Login nötig Web-Deploy - deploy/docker-compose.yml + nginx.conf: dist/ via nginx-Container, Port 8080 - .env.production.example: Build-time Cloud-URL - DEPLOY.md: Anleitung für LAN-only und extern via Nginx Proxy Manager Doku - README.md: Cloud-Variante prominent erklärt - ARCHITECTURE.md: Storage-Adapter, Migrations, neue Views in Risiko-Tabelle - DEPLOY.md: Schritt-für-Schritt für Mac Mini + NPM Version-Bump auf 0.8.0 in package.json, src-tauri/tauri.conf.json, Cargo.toml. Changelog-Entry im App.jsx-Modal (Karim sieht ihn beim ersten Start). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
650 lines
18 KiB
JavaScript
650 lines
18 KiB
JavaScript
// 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,
|
|
}),
|
|
};
|