// ─── Mietvertrag-Workflow ───────────────────────────────────── // Vier geführte Schritte + Übersicht. Wird für Neuanlage UND Bearbeiten // benutzt (mode="new" | "edit") und liefert die Übersichtskarten, die in // den Akten die Vertragsansicht bilden (RentalSummaryCards). const { useState: useStateW, useEffect: useEffectW, useRef: useRefW } = React; const WZ_STEPS = [ { id: 'mieter', label: 'Mieter', title: 'Wer mietet?', sub: 'Name, Kontakt und Ausweis' }, { id: 'zeit', label: 'Zeitraum', title: 'Wann & wie?', sub: 'Zeitraum, Uhrzeit, Abholung' }, { id: 'equip', label: 'Equipment', title: 'Was wird gemietet?', sub: 'Geräte und Zubehör' }, { id: 'preis', label: 'Preis', title: 'Preis & Status', sub: 'Kaution, Rabatt, Zahlung' }, { id: 'check', label: 'Übersicht', title: 'Alles prüfen', sub: 'Letzter Blick vor dem Speichern' }, ]; // ── kleine Bausteine ──────────────────────────────────────── function WzLabel({ t, children, top = 18 }) { return
{children}
; } function WzCard({ t, children, pad = 0 }) { return
{children}
; } function WzRow({ t, label, value, hint, last }) { return (
{label}
{value || '—'}
{hint ?
{hint}
: null}
); } // Fortschritt: Punkte, aktueller Schritt als Pille — antippbar (freies Springen) function WzDots({ t, steps, idx, onJump, done }) { const Pressable = window.Pressable; return (
{steps.map((s, i) => { const cur = i === idx; const filled = cur || (i < idx && done(i)); return ( onJump(i)} scale={0.86}>
{cur && {s.label}}
); })}
); } // ── Verfügbarkeits-Block (Equipment im Zeitraum) ──────────── function WzAvailability({ t, rental, rentals = [], events = [], excludeRentalId }) { const items = window.getRentalItems(rental) || []; if (!items.length) return null; const conflicts = items.flatMap((it) => (window.equipmentConflicts(it.equipmentId, rental.start, rental.end, rentals, events, excludeRentalId) || []) .map((c) => ({ ...c, equipmentName: it.equipmentName }))); const free = conflicts.length === 0; return (
{free ? : }
{free ? 'Alle Positionen verfügbar' : 'Doppelbuchung – mind. eine Position belegt'}
{free ?
{items.length} Position{items.length > 1 ? 'en' : ''} im Zeitraum {window.fmtRange(rental.start, rental.end)} frei.
:
{conflicts.map((c) => (
{c.equipmentName} · {c.title} · {window.fmtRange(c.start, c.end)}
))}
}
); } // ── Preisaufstellung (groß) ───────────────────────────────── function WzPriceBlock({ t, rental, big = true }) { const days = window.billableDays(rental); const calDays = window.daysBetween(rental.start, rental.end); const items = window.getRentalItems(rental) || []; const sub = items.reduce((s, it) => s + days * (Number(it.dailyRate) || 0) * Math.max(1, Number(it.quantity) || 1), 0); const accCost = window.rentalAccessoryCost ? window.rentalAccessoryCost(rental) : 0; const deliveryFee = (rental.delivery && rental.delivery.enabled && window.rentalDeliveryFee) ? window.rentalDeliveryFee(rental) : 0; const disc = Math.max(0, Number(rental.discount) || 0); const computed = Math.max(0, sub + accCost + deliveryFee - disc); const hasOverride = rental.priceOverride != null && rental.priceOverride !== ''; const total = hasOverride ? Math.max(0, Number(rental.priceOverride) || 0) : computed; const line = (label, val, color) => (
{label}
{val}
); return (
{line(`Equipment · ${days} Tag${days > 1 ? 'e' : ''}${days !== calDays ? ` (von ${calDays})` : ''}`, `${sub} €`)} {accCost > 0 && line(`Zubehör · ${days} Tag${days > 1 ? 'e' : ''}`, `+ ${accCost} €`)} {deliveryFee > 0 && line(`Lieferung${(rental.delivery || {}).feeMode === 'manual' ? ' · Festpreis' : ''}`, `+ ${Math.round(deliveryFee)} €`)} {disc > 0 && line('Rabatt', `− ${disc} €`, t.red)} {hasOverride && line('automatisch wäre', `${computed} €`, t.textTer)}
Gesamt
{Number(rental.deposit) > 0 &&
zzgl. {Number(rental.deposit)} € Kaution
}
{total} €
); } // ───────────────────────────────────────────────────────────── // Übersichtskarten — Workflow-Schritt 5 UND Vertragsansicht in Akten // ───────────────────────────────────────────────────────────── function RentalSummaryCards({ rental, equipment = [], accessories = [], allRentals = [], events = [], blockedCustomers = [], onEditStep, onViewPhoto, excludeRentalId, inset = 0, showIdentity = true }) { const t = window.useTheme(); const Pressable = window.Pressable; const items = window.getRentalItems(rental) || []; const accItems = rental.accessoryItems || []; const del = rental.delivery || {}; const blockedHit = window.findBlockedCustomer(rental.tenantName, blockedCustomers); const days = window.billableDays(rental); const calDays = window.daysBetween(rental.start, rental.end); const allDay = !rental.startTime && !rental.endTime; const sub = items.reduce((s, it) => s + days * (Number(it.dailyRate) || 0) * Math.max(1, Number(it.quantity) || 1), 0); const accCost = window.rentalAccessoryCost ? window.rentalAccessoryCost(rental) : 0; const deliveryFee = (del.enabled && window.rentalDeliveryFee) ? Math.round(window.rentalDeliveryFee(rental)) : 0; const disc = Math.max(0, Number(rental.discount) || 0); const computed = Math.max(0, sub + accCost + deliveryFee - disc); const hasOverride = rental.priceOverride != null && rental.priceOverride !== ''; const total = hasOverride ? Math.max(0, Number(rental.priceOverride) || 0) : computed; const sm = window.statusMeta(rental.status); const pm = window.paymentMeta(rental.paymentStatus || 'offen'); const dm = window.depositMeta(rental.depositStatus || 'offen'); // Einheitlicher Kartenkopf: gleiche Höhe, gleiche Typo, gleicher Bearbeiten-Button const Head = ({ title, step }) => (
{title}
{onEditStep && step && ( onEditStep(step)} scale={0.92}>
Bearbeiten
)}
); const Card = ({ title, step, children }) => (
{children}
); // Kompakte Wertzeile — eine Typo-Größe für alle Labels, eine für alle Werte const Line = ({ label, value, hint, strong, first, color }) => (
{label}
{value || '—'}
{hint ?
{hint}
: null}
); return (
{/* Kopf — Preis und Zeitraum zuerst */}
{showIdentity && (
{rental.tenantName || 'Ohne Namen'}
{sm.label}
)}
Zeitraum
{window.fmtRange(rental.start, rental.end)}
{calDays} Tag{calDays > 1 ? 'e' : ''}{days !== calDays ? ` · ${days} berechnet` : ''}{allDay ? ' · ganzer Tag' : ` · ${rental.startTime}–${rental.endTime}`}
{total} €
{hasOverride ? 'Festpreis' : 'Gesamt'}{Number(rental.deposit) > 0 ? ` · ${Number(rental.deposit)} € Kaution` : ''}
{[{ k: 'Miete', m: pm }, { k: 'Kaution', m: dm }].map(({ k, m }, i) => (
{k} {m.short || m.label}
))}
{/* Equipment & Zubehör */} {items.length === 0 && accItems.length === 0 ? (
Keine Positionen — der Vorgang läuft nur über die Notiz.
) : ( <> {items.map((it, i) => (
{Math.max(1, Number(it.quantity) || 1)}×
{it.equipmentName}
{Number(it.dailyRate) || 0} €/Tag
{days * (Number(it.dailyRate) || 0) * Math.max(1, Number(it.quantity) || 1)} €
))} {accItems.map((a, i) => (
{Math.max(1, Number(a.quantity) || 1)}×
{a.name}
Zubehör
0 ? t.text : t.textTer, fontVariantNumeric: 'tabular-nums' }}> {(Number(a.dailyRate) || 0) > 0 ? `${days * Number(a.dailyRate) * Math.max(1, Number(a.quantity) || 1)} €` : 'inkl.'}
))} {(() => { const conflicts = items.flatMap((it) => (window.equipmentConflicts(it.equipmentId, rental.start, rental.end, allRentals, events, excludeRentalId) || []).map((c) => ({ ...c, equipmentName: it.equipmentName }))); if (!items.length) return null; const free = conflicts.length === 0; return (
{free ? 'Im Zeitraum verfügbar' : 'Doppelbuchung'}
{!free && conflicts.map((c) => (
{c.equipmentName} · {c.title} · {window.fmtRange(c.start, c.end)}
))}
); })()} )} {/* Mieter */} {rental.phone ? : null} {rental.email ? : null} {rental.address ? : null} {!rental.phone && !rental.email && !rental.address ? : null} {(rental.photos || []).length ? 1 ? 's' : ''}`} /> : null} {blockedHit && (
Kunde ist gesperrt
{blockedHit.reason ?
{blockedHit.reason}
: null}
)}
{/* Abholung & Rückgabe */} {del.enabled ? : null} {del.enabled ? : null} {!allDay ? : null} {!allDay ? : null} {rental.purpose ? : null} {/* Preis im Detail */} 1 ? 'e' : ''}`} value={`${sub} €`} /> {accCost > 0 ? : null} {deliveryFee > 0 ? : null} {disc > 0 ? : null} {Number(rental.deposit) > 0 ? : null} {hasOverride ? : null}
Gesamt
{total} €
{/* Notiz */} {rental.note ? (
{rental.note}
) : null}
); } // ───────────────────────────────────────────────────────────── // Der Workflow selbst // ───────────────────────────────────────────────────────────── function RentalWizard({ open, onClose, onSave, equipment = [], accessories = [], tabSettings = {}, rentals = [], events = [], prefill = null, initial = null, mode = 'new', initialStep = 'mieter', logistik = {}, company = {}, blockedCustomers = [], toast, }) { const t = window.useTheme(); const Pressable = window.Pressable; const Sheet = window.Sheet; const Icon = window.Icon; const MiniRangePicker = window.MiniRangePicker; const [palette] = window.useLocal('rf-event-colors', window.EVENT_COLORS); const blank = { tenantName: '', address: '', email: '', phone: '', items: [], accessoryItems: [], purpose: '', start: window.todayISO(), end: window.todayISO(), billDays: '', startTime: '09:00', endTime: '18:00', deposit: 100, discount: 0, note: '', color: '', delivery: { enabled: false, km: '', address: '' }, status: 'reserviert', depositStatus: 'offen', paymentStatus: 'offen', photos: [], }; const [form, setForm] = useStateW(blank); const [step, setStep] = useStateW(0); const [showPicker, setShowPicker] = useStateW(true); useEffectW(() => { if (!open) return; const base = { ...blank, ...(initial || prefill || {}) }; if ((!base.items || !base.items.length) && base.equipmentId) { const eq = equipment.find((e) => e.id === base.equipmentId); if (eq) base.items = [{ equipmentId: eq.id, equipmentName: eq.name, dailyRate: Number(base.dailyRate || eq.price) || 0, quantity: Math.max(1, Number(base.quantity) || 1) }]; } if (!base.items) base.items = []; setForm(base); const i = WZ_STEPS.findIndex((s) => s.id === initialStep); setStep(i < 0 ? 0 : i); }, [open, initialStep]); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const hasItems = !!(form.items && form.items.length); const hasNote = !!(form.note && form.note.trim()); const hasName = !!(form.tenantName || '').trim(); const valid = hasName && (hasItems || hasNote); const isEdit = mode === 'edit'; const excludeId = isEdit ? (initial || {}).id : null; // Schritt gilt als erledigt, sobald das Wesentliche drin ist const stepDone = (i) => { if (i === 0) return hasName; if (i === 1) return !!form.start && !!form.end; if (i === 2) return hasItems || hasNote; if (i === 3) return valid; return false; }; // ── Ausweis-Fotos ── const idPhotoRef = useRefW(null); const [pendingSide, setPendingSide] = useStateW(null); const triggerIdPhoto = (side) => { setPendingSide(side); if (idPhotoRef.current) { idPhotoRef.current.value = ''; idPhotoRef.current.click(); } }; const onIdPhotoFile = async (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const side = pendingSide; setPendingSide(null); try { const dataUrl = await window.compressImage(file, 900); const label = side === 'front' ? 'Ausweis Vorderseite' : 'Ausweis Rückseite'; setForm((f) => ({ ...f, photos: [...(f.photos || []).filter((p) => p.label !== label), { id: 'ph-' + Date.now(), label, dataUrl, addedAt: window.todayDE() }] })); } catch (err) { toast && toast('Foto konnte nicht geladen werden'); } }; const removeIdPhoto = (side) => { const label = side === 'front' ? 'Ausweis Vorderseite' : 'Ausweis Rückseite'; setForm((f) => ({ ...f, photos: (f.photos || []).filter((p) => p.label !== label) })); }; const fInp = { width: '100%', padding: '13px 14px', borderRadius: 13, border: `0.5px solid ${t.inputBorder}`, background: t.card, fontSize: 15, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box' }; const days = window.billableDays(form); const calDays = window.daysBetween(form.start, form.end); const blockedHit = window.findBlockedCustomer(form.tenantName, blockedCustomers); const save = () => { if (!valid) return; const primary = (form.items && form.items[0]) || null; const out = { ...(initial || {}), ...form, id: (initial && initial.id) || 'r-' + Date.now(), items: (form.items || []).map((it) => ({ equipmentId: it.equipmentId, equipmentName: it.equipmentName, dailyRate: Number(it.dailyRate) || 0, quantity: Math.max(1, Number(it.quantity) || 1) })), billDays: (form.billDays === '' || Number(form.billDays) === calDays) ? '' : Math.max(1, Math.floor(Number(form.billDays) || 1)), equipmentId: primary ? primary.equipmentId : '', equipmentName: primary ? primary.equipmentName : '', dailyRate: primary ? primary.dailyRate : 0, quantity: primary ? primary.quantity : 1, deposit: Number(form.deposit) || 0, discount: Number(form.discount) || 0, status: window.autoRentalStatus(form.start, form.end), depositStatus: form.depositStatus || 'offen', paymentStatus: form.paymentStatus || 'offen', }; onSave && onSave(out); onClose && onClose(); }; const cur = WZ_STEPS[step]; // ─── Schritt 1: Mieter ─── const StepMieter = (
Name set('tenantName', e.target.value)} placeholder="Vor- und Nachname *" style={{ ...fInp, fontSize: 17, fontWeight: 600, border: blockedHit ? `1.5px solid ${t.red}` : fInp.border }} /> {blockedHit && (
Kunde ist gesperrt
{blockedHit.reason || 'Kein Grund hinterlegt.'}
Gesperrt seit {window.fmtDateDE(blockedHit.since)}
)} Kontakt · optional