Release 0.8.0: Cloud-Variante (Supabase, Multi-Studio, Realtime, Web-Deploy)
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>
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
// 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,
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user