// 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]) =>
)}
{/* ── 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 }) => (
);
// 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 }) => (
);
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"/>
{/* ── 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}>
{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 (
);
}
// ── 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.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 });