Rapport 0.6 — Initial Public Release

Sicherheits-Hardening
- Passwort-Hashing mit PBKDF2 (SHA-256, 100k Iterationen) inkl. transparenter
  Migration bestehender Klartext-Passwörter beim ersten Login
- Login Brute-Force-Schutz (5 Fehlversuche → 60s Lockout), Constant-Time-Compare,
  Mindestpasswortlänge 8 Zeichen
- HTML-Sanitizer für Brieftexte (Allowlist, entfernt javascript:/data:/vbscript:-URLs,
  Event-Handler, Script-Tags; rel=noopener für target=_blank)
- Datenexport entfernt Legacy-Klartextpasswörter (Hashes bleiben)
- Kryptografische IDs via crypto.randomUUID statt Math.random
- sessionStorage speichert keine Credentials mehr

GUI & Performance
- Code-Splitting pro View via React.lazy + Suspense (Initial-Bundle 86 KB gzipped)
- swissqrbill als lokale Dependency — QR-Rechnungen offline-fähig
- Spesenbelege (Bild/PDF) direkt in der Tageserfassung mit Bildkomprimierung
- Avatar-Upload: 256px-Skalierung + JPEG-Kompression, Typprüfung
- Über-Rapport-Modal, einheitliche Bearbeiten-Icons, Pinnwand-Kategorien als Pills

Bug-Fixes
- Auto-überfällig-Routine läuft nur noch einmal pro Tag (verhindert Re-Render-Loop)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
karim gabriele varano
2026-05-13 01:16:26 +02:00
commit 8de93ff27f
65 changed files with 28010 additions and 0 deletions
+252
View File
@@ -0,0 +1,252 @@
// ─── CONSTANTS ──────────────────────────────────────────────────
export const STORAGE_KEY = "studio_data_v1";
// SIA-Phasen nach SIA 102/103 (Architektur)
export const SIA_PHASES = [
{ id: "11", label: "11 Strategische Planung" },
{ id: "21", label: "21 Vorstudien" },
{ id: "22", label: "22 Machbarkeitsstudie" },
{ id: "31", label: "31 Vorprojekt" },
{ id: "32", label: "32 Bauprojekt" },
{ id: "33", label: "33 Bewilligungsverfahren" },
{ id: "41", label: "41 Ausschreibung" },
{ id: "51", label: "51 Ausführungsprojekt" },
{ id: "52", label: "52 Ausführung" },
{ id: "53", label: "53 Inbetriebnahme / Abschluss" },
];
// SIA 102 Teilleistungen mit Standard-Prozentanteilen
export const SIA_PHASE_WEIGHTS = [
{ id: "31", label: "Vorprojekt", items: [
{ label: "Lösungsstudien / Grobschätzung", pct: 3 },
{ label: "Vorprojekt / Kostenschätzung", pct: 6 },
]},
{ id: "32", label: "Bauprojekt", items: [
{ label: "Bauprojekt", pct: 13 },
{ label: "Detailstudien", pct: 4 },
{ label: "Kostenvoranschlag", pct: 4 },
]},
{ id: "33", label: "Bewilligungsverfahren", items: [
{ label: "Bewilligungsverfahren", pct: 2.5 },
]},
{ id: "41", label: "Ausschreibung", items: [
{ label: "Ausschreibungspläne", pct: 10 },
{ label: "Ausschreibung / Offertvergleich", pct: 8 },
]},
{ id: "51", label: "Ausführungsplanung", items: [
{ label: "Ausführungspläne", pct: 15 },
{ label: "Werkverträge", pct: 1 },
]},
{ id: "52", label: "Ausführung", items: [
{ label: "Gestalterische Leitung", pct: 6 },
{ label: "Bauleitung / Kostenkontrolle", pct: 23 },
]},
{ id: "53", label: "Inbetriebnahme / Abschluss", items: [
{ label: "Inbetriebnahme", pct: 1 },
{ label: "Dokumentation", pct: 1 },
{ label: "Garantiearbeiten", pct: 1.5 },
{ label: "Schlussabrechnung", pct: 1 },
]},
];
// Projekt-Typen
export const PROJECT_TYPES = [
"Wettbewerb",
"Studienauftrag",
"Direktauftrag",
"Machbarkeitsstudie",
"Gutachten",
"Grafik",
"Sonstiges",
];
export const EXPENSE_CATEGORIES = [
"Reise / Fahrt", "Drucken / Reprografie", "Modellbau / Material",
"Büromaterial", "Weiterbildung",
"Unterauftrag / Freelancer", "Verpflegung / Geschäftsessen",
"Sonstiges",
];
export const INTERNAL_EXPENSE_CATEGORIES = [
"Miete / Raumkosten", "Software / Lizenzen", "Hardware / IT",
"Telefon / Internet", "Versicherung", "Steuern / Abgaben",
"Büromaterial", "Marketing / Werbung", "Weiterbildung",
"Unterauftrag / Freelancer", "Bankgebühren", "Sonstiges",
];
export const NAV_ITEMS = [
{ id: "dashboard", label: "Übersicht", icon: "grid_view" },
{ id: "pinnwand", label: "Pinnwand", icon: "campaign" },
{ id: "projects", label: "Projekte", icon: "work" },
{ id: "time", label: "Zeiterfassung", icon: "schedule" },
{ id: "quotes", label: "Offerten", icon: "request_quote" },
{ id: "buchhaltung", label: "Buchhaltung", icon: "account_balance", children: [
{ id: "invoices", label: "Rechnungen" },
{ id: "internal-expenses", label: "Ausgaben" },
{ id: "expenses", label: "Spesen" },
{ id: "loehne", label: "Löhne" },
{ id: "studio-budget", label: "Budget" },
]},
{ id: "dokumente", label: "Dokumente", icon: "folder", children: [
{ id: "protokolle", label: "Protokolle" },
{ id: "lieferscheine", label: "Lieferscheine" },
{ id: "letters", label: "Briefe" },
]},
{ id: "personen", label: "Personen", icon: "group" },
{ id: "mitarbeiter", label: "Mitarbeiter", icon: "badge" },
{ id: "settings", label: "Einstellungen", icon: "settings" },
];
export const STATUS_COLORS = {
aktiv: "#2d6a4f", abgeschlossen: "#555", pausiert: "#b5621e",
entwurf: "#7a6a00", gesendet: "#1a4e8a", bezahlt: "#2d6a4f", überfällig: "#8a1a1a",
offen: "#7a6a00", angenommen: "#2d6a4f", abgelehnt: "#8a1a1a", abgelaufen: "#888",
genehmigt: "#1a4e8a", "auf nächsten Lohn": "#b5621e", ausbezahlt: "#2d6a4f",
};
export const STATUS_BG = {
aktiv: "#e8f5ee", abgeschlossen: "#f0f0ee", pausiert: "#fdf0e8",
entwurf: "#fffbe6", gesendet: "#e8f0fa", bezahlt: "#e8f5ee", überfällig: "#fdf2f2",
offen: "#fffbe6", angenommen: "#e8f5ee", abgelehnt: "#fdf2f2", abgelaufen: "#f2f2f2",
genehmigt: "#e8f0fa", "auf nächsten Lohn": "#fdf0e8", ausbezahlt: "#e8f5ee",
};
export const defaultData = {
settings: {
setupCompleted: false,
name: "Mein Studio",
address: "Musterstrasse 1\n8001 Zürich",
street: "",
zip: "",
city: "",
country: "CH",
email: "mail@studio.ch",
phone: "+41 79 000 00 00",
iban: "CH00 0000 0000 0000 0000 0",
ibanType: "qr", // "qr" | "normal"
mwst: "CHE-000.000.000 MWST",
mwstRate: 8.1,
defaultHourlyRate: 120,
autoPrint: false,
logoSize: 60,
expenseCategories: [...EXPENSE_CATEGORIES],
internalExpenseCategories: [...INTERNAL_EXPENSE_CATEGORIES],
projectNumberFormat: "YYYY/NN",
invoiceNumberFormat: "YYYY-NNN",
protokollNumberFormat: "YYYY-TT-NN",
protokollTypeAbbreviations: {
"Bausitzung": "BS",
"Planungssitzung": "PS",
"Baubesprechung": "BB",
"Jour fixe": "JF",
"Interne Sitzung": "IS",
"Kundensitzung": "KS",
"Abnahme": "AB",
"Sonstiges": "SO",
},
pdfNameFormat: "{studio}_{typ}_{nummer}",
qrNewPage: true,
pageMarginTop: 20,
pageMarginBottom: 20,
pageMarginLeft: 20,
pageMarginRight: 20,
defaultWochenstunden: 35,
defaultFerienWochen: 5,
closedMonths: [],
blockMaiTag: true,
roles: [
{ id: "PL", label: "Projektleiter/in", rate: 140 },
{ id: "TS", label: "Technischer Support", rate: 120 },
{ id: "BL", label: "Bauleiter/in", rate: 135 },
{ id: "AS", label: "Administrativer Support", rate: 120 },
],
},
persons: [],
projects: [],
timeEntries: [],
invoices: [],
quotes: [],
expenses: [],
internalExpenses: [],
deliveryNotes: [],
protocols: [],
employees: [],
feiertage: [],
absences: [],
ferienEntries: [],
absenzTypes: [],
lohnEntries: [],
uberstundenAbschluss: [],
dashboardTemplates: [
{ id: "tpl-admin", name: "Administrator", isPublic: true, layout: [
{ id: "dw-a1", cols: 4, minH: 0, widgets: ["kpi-projekte","kpi-stunden","kpi-ausstehend","kpi-umsatz"] },
{ id: "dw-a2", cols: 1, minH: 0, widgets: ["warnungen"] },
{ id: "dw-a3", cols: 2, minH: 0, widgets: ["aktive-projekte","unverrechnete-stunden"] },
{ id: "dw-a4", cols: 2, minH: 0, widgets: ["umsatz-sparkline","offene-offerten"] },
{ id: "dw-a5", cols: 1, minH: 0, widgets: ["letzte-zeiteintraege"] },
]},
{ id: "tpl-projektleiter", name: "Projektleiter", isPublic: true, layout: [
{ id: "dw-p1", cols: 2, minH: 0, widgets: ["kpi-projekte","kpi-stunden"] },
{ id: "dw-p2", cols: 1, minH: 0, widgets: ["warnungen"] },
{ id: "dw-p3", cols: 3, minH: 0, widgets: ["meine-projekte","team-auslastung","offene-offerten"] },
{ id: "dw-p4", cols: 1, minH: 0, widgets: ["letzte-zeiteintraege"] },
]},
{ id: "tpl-mitarbeiter", name: "Mitarbeiter", isPublic: true, layout: [
{ id: "dw-m1", cols: 3, minH: 0, widgets: ["kpi-stunden","ueberstunden","meine-ferien"] },
{ id: "dw-m2", cols: 2, minH: 0, widgets: ["meine-projekte","stunden-woche"] },
{ id: "dw-m3", cols: 1, minH: 0, widgets: ["meine-zeiteintraege"] },
]},
],
appRoles: [
{ id: "r-admin", name: "Administrator", permissions: null, dashboardTemplateId: "tpl-admin" },
{ id: "r-projektleiter", name: "Projektleiter", permissions: ["dashboard","projects","time","quotes","personen","mitarbeiter","settings"], dashboardTemplateId: "tpl-projektleiter" },
{ id: "r-mitarbeiter", name: "Mitarbeiter", permissions: ["dashboard","projects","time","personen","settings"], dashboardTemplateId: "tpl-mitarbeiter" },
],
users: [
{ id: "admin", username: "admin", password: "admin", role: "admin", displayName: "Administrator", appRoleId: "r-admin" },
],
blogPosts: [],
letterTemplates: [
{ id: "offer", name: "Offerte", body: "Sehr geehrte/r {{client}}\n\nGerne unterbreiten wir Ihnen die Offerte für das Projekt «{{project}}».\n\n[Leistungsumfang]\n\nHonorar: CHF [Betrag]\n\nWir freuen uns auf die Zusammenarbeit.\n\nFreundliche Grüsse" },
{ id: "reminder", name: "Zahlungserinnerung", body: "Sehr geehrte/r {{client}}\n\nBei einer Überprüfung unserer Buchhaltung stellen wir fest, dass die Rechnung [Nr.] vom [Datum] über CHF [Betrag] noch nicht beglichen ist.\n\nWir bitten Sie höflich, den offenen Betrag innert 10 Tagen zu überweisen.\n\nFreundliche Grüsse" },
],
};
export const PROTOKOLL_TYPES = ["Bausitzung", "Planungssitzung", "Baubesprechung", "Jour fixe", "Interne Sitzung", "Kundensitzung", "Abnahme", "Sonstiges"];
export const PROTOKOLL_ENTRY_TYPES = [
{ id: "beschluss", label: "Beschluss", color: "#1a4e8a", bg: "#e8f0fa", icon: "⬡" },
{ id: "info", label: "Info", color: "#2d6a4f", bg: "#e8f5ee", icon: "" },
{ id: "aufgabe", label: "Aufgabe", color: "#b5621e", bg: "#fdf0e8", icon: "◎" },
];
export const DASHBOARD_WIDGETS = [
{ id: "kpi-projekte", label: "Aktive Projekte (KPI)", span: 3 },
{ id: "kpi-stunden", label: "Stunden diesen Monat (KPI)", span: 3 },
{ id: "kpi-ausstehend", label: "Ausstehend (KPI)", span: 3 },
{ id: "kpi-umsatz", label: "Jahresumsatz (KPI)", span: 3 },
{ id: "warnungen", label: "Warnungen", span: 12 },
{ id: "aktive-projekte", label: "Projektliste mit Budget", span: 4 },
{ id: "unverrechnete-stunden", label: "Unverrechnete Stunden", span: 4 },
{ id: "umsatz-sparkline", label: "Umsatz Sparkline", span: 4 },
{ id: "offene-offerten", label: "Offene Offerten", span: 4 },
{ id: "letzte-zeiteintraege", label: "Letzte Zeiteinträge", span: 12 },
{ id: "meine-zeiteintraege", label: "Meine Zeiteinträge", span: 12 },
{ id: "meine-projekte", label: "Meine Projekte", span: 6 },
{ id: "meine-ferien", label: "Ferienstand", span: 4 },
{ id: "ueberstunden", label: "Stundensaldo", span: 4 },
{ id: "stunden-woche", label: "Stunden pro Woche", span: 6 },
{ id: "team-auslastung", label: "Team-Auslastung", span: 6 },
{ id: "interner-blog", label: "Pinnwand", span: 6 },
];
export const DEFAULT_ABSENZ_TYPES = [
{ id: "krankheit", label: "Krankheit", color: "#8a1a1a" },
{ id: "unfall", label: "Unfall", color: "#b5621e" },
{ id: "intern", label: "Intern", color: "#1a4e8a" },
{ id: "informatik", label: "Informatik", color: "#555" },
{ id: "rechnungswesen", label: "Rechnungswesen", color: "#7a6a00" },
{ id: "weiterbildung", label: "Weiterbildung", color: "#2d6a4f" },
{ id: "militaer", label: "Militär / Zivildienst", color: "#3d3d38" },
];