// ───────────────────────────────────────────────────────────── // Übergabe- / Rückgabe-Workflows // Guided step-by-step process for handing equipment over to a tenant // (Übergabe) and taking it back (Rückgabe), with checklists, deposit / // fee / ID confirmation, and a damage/missing report that writes straight // into the Lager (equipment.repairQty / accessory.defectQty). // // Data model — rf-workflows: { [rentalId]: { // handover: { checks:{[key]:bool}, deposit:bool, fee:bool, id:bool, done:bool, doneAt:iso }, // ret: { checks:{[key]:bool}, depositBack:bool, done:bool, doneAt:iso }, // reports: [{ id, itemType:'equipment'|'accessory', itemId, name, issue:'defect'|'missing', qty, note, at }] // }} // ───────────────────────────────────────────────────────────── const { useState: useStateWF, useEffect: useEffectWF } = React; // Blank workflow record function wfBlank() { return { handover: { checks: {}, deposit: false, fee: false, id: false, done: false, doneAt: null }, ret: { checks: {}, depositBack: false, done: false, doneAt: null }, reports: [] }; } function wfGet(workflows, rentalId) { const w = workflows && workflows[rentalId]; if (!w) return wfBlank(); return { ...wfBlank(), ...w, handover: { ...wfBlank().handover, ...(w.handover || {}) }, ret: { ...wfBlank().ret, ...(w.ret || {}) }, reports: w.reports || [] }; } // Which action is due for a rental? 'handover' | 'return' | null // handover: not yet handed over and pickup within ±2 days (or already past-due). // return: handed over, not yet returned, and end within ±2 days (or past-due). function wfDue(rental, workflows, today) { if (!rental || rental.status === 'abgeschlossen') return null; const w = wfGet(workflows, rental.id); const near = (iso, back = 2, fwd = 2) => { if (!iso) return false; const d = Math.round((fromISO(iso) - fromISO(today)) / 86400000); return d >= -60 && d <= fwd; // due once within window, stays due if overdue }; if (!w.handover.done && near(rental.start)) return 'handover'; if (w.handover.done && !w.ret.done && near(rental.end)) return 'return'; return null; } // Build the equipment + accessory line items a workflow checklist covers. function wfLines(rental, accessories = []) { const eq = (window.getRentalItems ? window.getRentalItems(rental) : []).map(it => ({ key: 'e:' + it.equipmentId, type: 'equipment', id: it.equipmentId, name: it.equipmentName || it.name, qty: Math.max(1, Number(it.quantity) || 1), })); const acc = (rental.accessoryItems || []).map(a => ({ key: 'a:' + a.accessoryId, type: 'accessory', id: a.accessoryId, name: a.name, qty: Math.max(1, Number(a.quantity) || 1), cat: (accessories.find(x => x.id === a.accessoryId) || {}).cat, })); return { eq, acc, all: [...eq, ...acc] }; } // Overall workflow progress label function wfStatusMeta(w, t) { if (w.ret.done) return { label: 'Abgeschlossen', color: t.textSec, tint: t.chip }; if (w.handover.done) return { label: 'Läuft · Rückgabe offen', color: t.accent, tint: t.accentSoft }; return { label: 'Übergabe offen', color: t.green, tint: t.greenSoft }; } // ─── The step-by-step runner sheet ─────────────────────────── function WorkflowSheet({ open, mode, rental, workflows, setWorkflows, equipment, setEquipment, accessories, setAccessories, setRentals, onClose, toast }) { const t = useTheme(); const [step, setStep] = useStateWF(0); const [reportFor, setReportFor] = useStateWF(null); // line being reported useEffectWF(() => { if (open) { setStep(0); setReportFor(null); } }, [open, rental && rental.id, mode]); if (!rental) return null; const w = wfGet(workflows, rental.id); const lines = wfLines(rental, accessories); const patch = (fn) => setWorkflows(prev => { const cur = wfGet(prev, rental.id); const next = fn(JSON.parse(JSON.stringify(cur))); return { ...prev, [rental.id]: next }; }); const isReturn = mode === 'return'; const branch = isReturn ? w.ret : w.handover; const setCheck = (key, val) => patch(c => { const b = isReturn ? c.ret : c.handover; b.checks[key] = val; return c; }); const allChecked = lines.all.length > 0 && lines.all.every(l => branch.checks[l.key]); const toggleAll = (val) => patch(c => { const b = isReturn ? c.ret : c.handover; lines.all.forEach(l => { b.checks[l.key] = val; }); return c; }); // Damage / missing report — writes into Lager const submitReport = (rep) => { patch(c => { c.reports = [...(c.reports || []), { ...rep, id: 'rep-' + Date.now().toString(36), at: todayISO() }]; return c; }); if (rep.itemType === 'equipment' && setEquipment) { setEquipment(prev => prev.map(e => e.id === rep.itemId ? { ...e, repairQty: Math.max(0, (Number(e.repairQty) || 0) + (Number(rep.qty) || 1)), maint: [...(e.maint || []), { id: 'm-' + Date.now().toString(36), type: rep.issue === 'missing' ? 'defect' : 'repair', title: (rep.issue === 'missing' ? 'Fehlt nach Rückgabe' : 'Schaden nach Rückgabe') + ' · ' + rental.tenantName, date: todayISO(), done: false, note: rep.note || '' }] } : e)); } if (rep.itemType === 'accessory' && setAccessories) { setAccessories(prev => prev.map(a => a.id === rep.itemId ? { ...a, defectQty: Math.max(0, (Number(a.defectQty) || 0) + (Number(rep.qty) || 1)), note: [a.note, (rep.issue === 'missing' ? 'Fehlt' : 'Schaden') + ' (' + rental.tenantName + '): ' + (rep.note || '')].filter(Boolean).join(' · ') } : a)); } if (toast) toast(rep.issue === 'missing' ? 'Als fehlend im Lager vermerkt' : 'Schaden im Lager vermerkt'); setReportFor(null); }; // Steps definition const HANDOVER_STEPS = ['Equipment einpacken', 'Zubehör einpacken', 'Kaution & Gebühr', 'Ausweis', 'Abschluss']; const RETURN_STEPS = ['Equipment zurück', 'Zubehör zurück', 'Kaution zurück', 'Abschluss']; const steps = isReturn ? RETURN_STEPS : HANDOVER_STEPS; const finish = () => { patch(c => { const b = isReturn ? c.ret : c.handover; b.done = true; b.doneAt = todayISO(); return c; }); if (setRentals) { setRentals(prev => prev.map(r => r.id === rental.id ? { ...r, status: isReturn ? 'abgeschlossen' : 'aktiv', depositStatus: isReturn ? 'zurueckgezahlt' : 'erhalten', paymentStatus: isReturn ? r.paymentStatus : 'bezahlt' } : r)); } if (toast) toast(isReturn ? 'Rückgabe abgeschlossen' : 'Übergabe abgeschlossen'); onClose(); }; // ── Checklist renderer ── const Checklist = ({ items, emptyLabel, withReport }) => (
{items.length === 0 &&
{emptyLabel}
} {items.length > 0 && toggleAll(!items.every(l => branch.checks[l.key]))} scale={0.98}>
{items.filter(l => branch.checks[l.key]).length}/{items.length} erledigt {items.every(l => branch.checks[l.key]) ? 'Alle abwählen' : 'Alle abhaken'}
}
{items.map(l => { const on = !!branch.checks[l.key]; const rep = (w.reports || []).find(r => r.itemType === l.type && r.itemId === l.id); return (
setCheck(l.key, !on)} scale={0.85}>
{on && }
setCheck(l.key, !on)}>
{l.name}
{l.qty}× {l.type === 'accessory' ? (l.cat || 'Zubehör') : 'Gerät'}
{withReport && setReportFor(l)} scale={0.9}>
{rep ? 'Gemeldet' : 'Melden'}
}
{rep &&
{rep.issue === 'missing' ? 'Fehlt' : 'Schaden'}{rep.qty > 1 ? ` (${rep.qty}×)` : ''}{rep.note ? ' · ' + rep.note : ''}
}
); })}
); // ── Confirm row (Kaution / Gebühr / Ausweis) ── const ConfirmRow = ({ label, sub, field, icon }) => { const on = !!branch[field]; return ( patch(c => { const b = isReturn ? c.ret : c.handover; b[field] = !b[field]; return c; })} scale={0.98}>
{on && }
{label}
{sub &&
{sub}
}
); }; // ── Step body ── const deposit = Number(rental.deposit) || 0; const total = window.rentalTotal ? window.rentalTotal(rental) : 0; let body; if (!isReturn) { if (step === 0) body = ; else if (step === 1) body = ; else if (step === 2) body = (
); else if (step === 3) body = (
{(rental.photos || []).some(p => /ausweis/i.test(p.label || '')) &&
✓ Ausweis-Fotos liegen dieser Miete bei.
}
); else body = ; } else { if (step === 0) body = ; else if (step === 1) body = ; else if (step === 2) body = ; else body = ; } const lastStep = step === steps.length - 1; // Gate advancing on the current step being satisfied const canNext = (() => { if (isReturn) { if (step === 0) return lines.eq.every(l => branch.checks[l.key]) || lines.eq.length === 0; if (step === 1) return lines.acc.every(l => branch.checks[l.key]) || lines.acc.length === 0; return true; } if (step === 0) return lines.eq.every(l => branch.checks[l.key]) || lines.eq.length === 0; if (step === 1) return lines.acc.every(l => branch.checks[l.key]) || lines.acc.length === 0; return true; })(); return (
{/* Header */}
{isReturn ? 'Rückgabe' : (rental.delivery && rental.delivery.enabled ? 'Auslieferung' : 'Übergabe')}
Schließen
{rental.tenantName}
{fmtRange(rental.start, rental.end)}
{/* Progress dots */}
{steps.map((s, i) => (
))}
{steps[step]}
{body} {/* Nav */}
{step > 0 && setStep(s => s - 1)} scale={0.97} style={{ flex: 1 }}>
Zurück
} {!lastStep && canNext && setStep(s => s + 1)} scale={canNext ? 0.97 : 1} style={{ flex: 2 }}>
Weiter
} {lastStep &&
{isReturn ? 'Rückgabe abschließen' : 'Übergabe abschließen'}
}
{/* Report sub-sheet */} setReportFor(null)} onSubmit={submitReport} t={t} /> ); } // ── Final summary step ── function WorkflowSummary({ mode, lines, branch, w, t, deposit }) { const isReturn = mode === 'return'; const done = lines.all.filter(l => branch.checks[l.key]).length; const Row = ({ label, ok }) => (
{ok && }
{label}
); return (
0 && done === lines.all.length} /> {!isReturn && } {!isReturn && } {!isReturn && } {isReturn && } {isReturn && (w.reports || []).length > 0 &&
{w.reports.length} Meldung(en) ans Lager
{w.reports.map(r =>
· {r.name}: {r.issue === 'missing' ? 'fehlt' : 'Schaden'}{r.note ? ' – ' + r.note : ''}
)}
}
{isReturn ? 'Mit „Rückgabe abschließen" wird die Miete beendet und Meldungen im Lager verbucht.' : 'Mit „Übergabe abschließen" wird die Miete aktiv gesetzt.'}
); } // ── Damage / missing report sub-sheet ── function ReportSheet({ open, line, onClose, onSubmit, t }) { const [issue, setIssue] = useStateWF('defect'); const [qty, setQty] = useStateWF(1); const [note, setNote] = useStateWF(''); useEffectWF(() => { if (open) { setIssue('defect'); setQty(1); setNote(''); } }, [open]); if (!line) return null; return (
Problem melden
Abbrechen
{line.name}
{[['defect', 'Beschädigt'], ['missing', 'Fehlt']].map(([id, label]) => { const on = issue === id; return ( setIssue(id)} scale={0.97} style={{ flex: 1 }}>
{label}
); })}
Anzahl betroffen
setQty(q => Math.max(1, q - 1))} scale={0.85}>
{qty} setQty(q => Math.min(line.qty, q + 1))} scale={0.85}>