// Desktop Equipment — grid of cards + detail pane + add/edit dialog. const EQ_GLYPHS = ['speaker', 'mic', 'mixer', 'trailer', 'light', 'cable', 'case']; const MAINT_TYPES = ['service', 'repair', 'defect', 'tuev']; const maintMeta = (ty) => ({ service: { icon: '🧰', label: 'Wartung', color: '#34C759' }, repair: { icon: '🔧', label: 'Reparatur', color: '#FF9500' }, defect: { icon: '⚠️', label: 'Defekt', color: '#FF3B30' }, tuev: { icon: '📋', label: 'TÜV / HU', color: '#5856D6' } })[ty] || { icon: '•', label: ty, color: '#8E8E93' }; // ── Add / Edit equipment ── function EquipmentSheet({ open, onClose, initial, categories, rentals = [], onSave }) { const t = useTheme(); const isEdit = !!(initial && initial.id); const blank = { id: '', name: '', cat: '', sub: '', kind: categories[0] || 'Tontechnik', repairQty: 0, price: 0, qty: 1, ic: 'speaker', emoji: '', photo: '', accessories: [], maint: [] }; const [form, setForm] = useState(blank); const [accInput, setAccInput] = useState(''); const fileRef = useRef(null); useEffect(() => {if (open) {setForm(initial ? { ...blank, ...initial } : blank);setAccInput('');}}, [open, initial]); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const valid = form.name.trim() && form.cat.trim(); const addAcc = () => {const v = accInput.trim();if (!v) return;set('accessories', [...(form.accessories || []), v]);setAccInput('');}; const onPickPhoto = (ev) => { const file = ev.target.files && ev.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => setForm((f) => ({ ...f, photo: reader.result })); reader.readAsDataURL(file); ev.target.value = ''; }; // Live availability tiles (uses current form qty + repairQty + live rentals for active count) const liveTotal = Math.max(1, Number(form.qty) || 1); const liveRepair = Math.max(0, Math.min(liveTotal, Number(form.repairQty) || 0)); const liveActive = isEdit ? rentals.filter((r) => r && r.status === 'aktiv' && (r.equipmentId === form.id || (Array.isArray(r.items) && r.items.some((it) => it && it.equipmentId === form.id)))).reduce((s, r) => s + (Number(r.quantity) || 1), 0) : 0; const liveAvail = Math.max(0, liveTotal - liveActive - liveRepair); const save = () => {if (!valid) return;onSave({ ...form, id: form.id || 'eq-' + Date.now(), price: Number(form.price) || 0, qty: liveTotal, repairQty: liveRepair });onClose();}; return (
{isEdit ? 'Equipment bearbeiten' : 'Neues Equipment'}
{isEdit ? 'Felder anpassen' : 'Lege ein neues Mietobjekt an'}
Abbrechen
set('name', e.target.value)} placeholder="z.B. JBL PRX-815" /> set('cat', e.target.value)} placeholder="z.B. Aktivlautsprecher" />
set('sub', e.target.value)} placeholder="z.B. 1.300 W · 12″ · Bluetooth" />
{categories.map((k) => set('kind', k)} scale={0.95}>
{k}
)}
{/* ── Bild / Symbol ── */}
fileRef.current && fileRef.current.click()} scale={0.98} style={{ flex: 1 }}>
{form.photo ? 'Foto ersetzen' : 'Eigenes Foto hochladen'}
{form.photo && set('photo', '')} scale={0.94}>
}
Eigenes Bild hochladen — oder unten ein Symbol/Emoji wählen.
{EQ_GLYPHS.map((g) => {const sel = !form.photo && !form.emoji && form.ic === g;return ( set('ic', g)} scale={0.9}>
); })}
{form.emoji || ?}
set('emoji', e.target.value.slice(0, 4))} placeholder="Emoji eingeben …" />
set('price', e.target.value)} /> set('qty', e.target.value)} />
{/* ── Verfügbarkeit (live) ── */}
{[['Verfügbar', liveAvail, liveAvail > 0 ? t.green : t.textTer], ['Vermietet', liveActive, liveActive > 0 ? t.accent : t.textTer], ['In Reparatur', liveRepair, liveRepair > 0 ? t.orange : t.textTer]].map(([l, v, c]) =>
{v}
{l}
)}
{/* ── Repair stepper ── */}
Für Reparatur / Wartung rausnehmen
Reduziert automatisch die verfügbare Menge.
set('repairQty', Math.max(0, liveRepair - 1))} scale={0.9}>
{liveRepair}
set('repairQty', Math.min(liveTotal, liveRepair + 1))} scale={0.9}>
= liveTotal ? 0.4 : 1 }}>
setAccInput(e.target.value)} onKeyDown={(e) => {if (e.key === 'Enter') {e.preventDefault();addAcc();}}} placeholder="z.B. Stromkabel 5m" />
{form.accessories && form.accessories.length > 0 &&
{form.accessories.map((a, i) => {a} set('accessories', form.accessories.filter((_, j) => j !== i))} scale={0.8}> )}
}
{isEdit ? 'Speichern' : 'Equipment anlegen'}
); } // ── Detail pane ── function EquipmentDetail({ item, rentals, equipment, categories, onEdit, onDelete, onAddMaint, onToggleMaint, onRemoveMaint, onUpdateRepair, go }) { const t = useTheme(); const toast = useToast(); const e = item; const util = eqUtil(e, rentals, equipment); const related = rentals.filter((r) => r.equipmentId === e.id || (Array.isArray(r.items) && r.items.some((it) => it && it.equipmentId === e.id))).sort((a, b) => b.start.localeCompare(a.start)); const uses = related.length; const earned = related.filter((r) => r.paymentStatus === 'bezahlt').reduce((s, r) => s + rentalTotal(r), 0); const total = Math.max(1, Number(e.qty) || 1); const repairQty = Math.max(0, Math.min(total, Number(e.repairQty) || 0)); const activeQty = rentals.filter((r) => r && r.status === 'aktiv' && (r.equipmentId === e.id || (Array.isArray(r.items) && r.items.some((it) => it && it.equipmentId === e.id)))).reduce((s, r) => s + (Number(r.quantity) || 1), 0); const availQty = Math.max(0, total - activeQty - repairQty); const [maintOpen, setMaintOpen] = useState(false); const [draft, setDraft] = useState({ type: 'service', title: '', date: todayISO(), note: '' }); // ── small atoms ── const Section = ({ label, action, children, top = 22 }) => (
{label}
{action}
{children}
); // KPI tile (compact) const KPI = ({ label, value, color, sub }) => (
{label}
{value}
{sub &&
{sub}
}
); // Info row with icon chip (matches rentals detail pattern) const InfoRow = ({ icon, label, value, isLast }) => (
{label}
{value || }
); return (
{/* ── Header ── */}
{e.name}
{e.cat}{e.sub ? ' · ' + e.sub : ''}
{e.kind} {e.price} €/Tag
{/* ── KPI strip — 4 compact tiles incl. embedded Buchungsanteil ── */}
0 ? t.green : t.textTer} sub={`von ${total}`}/>
0 ? t.accent : t.textTer} sub="aktiv"/>
0 ? t.orange : t.textTer} sub="rausgenommen"/>
Auslastung
{util}
%
{/* ── Details card ── */}
{/* ── Reparatur Stepper (compact inline) ── */}
Für Reparatur rausnehmen
Reduziert die verfügbare Menge automatisch.
onUpdateRepair(Math.max(0, repairQty - 1))} scale={0.9}>
{repairQty}
onUpdateRepair(Math.min(total, repairQty + 1))} scale={0.9}>
= total ? 0.4 : 1 }}>
{/* ── Wartung · TÜV · Defekte ── */}
setMaintOpen((v) => !v)}>Eintrag}> {maintOpen &&
{MAINT_TYPES.map((ty) => { const mm = maintMeta(ty); const sel = draft.type === ty; return ( setDraft((d) => ({ ...d, type: ty }))} scale={0.94}>
{mm.icon} {mm.label}
); })}
setDraft((d) => ({ ...d, title: ev.target.value }))} placeholder="z.B. Nächster TÜV / HU" />
setDraft((d) => ({ ...d, date: ev.target.value }))} /> setDraft((d) => ({ ...d, note: ev.target.value }))} placeholder="Notiz" />
}
{(!e.maint || e.maint.length === 0) && !maintOpen &&
Keine Einträge.
} {(e.maint || []).map((m) => { const mm = maintMeta(m.type); return (
onToggleMaint(m.id)} scale={0.85}>
{m.done && }
{mm.icon} {m.title}
{mm.label} · {fmtDateDE(m.date)}{m.note ? ' · ' + m.note : ''}
onRemoveMaint(m.id)} scale={0.8}>
); })}
{/* ── Zubehör ── */} {e.accessories && e.accessories.length > 0 &&
{e.accessories.map((a, i) => {a} )}
} {/* ── Mietverlauf ── */} {related.length > 0 &&
{related.map((r, i) => go('rentals', { rentalId: r.id })} scale={0.998} hoverBg={t.cardAlt}>
{r.tenantName}
{fmtRange(r.start, r.end)}
{rentalTotal(r)} €
)}
}
); } // ── Editable kind chip ── function EditableKindChip({ name, onRename, onDelete }) { const t = useTheme(); const [val, setVal] = useState(name); const [focus, setFocus] = useState(false); useEffect(() => {setVal(name);}, [name]); const inputRef = useRef(null); const commit = () => {if (val.trim() && val !== name) onRename(val.trim());else setVal(name);}; return (
setVal(e.target.value)} onFocus={() => setFocus(true)} onBlur={() => {setFocus(false);commit();}} onKeyDown={(e) => {if (e.key === 'Enter') {commit();inputRef.current && inputRef.current.blur();}if (e.key === 'Escape') {setVal(name);inputRef.current && inputRef.current.blur();}}} style={{ background: 'transparent', border: 'none', outline: 'none', fontSize: 12.5, fontWeight: 600, color: t.text, width: Math.max(40, val.length * 7.5 + 6), padding: '4px 0' }} />
); } // ── Screen ── function ScreenEquipment({ go, nav, equipment, setEquipment, categories, setCategories, rentals }) { const t = useTheme(); const toast = useToast(); const [query, setQuery] = useState(''); const [kind, setKind] = useState('alle'); const [selId, setSelId] = useState(equipment[0] ? equipment[0].id : null); const [sheet, setSheet] = useState(null); const [confirmDel, setConfirmDel] = useState(null); const [editKinds, setEditKinds] = useState(false); const [addingKind, setAddingKind] = useState(false); const [newKindName, setNewKindName] = useState(''); const [analyseOpen, setAnalyseOpen] = useState(false); const renameCategory = (oldName, newName) => { const v = (newName || '').trim(); if (!v || v === oldName) return; if (categories.includes(v)) {toast('Typ existiert bereits');return;} setCategories((prev) => prev.map((c) => c === oldName ? v : c)); setEquipment((prev) => prev.map((e) => e.kind === oldName ? { ...e, kind: v } : e)); if (kind === oldName) setKind(v); toast('Umbenannt'); }; const deleteCategory = (name) => { const inUse = equipment.filter((e) => e.kind === name).length; if (inUse > 0) {toast(`„${name}" wird von ${inUse} Objekt(en) genutzt`);return;} setCategories((prev) => prev.filter((c) => c !== name)); if (kind === name) setKind('alle'); toast('Typ entfernt'); }; const addCategory = () => { const v = newKindName.trim(); if (!v) {setAddingKind(false);return;} if (categories.includes(v)) {toast('Typ existiert bereits');return;} setCategories((prev) => [...prev, v]); setNewKindName(''); setAddingKind(false); toast('Typ hinzugefügt'); }; useEffect(() => {if (nav && nav.equipmentId) setSelId(nav.equipmentId);if (nav && nav.newEquipment) setSheet({});}, [nav]); const filtered = equipment. filter((e) => kind === 'alle' || e.kind === kind). filter((e) => !query || (e.name + ' ' + e.cat + ' ' + e.sub).toLowerCase().includes(query.toLowerCase())); const selected = equipment.find((e) => e.id === selId); const saveEq = (e) => {setEquipment((prev) => {const ex = prev.find((x) => x.id === e.id);return ex ? prev.map((x) => x.id === e.id ? e : x) : [...prev, e];});setSelId(e.id);toast(equipment.find((x) => x.id === e.id) ? 'Gespeichert' : 'Equipment angelegt');}; const del = (e) => {setEquipment((prev) => prev.filter((x) => x.id !== e.id));if (selId === e.id) setSelId(null);toast('Gelöscht');}; const patchMaint = (fn) => setEquipment((prev) => prev.map((x) => x.id === selId ? { ...x, maint: fn(x.maint || []) } : x)); return ( <>
{/* grid */}
setKind('alle')} scale={0.95}>
Alle
{categories.map((k) => editKinds ? renameCategory(k, v)} onDelete={() => deleteCategory(k)} /> : setKind(k)} scale={0.95}>
{k}
)} {editKinds && (addingKind ?
setNewKindName(e.target.value)} onKeyDown={(e) => {if (e.key === 'Enter') addCategory();if (e.key === 'Escape') {setAddingKind(false);setNewKindName('');}}} onBlur={addCategory} placeholder="Neuer Typ" style={{ background: 'transparent', border: 'none', outline: 'none', fontSize: 12.5, fontWeight: 600, color: t.text, width: 90, padding: '4px 0' }} />
: setAddingKind(true)} scale={0.94}>
Typ
)}
{setEditKinds((v) => !v);setAddingKind(false);setNewKindName('');}} scale={0.94}>
{filtered.map((e) => { const st = eqStock(e, rentals); const sel = e.id === selId; return ( setSelId(e.id)} scale={0.997}>
{e.name}
{e.cat}
{e.price} €/Tag
{st.verfuegbar}/{st.total} verfügbar
); })}
{filtered.length === 0 &&
Keine Treffer.
}
{/* detail */}
{selected ? setSheet(selected)} onDelete={() => setConfirmDel(selected)} onUpdateRepair={(n) => setEquipment((prev) => prev.map((x) => x.id === selected.id ? { ...x, repairQty: n } : x))} onAddMaint={(m) => patchMaint((list) => [m, ...list])} onToggleMaint={(id) => patchMaint((list) => list.map((m) => m.id === id ? { ...m, done: !m.done } : m))} onRemoveMaint={(id) => patchMaint((list) => list.filter((m) => m.id !== id))} /> : setSheet({})} /> }
setSheet(null)} onSave={saveEq} /> setAnalyseOpen(false)} equipment={equipment} rentals={rentals} go={go} /> del(confirmDel)} onClose={() => setConfirmDel(null)} /> ); } Object.assign(window, { ScreenEquipment, EquipmentSheet, EquipmentDetail, EditableKindChip, EQ_GLYPHS, MAINT_TYPES, maintMeta });