import React, { useState } from "react"; import { generateId, formatCHF, formatDate, formatIban, calcLohn } from "../utils.js"; import { Header, useConfirm, NavArrows } from "../components/UI.jsx"; export default function Payroll({ data, update, saveAll, setPrintContent, setView }) { const now = new Date(); const [selYear, setSelYear] = useState(now.getFullYear()); const [selMonth, setSelMonth] = useState(now.getMonth()); const [selEmpId, setSelEmpId] = useState(""); const [bruttoOverride, setBruttoOverride] = useState({}); const [pensumOverride, setPensumOverride] = useState({}); const [bonusOverride, setBonusOverride] = useState({}); // empId -> bonus CHF const { askConfirm, ConfirmModalEl } = useConfirm(); const employees = data.employees || []; const lohnEntries = data.lohnEntries || []; const months = ["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"]; const monatStr = `${selYear}-${String(selMonth + 1).padStart(2, "0")}`; const todayYear = now.getFullYear(); const todayMonth = now.getMonth(); const isAtFuture = selYear > todayYear || (selYear === todayYear && selMonth >= todayMonth); const goNext = () => { if (isAtFuture) return; if (selMonth === 11) { setSelMonth(0); setSelYear(y => y + 1); } else setSelMonth(m => m + 1); }; const goPrev = () => { if (selMonth === 0) { setSelMonth(11); setSelYear(y => y - 1); } else setSelMonth(m => m - 1); }; const isMonatAvailable = (emp) => { // Nicht in die Zukunft (aktueller Monat ist max) if (selYear > todayYear || (selYear === todayYear && selMonth > todayMonth)) return false; // Nicht vor Eintrittsdatum if (emp.eintrittsdatum) { const parts = emp.eintrittsdatum.split("-"); const ey = parseInt(parts[0]); const em = parseInt(parts[1]) - 1; // 0-basiert if (selYear < ey || (selYear === ey && selMonth < em)) return false; } return true; }; const getEffBrutto = (emp) => bruttoOverride[emp.id] != null ? bruttoOverride[emp.id] : (emp.monatslohn || 0); const getEffPensum = (emp) => pensumOverride[emp.id] != null ? pensumOverride[emp.id] : (emp.pensum || 100); const offeneSpesen = (empId) => (data.expenses || []).filter(e => e.employeeId === empId && (e.date || "").startsWith(monatStr) && !e.lohnEntryId ); const findEntry = (empId) => lohnEntries.find(l => l.monat === monatStr && l.employeeId === empId); const abschliessen = (emp) => { if (findEntry(emp.id)) return; const spesen = offeneSpesen(emp.id); const effBrutto = getEffBrutto(emp); const effPensum = getEffPensum(emp); const bonus = bonusOverride[emp.id] || 0; const calc = calcLohn({ ...emp, monatslohn: effBrutto, pensum: effPensum }, monatStr, spesen, bonus); // Alle Sätze statisch festhalten — unabhängig von späteren Änderungen const saetzeSnapshot = { ahvSatz: emp.ahvSatz ?? 5.3, alvSatz: emp.alvSatz ?? 1.1, bvgSatz: emp.bvgSatz ?? 8.0, nbuSatz: emp.nbuSatz ?? 1.5, ktgSatz: emp.ktgSatz ?? 0.5, quellensteuerPflichtig: emp.quellensteuerPflichtig || false, quellensteuerSatz: emp.quellensteuerSatz ?? 10, dreizehnterLohn: emp.dreizehnterLohn || false, pensum: effPensum, eintrittsdatum: emp.eintrittsdatum || "", pkAGSatz: emp.pkAGSatz ?? 8.0, }; const entry = { id: generateId(), monat: monatStr, employeeId: emp.id, empSnapshot: { name: emp.name, role: emp.role, adresse: emp.adresse, ort: emp.ort, ahvNr: emp.ahvNr, personalNr: emp.personalNr, lohnIban: emp.lohnIban }, saetzeSnapshot, bruttoWasOverridden: bruttoOverride[emp.id] != null, bonusBeschrieb: bonusOverride[`${emp.id}_beschrieb`] || "", ...calc, spesenIds: spesen.map(s => s.id), createdAt: new Date().toISOString(), }; const updatedExp = (data.expenses || []).map(e => spesen.some(s => s.id === e.id) ? { ...e, lohnEntryId: entry.id, status: "ausbezahlt" } : e ); saveAll({ ...data, lohnEntries: [...lohnEntries, entry], expenses: updatedExp }); }; const stornieren = async (id) => { if (!(await askConfirm("Lohnabrechnung stornieren? Spesen werden wieder freigegeben.", "Stornieren"))) return; const updatedExp = (data.expenses || []).map(e => e.lohnEntryId === id ? { ...e, lohnEntryId: null, status: "auf nächsten Lohn" } : e); saveAll({ ...data, lohnEntries: lohnEntries.filter(l => l.id !== id), expenses: updatedExp }); }; const selectedEmp = employees.find(e => e.id === selEmpId); const previewSpesen = selEmpId ? offeneSpesen(selEmpId) : []; const previewCalc = selectedEmp ? calcLohn({ ...selectedEmp, monatslohn: getEffBrutto(selectedEmp), pensum: getEffPensum(selectedEmp) }, monatStr, previewSpesen, bonusOverride[selectedEmp?.id] || 0) : null; const existingEntry = selEmpId ? findEntry(selEmpId) : null; const monatEntries = lohnEntries.filter(l => l.monat === monatStr); const cardPensum = existingEntry ? (existingEntry.saetzeSnapshot?.pensum || 100) : (selectedEmp ? getEffPensum(selectedEmp) : 100); const LohnRow = ({ label, value, bold, color, indent }) => (
{label}{formatCHF(value)}
); const renderCalc = (calc, emp, isPreview, displayPensum) => (
{isPreview && (
MONATSLOHN (100% BASIS) — für diesen Lohnlauf anpassbar
setBruttoOverride(o => ({ ...o, [emp.id]: +e.target.value }))} style={{ flex: 1, height: 32, fontSize: 13, textAlign: "right", border: bruttoOverride[emp.id] != null && bruttoOverride[emp.id] !== emp.monatslohn ? "1.5px solid #b5621e" : "1px solid #c8c0b8" }} /> CHF {bruttoOverride[emp.id] != null && bruttoOverride[emp.id] !== emp.monatslohn && ( )}
{bruttoOverride[emp.id] != null && bruttoOverride[emp.id] !== emp.monatslohn && (
Abweichend von Stammdaten ({formatCHF(emp.monatslohn || 0)})
)}
PENSUM DIESEN MONAT
setPensumOverride(o => ({ ...o, [emp.id]: +e.target.value }))} style={{ width: 72, height: 32, fontSize: 13, textAlign: "right", border: pensumOverride[emp.id] != null && pensumOverride[emp.id] !== (emp.pensum || 100) ? "1.5px solid #b5621e" : "1px solid #c8c0b8" }} /> % {pensumOverride[emp.id] != null && pensumOverride[emp.id] !== (emp.pensum || 100) && ( )}
{pensumOverride[emp.id] != null && pensumOverride[emp.id] !== (emp.pensum || 100) && (
Abweichend von Stammdaten ({emp.pensum || 100}%)
)}
EINMALZAHLUNG / BONUS (AHV-pflichtig)
setBonusOverride(o => ({ ...o, [emp.id]: +e.target.value || 0 }))} placeholder="0" style={{ width: 110, height: 32, fontSize: 13, textAlign: "right", border: bonusOverride[emp.id] ? "1.5px solid #2d6a4f" : "1px solid #c8c0b8" }} /> CHF setBonusOverride(o => ({ ...o, [`${emp.id}_beschrieb`]: e.target.value }))} placeholder="Beschrieb (z.B. Jahresbonus)" style={{ flex: 1, height: 32, fontSize: 12, border: "1px solid #c8c0b8" }} />
)}
Monatslohn {months[selMonth]} {selYear}{displayPensum < 100 ? ` (${displayPensum}%)` : ""} {formatCHF(calc.brutto)}
{displayPensum < 100 && calc.bruttoBase != null && calc.bruttoBase !== calc.brutto && (
Basis 100%: {formatCHF(calc.bruttoBase)} × {displayPensum}%
)} {calc.dreizehnter > 0 && } {calc.bonusBetrag > 0 && }
ABZÜGE ARBEITNEHMER
{(() => { const s = emp.saetzeSnapshot || emp; // abgeschlossen: snapshot, preview: live emp return <> {calc.qst > 0 && } ; })()}
{calc.spesenTotal > 0 && <>
SPESENERSTATTUNG
}
{calc.bvgAG > 0 && (() => { const s = emp.saetzeSnapshot || emp; return (
LOHNKOSTEN ARBEITGEBER
); })()}
); return (
{ConfirmModalEl}
{employees.length === 0 ? (
NameBruttoNettoSpesenAuszahlungStatus
Noch keine Mitarbeiter erfasst
) : (
{months[selMonth]} {selYear}
MITARBEITER
{employees.filter(e => e.monatslohn > 0).length === 0 && ( )} {employees.filter(e => e.monatslohn > 0).map(emp => { const available = isMonatAvailable(emp); const entry = findEntry(emp.id); const spesen = entry ? (data.expenses || []).filter(e => e.lohnEntryId === entry.id) : offeneSpesen(emp.id); const effBrutto = getEffBrutto(emp); const calc = entry ? entry : calcLohn({ ...emp, monatslohn: effBrutto, pensum: getEffPensum(emp) }, monatStr, spesen); return ( available && setSelEmpId(emp.id)} style={{ cursor: available ? "pointer" : "default", background: selEmpId === emp.id ? "#faf8f5" : "", opacity: available ? 1 : 0.35 }}> ); })} {monatEntries.length > 0 && (() => { const t = monatEntries.reduce((s,l) => ({ b:s.b+l.bruttoTotal, n:s.n+l.netto, sp:s.sp+l.spesenTotal, a:s.a+l.auszahlung }), {b:0,n:0,sp:0,a:0}); return ( ); })()}
Name Brutto Netto Spesen Auszahlung Status
Noch kein Monatslohn hinterlegt.
{emp.name} {emp.role &&
{emp.role}
} {!available &&
vor Eintritt
}
{available ? formatCHF(calc.bruttoTotal) : "—"} {available ? formatCHF(calc.netto) : "—"} 0 ? "#b5621e" : "#aaa" }}>{available && calc.spesenTotal > 0 ? formatCHF(calc.spesenTotal) : "—"} {available ? formatCHF(calc.auszahlung) : "—"} {!available ? null : entry ? Abgeschlossen : Offen} {available && (entry ? : )}
Total {formatCHF(t.b)} {formatCHF(t.n)} {formatCHF(t.sp)} {formatCHF(t.a)}
{selectedEmp && isMonatAvailable(selectedEmp) ? (
LOHNABRECHNUNG
{selectedEmp.name}
{[selectedEmp.role, `${months[selMonth]} ${selYear}`, cardPensum < 100 ? `${cardPensum}%` : null].filter(Boolean).join(" · ")}
{existingEntry ? renderCalc(existingEntry, { ...selectedEmp, ...existingEntry.empSnapshot, saetzeSnapshot: existingEntry.saetzeSnapshot }, false, existingEntry.saetzeSnapshot?.pensum || 100) : renderCalc(previewCalc, selectedEmp, true, getEffPensum(selectedEmp)) } {(() => { const sp = existingEntry ? (data.expenses||[]).filter(e => e.lohnEntryId === existingEntry.id) : previewSpesen; return sp.length > 0 ? (
SPESEN DETAIL
{sp.map(s => (
{s.category}{s.description ? ` — ${s.description}` : ""} {formatCHF(s.amount)}
))}
) : null; })()}
{existingEntry ? ( <> ) : ( )}
{!existingEntry && !selectedEmp.monatslohn && bruttoOverride[selectedEmp.id] == null && (
Kein Monatslohn hinterlegt.
)}
) : (
Mitarbeiter auswählen für Detailansicht
)}
)}
); }