import React, { useState } from "react"; import { SIA_PHASES, PROJECT_TYPES, PROTOKOLL_TYPES } from "../constants.js"; const NON_BILLING_CATEGORIES = ["Wettbewerb"]; import { calcSIAHours, calcManualHours, deriveQuoteBudget, migrateLinkedQuotes, generateId, formatCHF, formatDate, formatHours, applyProjectNumberFormat, parseSeqFromNumber, nextProtoSeq, applyProtoNumberFormat } from "../utils.js"; import { Header, Modal, FormField, StatusBadge, useConfirm , DateInput } from "../components/UI.jsx"; function ProjectEditForm({ form, setForm, data }) { const [newPhaseLabel, setNewPhaseLabel] = useState(""); const togglePhase = (phaseId) => { setForm(prev => { const phases = prev.enabledPhases || []; return { ...prev, enabledPhases: phases.includes(phaseId) ? phases.filter(p => p !== phaseId) : [...phases, phaseId] }; }); }; return ( <>
setForm({ ...form, number: e.target.value })} placeholder={`z.B. ${new Date().getFullYear()}/01`} style={{ maxWidth: 140 }} /> setForm({ ...form, name: e.target.value })} placeholder="z.B. Wohnhaus Müller" autoFocus style={!form.name?.trim() ? { borderColor: "#b5621e" } : {}} />
{NON_BILLING_CATEGORIES.includes(form.category) ? (
Nicht verrechenbar — Stunden werden erfasst, aber nicht fakturiert.
) : ( <> {form.billingType === "stundensatz" ? setForm({ ...form, hourlyRate: +e.target.value })} /> : setForm({ ...form, budget: +e.target.value })} />} )} setForm({ ...form, startDate: e.target.value })} />
setForm({ ...form, budgetHours: +e.target.value, linkedQuotes: [], phasesBudget: [] })} placeholder="0 = aus Offerten berechnet" />
{(() => { const linked = form.linkedQuotes || []; const ROLES = ["Hauptofferte", "Nachtrag", "Referenz"]; const buildPositions = (newLinked, existingPositions) => { const manual = (existingPositions || []).filter(p => !p.quoteId); const nachtraege = newLinked.filter(lq => lq.role === "Nachtrag").map((lq, idx) => { const existing = (existingPositions || []).find(p => p.quoteId === lq.quoteId); const q = (data.quotes || []).find(x => x.id === lq.quoteId); const sd = deriveQuoteBudget([lq], data.quotes || [], data.settings.roles || []); return { code: existing?.code || `N${idx + 1}`, label: existing !== undefined ? existing.label : (q?.projectName || q?.number || ""), enabledPhases: existing?.enabledPhases || sd.enabledPhases, quoteId: lq.quoteId }; }); return [...manual, ...nachtraege]; }; const addQuote = (quoteId) => { if (!quoteId || linked.some(lq => lq.quoteId === quoteId)) return; const isFirst = linked.length === 0; const newLinked = [...linked, { quoteId, role: isFirst ? "Hauptofferte" : "Nachtrag" }]; const d = deriveQuoteBudget(newLinked, data.quotes || [], data.settings.roles || []); const q = (data.quotes || []).find(x => x.id === quoteId); setForm(f => ({ ...f, linkedQuotes: newLinked, budgetHours: d.budgetHours, budgetAmount: d.budgetAmount, phasesBudget: d.phasesBudget, enabledPhases: isFirst ? [...new Set([...(f.enabledPhases || []), ...d.enabledPhases])] : (f.enabledPhases || []), billingType: isFirst ? (q?.mode === "manual" ? "stundensatz" : "pauschal") : f.billingType, positions: buildPositions(newLinked, f.positions || []), })); }; const removeQuote = (quoteId) => { const newLinked = linked.filter(lq => lq.quoteId !== quoteId); const d = newLinked.length > 0 ? deriveQuoteBudget(newLinked, data.quotes || [], data.settings.roles || []) : { budgetHours: 0, budgetAmount: 0, phasesBudget: [] }; setForm(f => ({ ...f, linkedQuotes: newLinked, budgetHours: d.budgetHours, budgetAmount: d.budgetAmount, phasesBudget: d.phasesBudget, positions: buildPositions(newLinked, (f.positions || []).filter(p => p.quoteId !== quoteId)) })); }; const changeRole = (quoteId, role) => setForm(f => { const newLinked = f.linkedQuotes.map(lq => lq.quoteId === quoteId ? { ...lq, role } : lq); return { ...f, linkedQuotes: newLinked, positions: buildPositions(newLinked, f.positions || []) }; }); const updateNachtragPos = (quoteId, updates) => setForm(f => ({ ...f, positions: (f.positions || []).map(p => p.quoteId === quoteId ? { ...p, ...updates } : p) })); const toggleHOPhase = (phId) => setForm(f => { const phases = f.enabledPhases || []; return { ...f, enabledPhases: phases.includes(phId) ? phases.filter(p => p !== phId) : [...phases, phId] }; }); const toggleNTPhase = (quoteId, phId) => setForm(f => ({ ...f, positions: (f.positions || []).map(p => { if (p.quoteId !== quoteId) return p; const phases = p.enabledPhases || []; return { ...p, enabledPhases: phases.includes(phId) ? phases.filter(x => x !== phId) : [...phases, phId] }; }) })); const alreadyInOtherProjects = new Set((data.projects || []).filter(p => p.id !== form.id).flatMap(p => migrateLinkedQuotes(p).map(lq => lq.quoteId))); const unlinkedQuotes = (data.quotes || []).filter(q => !linked.some(lq => lq.quoteId === q.id) && !alreadyInOtherProjects.has(q.id)); const sortedLinked = [...linked.filter(lq => lq.role === "Hauptofferte"), ...linked.filter(lq => lq.role !== "Hauptofferte")]; return (
VERKNÜPFTE HONORAROFFERTEN
{sortedLinked.map(lq => { const q = (data.quotes || []).find(x => x.id === lq.quoteId); if (!q) return null; const isNT = lq.role === "Nachtrag"; const pos = isNT ? (form.positions || []).find(p => p.quoteId === lq.quoteId) : null; const currentPhases = isNT ? (pos?.enabledPhases || []) : (form.enabledPhases || []); const qRoles = q.quoteRoles || data.settings.roles || []; const qH = q.mode === "sia" ? (calcSIAHours(q.sia?.baukosten, q.sia?.schwierigkeit, q.sia?.phases || []).total || 0) : q.mode === "manual" ? (calcManualHours(q.manualPhases || [], qRoles).totalHours || 0) : 0; const bgColor = isNT ? "#f0f5fd" : "#faf8f5"; const borderColor = isNT ? "#c8d8ee" : "#ddd8d0"; const accentColor = isNT ? "#1a4e8a" : "#2d6a4f"; return (
{isNT && pos ? ( <> updateNachtragPos(lq.quoteId, { code: e.target.value.toUpperCase().replace(/\s/g, "").slice(0, 6) })} placeholder="N1" style={{ width: 52, fontWeight: 700, fontSize: 12, height: 26, padding: "0 6px" }} /> updateNachtragPos(lq.quoteId, { label: e.target.value })} placeholder="z.B. Nachtrag Fassade" style={{ flex: 1, minWidth: 100, fontSize: 12, height: 26, padding: "0 6px" }} /> ) : ( ⬡ HO )} {q.number} {q.mode === "sia" ? "SIA 102" : q.mode === "manual" ? "Aufwand" : "Pauschal"} {qH > 0 && {qH.toFixed(1)}h}
AKTIVIERTE SIA-PHASEN FÜR ZEITERFASSUNG
{SIA_PHASES.map(ph => ( ))}
); })} {linked.length === 0 &&
Noch keine Offerte verknüpft.
} {unlinkedQuotes.length > 0 && ( )}
); })()}