e.stopPropagation()}
style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(0,0,0,0.25)', backdropFilter: 'blur(2px)', WebkitBackdropFilter: 'blur(2px)' }}/>
);
}
function ProtocolsList({ protocols, onEdit, onNew, onDelete }) {
const t = useTheme();
return (
Tippe an, um zu bearbeiten. Lange drücken für Optionen.
Neue Vorlage
{protocols.map((tpl) =>
onEdit(tpl)}
onDelete={() => onDelete(tpl)}/>
)}
);
}
// ─── Protocol editor sheet ─────────────────────────────────
const EMOJI_OPTIONS = ['📋', '📦', '↩️', '📝', '✉️', '📅', '🧾', '🔔', '⚠️', '✅', '💼', '🔧'];
function ProtocolEditor({ open, onClose, initial, onSave }) {
const t = useTheme();
const isEdit = !!(initial && initial.id);
const blank = { id: '', name: '', icon: '📋', color: PROTOCOL_COLORS[0], desc: '', text: '' };
const [form, setForm] = useState(blank);
useEffect(() => {if (open) setForm(initial ? { ...blank, ...initial } : blank);}, [open, initial]);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const valid = form.name.trim() && form.text.trim();
const save = () => {
if (!valid) return;
const out = { ...form, id: form.id || 'p-' + Date.now() };
onSave(out);
onClose();
};
const fInp = {
width: '100%', padding: '12px 14px', borderRadius: 12,
border: `0.5px solid ${t.inputBorder}`, background: t.card,
fontSize: 15, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box'
};
return (
{isEdit ? 'Vorlage bearbeiten' : 'Neue Vorlage'}
Text + Platzhalter anpassen
Abbrechen
Name
set('name', e.target.value)} placeholder="z.B. Mahnung" style={fInp} />
Kurzbeschreibung
set('desc', e.target.value)} placeholder="z.B. Erinnerung an offene Zahlung" style={fInp} />
Icon
{EMOJI_OPTIONS.map((emo) =>
set('icon', emo)} scale={0.9}>
{emo}
)}
Farbe
{PROTOCOL_COLORS.map((c) =>
set('color', c)} scale={0.9}>
)}
Text (Platzhalter werden ersetzt)
Verfügbar: {'{tenant} {equipment} {period} {start} {end} {total} {deposit} {days} {rate} {address} {today}'}
);
}
// ─── Schnelltexte (editable WhatsApp/E-Mail templates) ───────────────────
function QuickTextsList({ quickTexts, onEdit, onNew, onDelete }) {
const t = useTheme();
const purple = t.purple || '#AF52DE';
return (
Fertige WhatsApp- & E-Mail-Texte. Platzhalter wie {'{tenant}'} werden mit Mieterdaten ausgefüllt.
Neuer Schnelltext
{quickTexts.map((qt) =>
onEdit(qt)}
onEdit={() => onEdit(qt)}
onDelete={() => onDelete(qt)}>
{qt.icon && {qt.icon} }
{qt.name}
{qt.desc}
{qt.kind && (
{qt.kind === 'email' ? 'E-MAIL' : 'WHATSAPP'}
)}
)}
{quickTexts.length === 0 &&
Noch keine Schnelltexte. Tippe oben auf Neuer Schnelltext .
}
);
}
function QuickTextEditor({ open, onClose, initial, onSave }) {
const t = useTheme();
const purple = t.purple || '#AF52DE';
const isEdit = !!(initial && initial.id);
const blank = { id: '', name: '', desc: '', icon: '💬', kind: 'whatsapp', text: '' };
const [form, setForm] = useState(blank);
useEffect(() => { if (open) setForm(initial ? { ...blank, ...initial } : blank); }, [open, initial]);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const valid = form.name.trim() && form.text.trim();
const save = () => {
if (!valid) return;
const id = form.id || 'qt-' + Date.now();
onSave({ ...form, id, name: form.name.trim(), desc: form.desc.trim() });
onClose();
};
const fInp = {
width: '100%', padding: '12px 14px', borderRadius: 12,
border: `0.5px solid ${t.inputBorder}`, background: t.card,
fontSize: 15, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box'
};
const KIND_OPTIONS = [
{ id: 'whatsapp', label: 'WhatsApp', icon: '💬' },
{ id: 'email', label: 'E-Mail', icon: '✉️' },
];
const QT_EMOJI = ['💬', '✉️', '⏰', '📨', '📲', '🔔', '✅', '📝', '🙌', '🎉'];
return (
{isEdit ? 'Schnelltext bearbeiten' : 'Neuer Schnelltext'}
Text + Platzhalter anpassen
Abbrechen
Name
set('name', e.target.value)} placeholder="z.B. Reservierungsbestätigung" style={fInp} />
Kurzbeschreibung
set('desc', e.target.value)} placeholder="z.B. Kurze Bestätigung per WhatsApp" style={fInp} />
Versandweg
{KIND_OPTIONS.map((opt) => {
const on = form.kind === opt.id;
return (
set('kind', opt.id)} scale={0.97}>
{opt.icon}
{opt.label}
);
})}
Icon
set('icon', '')} scale={0.9}>
{QT_EMOJI.map((emo) =>
set('icon', emo)} scale={0.9}>
{emo}
)}
Text (Platzhalter werden ersetzt)
Verfügbar: {'{tenant} {equipment} {period} {start} {end} {total} {deposit} {days} {rate} {address} {today}'}
);
}
// ─── Doc preview — editable, copy, share ────────────────────
function DocPreview({ doc, onBack, onUpdate, company, toast }) {
const t = useTheme();
const isInvoice = doc.kind === 'invoice';
const [editing, setEditing] = useState(false);
const [tmp, setTmp] = useState(doc.text);
useEffect(() => {setTmp(doc.text);setEditing(false);}, [doc.id]);
const save = () => {onUpdate({ ...doc, text: tmp });setEditing(false);toast('Gespeichert');};
// Clean PDF render: invoices use structured data; protocols render their (edited) text as body.
const isProtocol = !isInvoice;
const wantsSign = isProtocol && /unterschrift|übergabe|protokoll|übernahme/i.test((doc.title || '') + ' ' + (tmp || ''));
const renderDoc = isInvoice ?
doc :
{ ...doc, bodyText: tmp, signature: wantsSign };
const cleanHTML = buildDocHTML(company, renderDoc);
const savePDF = () => {printDocHTML(buildDocHTML(company, renderDoc));toast('Drucken-Dialog → „Als PDF sichern“');};
const copy = async () => {
try {
await navigator.clipboard.writeText(tmp);
toast('In Zwischenablage kopiert');
} catch {
const ta = document.createElement('textarea');
ta.value = tmp;document.body.appendChild(ta);ta.select();
try {document.execCommand('copy');toast('Kopiert!');} catch {toast('Kopieren nicht möglich');} finally
{document.body.removeChild(ta);}
}
};
return (
{doc.title}
editing ? save() : setEditing(true)} scale={0.88}>
{editing ?
Fertig :
}
{editing ?
:
}
{editing ? '✎ Text wird gespeichert, sobald du „Fertig“ tippst.' :
isInvoice ? '💡 Logo, Firmendaten & Vorlage kommen aus dem Firmenprofil (⚙️). „Als PDF“ → Drucken-Dialog → „Als PDF sichern“.' :
'💡 Tippe ✎ zum Anpassen. „Als PDF“ erzeugt das saubere Dokument mit deinem Logo.'}
);
}
// ─── Main Dokumente screen ─────────────────────────────────
function ScreenDokumente({ equipment, setEquipment, rentals, setRentals, protocols, setProtocols, quickTexts = [], setQuickTexts, genDocs, setGenDocs, emails, setEmails, company, setCompany, blockedCustomers = [], setBlockedCustomers, logistik, setLogistik, dark, toggleDark, events = [], focusRentalId, initialTab, openNewRental, origin, go, toast }) {
const t = useTheme();
const [tab, setTab] = useState(initialTab || 'index');
const [search, setSearch] = useState('');
const [selectedRental, setSelectedRental] = useState(null);
const [previewDoc, setPreviewDoc] = useState(null);
const [newRentalOpen, setNewRentalOpen] = useState(false);
const [newRentalPrefill, setNewRentalPrefill] = useState(null);
const [pendingEmailId, setPendingEmailId] = useState(null);
const [protEdit, setProtEdit] = useState({ open: false, initial: null });
const [confirmProt, setConfirmProt] = useState(null);
const [qtEdit, setQtEdit] = useState({ open: false, initial: null });
const [confirmQt, setConfirmQt] = useState(null);
const [profileOpen, setProfileOpen] = useState(false);
// Jump straight to a rental when navigated here from search / home / reminders
useEffect(() => {
if (focusRentalId) {
const r = rentals.find((x) => x.id === focusRentalId);
if (r) setSelectedRental(r);
}
}, [focusRentalId]);
// Auto-open the new-rental sheet when navigated here with newRental=true
useEffect(() => {
if (openNewRental) {
setNewRentalPrefill(null);
setPendingEmailId(null);
setNewRentalOpen(true);
setTab('rentals');
}
}, [openNewRental]);
const onGenerated = (doc) => {
const rid = doc.rentalId;
if (!rid) return;
setGenDocs((prev) => ({ ...prev, [rid]: [doc, ...(prev[rid] || [])] }));
};
const updateRental = (r) => {
setRentals((prev) => prev.map((p) => p.id === r.id ? r : p));
setSelectedRental(r);
};
const addRental = (r) => {
setRentals((prev) => [r, ...prev]);
// Auto-increment uses on each equipment item attached to this rental
if (setEquipment) {
const its = r.items && r.items.length ? r.items : r.equipmentId ? [{ equipmentId: r.equipmentId }] : [];
if (its.length) {
const ids = new Set(its.map((it) => it.equipmentId));
setEquipment((prev) => prev.map((e) => ids.has(e.id) ? { ...e, uses: (Number(e.uses) || 0) + 1 } : e));
}
}
// link the originating email, if this rental came from a Posteingang request
if (pendingEmailId && setEmails) {
setEmails((prev) => prev.map((m) => m.id === pendingEmailId ? { ...m, linkedRentalId: r.id, unread: false } : m));
setPendingEmailId(null);
}
setNewRentalPrefill(null);
toast(`„${r.tenantName}“ angelegt`);
setSelectedRental(r);
};
// Convert a Posteingang request email into a prefilled new-rental sheet
const convertEmailToRental = (m) => {
const p = m.parsed || {};
const prefill = {
tenantName: m.from,
email: m.fromEmail,
phone: p.phone || '',
address: p.deliveryAddress || '',
equipmentId: matchEquipmentId(p.equipmentHint, equipment),
purpose: p.purpose || '',
start: p.startISO || todayISO(),
end: p.endISO || p.startISO || todayISO(),
status: 'reserviert'
};
setNewRentalPrefill(prefill);
setPendingEmailId(m.id);
setNewRentalOpen(true);
};
const deleteRental = (r) => {
setRentals((prev) => prev.filter((p) => p.id !== r.id));
setGenDocs((prev) => {const c = { ...prev };delete c[r.id];return c;});
setSelectedRental(null);
toast(`„${r.tenantName}“ gelöscht`);
};
const updateDoc = (d) => {
setGenDocs((prev) => {
const list = prev[d.rentalId] || [];
return { ...prev, [d.rentalId]: list.map((x) => x.id === d.id ? d : x) };
});
setPreviewDoc(d);
};
// Protocols
const saveProtocol = (p) => {
setProtocols((prev) => {
const exists = prev.find((x) => x.id === p.id);
if (exists) return prev.map((x) => x.id === p.id ? p : x);
return [...prev, p];
});
toast(protEdit.initial ? 'Vorlage gespeichert' : `„${p.name}“ angelegt`);
};
const doDeleteProt = () => {
setProtocols((prev) => prev.filter((p) => p.id !== confirmProt.id));
toast(`„${confirmProt.name}“ gelöscht`);
setConfirmProt(null);
};
// Quick texts (Schnelltexte)
const saveQuickText = (q) => {
setQuickTexts && setQuickTexts((prev) => {
const exists = prev.find((x) => x.id === q.id);
if (exists) return prev.map((x) => x.id === q.id ? q : x);
return [...prev, q];
});
toast(qtEdit.initial ? 'Schnelltext gespeichert' : `„${q.name}“ angelegt`);
};
const doDeleteQt = () => {
setQuickTexts && setQuickTexts((prev) => prev.filter((p) => p.id !== confirmQt.id));
toast(`„${confirmQt.name}“ gelöscht`);
setConfirmQt(null);
};
// Delete generated doc (Rechnung / Protokoll / Schnelltext) from a rental
const deleteDoc = (d) => {
setGenDocs((prev) => {
const list = prev[d.rentalId] || [];
return { ...prev, [d.rentalId]: list.filter((x) => x.id !== d.id) };
});
};
// Layered routing
if (previewDoc) return
setPreviewDoc(null)} onUpdate={updateDoc} company={company} toast={toast} />;
if (selectedRental) {
const live = rentals.find((r) => r.id === selectedRental.id) || selectedRental;
return (
{if (origin === 'cal' && go) {go('cal');} else {setSelectedRental(null);}}}
equipment={equipment} protocols={protocols} quickTexts={quickTexts}
allRentals={rentals} onSelectRental={(r) => setSelectedRental(r)}
blockedCustomers={blockedCustomers} setBlockedCustomers={setBlockedCustomers}
onPreview={setPreviewDoc} onGenerated={onGenerated} onDeleteDoc={deleteDoc}
generatedDocs={genDocs[live.id] || []} toast={toast} />);
}
// ─── Grid landing ───
const invoiceCount = Object.values(genDocs || {}).flat().filter((d) => d.kind === 'invoice').length;
const unreadMail = (emails || []).filter((m) => !m.archived && m.unread).length;
const openRentals = rentals.filter((r) => r.status !== 'abgeschlossen').length;
const totalDocs = rentals.length + invoiceCount + protocols.length + (emails || []).filter((m) => !m.archived).length;
const CATEGORIES = [
{ id: 'rentals', label: 'Mietaufträge', count: rentals.length,
icon: 'M6 2h8l6 6v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4a2 2 0 012-2zM14 2v6h6M9 14h6M9 18h4',
hue: t.accent, tint: t.accentSoft },
{ id: 'protocols', label: 'Übergabe-Protokolle', count: protocols.length,
icon: 'M6 2h8l6 6v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4a2 2 0 012-2zM14 2v6h6M9 13l1.8 1.8L14 11M9 18h4',
hue: t.green, tint: t.greenSoft },
{ id: 'quicktexts', label: 'Schnelltexte', count: quickTexts.length,
icon: 'M3 7l9 6 9-6M3 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z',
hue: t.purple || '#AF52DE', tint: t.dark ? 'rgba(175,82,222,0.18)' : 'rgba(175,82,222,0.12)' },
{ id: 'invoices', label: 'Rechnungen', count: invoiceCount,
icon: 'M6 2h8l6 6v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4a2 2 0 012-2zM14 2v6h6M12 11v7M9 14h6',
hue: t.orange, tint: t.orangeSoft },
{ id: 'mail', label: 'Posteingang', count: (emails || []).filter((m) => !m.archived).length, badge: unreadMail,
icon: 'M3 7l9 6 9-6M3 7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V7z',
hue: t.purple || '#AF52DE', tint: t.dark ? 'rgba(175,82,222,0.18)' : 'rgba(175,82,222,0.12)' }];
const filteredCats = search.trim() ?
CATEGORIES.filter((c) => c.label.toLowerCase().includes(search.trim().toLowerCase())) :
CATEGORIES;
// Full-text search across rentals, invoices, protocols, emails
const searchResults = (() => {
const q = search.trim().toLowerCase();
if (!q) return [];
const r = [];
rentals.forEach((rt) => {
const txt = [rt.tenantName, rt.email, rt.phone, rt.equipmentName, rt.purpose, rentalEquipmentLabel(rt)].filter(Boolean).join(' ').toLowerCase();
if (txt.includes(q)) r.push({ kind: 'rental', id: rt.id, title: rt.tenantName, sub: (rentalEquipmentLabel(rt) || rt.equipmentName) + ' · ' + fmtRange(rt.start, rt.end), data: rt });
});
Object.values(genDocs || {}).flat().forEach((d) => {
const txt = [d.title, d.tenantName, d.number, d.text].filter(Boolean).join(' ').toLowerCase();
if (txt.includes(q)) r.push({ kind: 'doc', id: d.id, title: d.title, sub: (d.kind === 'invoice' ? 'Rechnung' : 'Protokoll') + ' · ' + (d.date || ''), data: d });
});
protocols.forEach((pr) => {
const txt = [pr.name, pr.text].filter(Boolean).join(' ').toLowerCase();
if (txt.includes(q)) r.push({ kind: 'protocol', id: pr.id, title: pr.name, sub: 'Vorlage', data: pr });
});
(emails || []).forEach((em) => {
const txt = [em.from, em.subject, em.body, em.fromEmail].filter(Boolean).join(' ').toLowerCase();
if (txt.includes(q)) r.push({ kind: 'email', id: em.id, title: em.subject || em.from, sub: 'E-Mail · ' + em.from, data: em });
});
return r.slice(0, 30);
})();
// ─── Landing (grid) view ───
if (tab === 'index') {
return (
{totalDocs} Dateien · {openRentals} Verträge offen
Dokumente
{/* Search */}
{/* Category grid 2×2 */}
{filteredCats.map((cat) =>
setTab(cat.id)} scale={0.97}>
{cat.badge > 0 &&
{cat.badge}
}
{cat.label}
{cat.count} {cat.count === 1 ? 'Datei' : 'Dateien'}
)}
{filteredCats.length === 0 &&
Keine Treffer für „{search}".
}
{/* Full-text search results */}
{search.trim() && searchResults.length > 0 &&
<>
{searchResults.length} {searchResults.length === 1 ? 'Treffer' : 'Treffer'}
{searchResults.map((r, i) => {
const onTap = () => {
if (r.kind === 'rental') {setSelectedRental(r.data);} else
if (r.kind === 'doc') {setPreviewDoc(r.data);} else
if (r.kind === 'protocol') {setTab('protocols');} else
if (r.kind === 'email') {setTab('mail');}
};
const cat = CATEGORIES.find((c) =>
c.id === 'rentals' && r.kind === 'rental' ||
c.id === 'invoices' && r.kind === 'doc' && r.data.kind === 'invoice' ||
c.id === 'protocols' && (r.kind === 'protocol' || r.kind === 'doc' && r.data.kind !== 'invoice') ||
c.id === 'mail' && r.kind === 'email'
) || CATEGORIES[0];
return (
);
})}
>
}
{search.trim() && searchResults.length === 0 && filteredCats.length === 0 &&
Keine Dokumente gefunden.
}
setProfileOpen(false)}
company={company} setCompany={setCompany} toast={toast}
logistik={logistik} setLogistik={setLogistik}
dark={dark} toggleDark={toggleDark} />
);
}
const currentCat = CATEGORIES.find((c) => c.id === tab);
return (
setTab('index')} scale={0.88}>
Dokumente
{/* Section header — title only (no tabs) */}
{currentCat &&
}
{currentCat ? currentCat.count + ' Dateien' : ''}
{currentCat ? currentCat.label : 'Akten'}
{tab === 'rentals' &&
{setNewRentalPrefill(null);setPendingEmailId(null);setNewRentalOpen(true);}} />}
{tab === 'invoices' &&
}
{tab === 'protocols' &&
setProtEdit({ open: true, initial: null })}
onEdit={(p) => setProtEdit({ open: true, initial: p })}
onDelete={(p) => setConfirmProt(p)} />
}
{tab === 'quicktexts' &&
setQtEdit({ open: true, initial: null })}
onEdit={(q) => setQtEdit({ open: true, initial: q })}
onDelete={(q) => setConfirmQt(q)} />
}
{tab === 'mail' &&
}
setNewRentalOpen(false)}
onSave={addRental} equipment={equipment} rentals={rentals} events={events} prefill={newRentalPrefill}
logistik={logistik} company={company} blockedCustomers={blockedCustomers} toast={toast} />
setProtEdit({ open: false, initial: null })}
initial={protEdit.initial} onSave={saveProtocol} />
setQtEdit({ open: false, initial: null })}
initial={qtEdit.initial} onSave={saveQuickText} />
setConfirmProt(null)} onConfirm={doDeleteProt} />
setConfirmQt(null)} onConfirm={doDeleteQt} />
setProfileOpen(false)}
company={company} setCompany={setCompany} toast={toast}
logistik={logistik} setLogistik={setLogistik}
dark={dark} toggleDark={toggleDark} />
);
}
Object.assign(window, { ScreenDokumente });