// Posteingang — E-Mail inbox: read mail, convert rental requests, link to tenants, reply.
// Two view styles (Liste / Karten) so the look can be compared.
const { useState, useEffect } = React;
// Icons
const ICON_ENVELOPE = 'M3 6a1 1 0 011-1h16a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V6zM3 7l9 6 9-6';
const ICON_REPLY = 'M9 17l-5-5 5-5M4 12h11a4 4 0 014 4v2';
const ICON_LINK = 'M10 13a5 5 0 007 0l2-2a5 5 0 00-7-7l-1 1M14 11a5 5 0 00-7 0l-2 2a5 5 0 007 7l1-1';
const ICON_ARCHIVE = 'M3 6h18v3H3zM5 9v10a1 1 0 001 1h12a1 1 0 001-1V9M9 13h6';
const ICON_TRASH = 'M3 6h18M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6';
const ICON_BOLT = 'M13 2L3 14h8l-1 8 10-12h-8l1-8z';
const ICON_STAR = 'M12 2.5l2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 18.2 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9z';
// Map a free-text equipment hint to an actual equipment id (best effort).
function matchEquipmentId(hint, equipment) {
if (!equipment || !equipment.length) return '';
if (!hint) return equipment[0].id;
const h = hint.toLowerCase();
const direct = equipment.find(e => h.includes(e.name.toLowerCase()) || e.name.toLowerCase().includes(h));
if (direct) return direct.id;
const byKind = (re) => { const e = equipment.find(x => re.test((x.name + ' ' + x.cat + ' ' + x.kind).toLowerCase())); return e && e.id; };
if (/funk/.test(h)) return byKind(/funk|ew100|sennheiser/) || equipment[0].id;
if (/mikro|mic|gesang/.test(h)) return byKind(/sm58|mikro|shure/) || equipment[0].id;
if (/lautsprech|box|^pa\b|pa |beschall|sound|musik|anlage/.test(h)) return byKind(/jbl|lautsprech|eon/) || equipment[0].id;
if (/misch|mixer|pult/.test(h)) return byKind(/mg10|yamaha|misch/) || equipment[0].id;
if (/anh\u00e4ng|trailer|umzug|transport|kasten/.test(h)) return byKind(/anh\u00e4nger/) || equipment[0].id;
return equipment[0].id;
}
// Relative date label vs the app's fixed today.
function mailDate(dateISO, time) {
const today = todayISO();
if (dateISO === today) return time || 'Heute';
const d = fromISO(dateISO), tdy = fromISO(today);
const diff = Math.round((tdy - d) / 86400000);
if (diff === 1) return 'Gestern';
if (diff > 1 && diff < 7) return DE_DAYS[d.getDay()].slice(0, 2);
return `${String(d.getDate()).padStart(2, '0')}.${String(d.getMonth() + 1).padStart(2, '0')}.`;
}
const initials = (name) => name.split(/\s+/).filter(Boolean).slice(0, 2).map(w => w[0]).join('').toUpperCase();
// Stable-ish avatar tint from name
const AVATAR_TINTS = ['#007AFF', '#34C759', '#FF9500', '#AF52DE', '#FF2D55', '#5856D6', '#30B0C7'];
const tintFor = (s) => AVATAR_TINTS[[...s].reduce((a, c) => a + c.charCodeAt(0), 0) % AVATAR_TINTS.length];
function Avatar({ name, size = 42 }) {
const c = tintFor(name);
return (
{initials(name)}
);
}
// ─────────────────────────────────────────────────────────────
// Inbox list — two visual styles
// ─────────────────────────────────────────────────────────────
function InboxList({ emails, rentals, style, onOpen }) {
const t = useTheme();
if (emails.length === 0) {
return Keine E-Mails in diesem Filter.
;
}
// ── Apple-Mail-style list ──
if (style === 'liste') {
return (
{emails.map((m, i) => {
const linked = m.linkedRentalId && rentals.find(r => r.id === m.linkedRentalId);
return (
onOpen(m)} scale={0.99}>
{/* unread dot */}
{m.from}
{mailDate(m.dateISO, m.time)}
{m.subject}
{m.kind === 'request' && !m.linkedRentalId && (
Anfrage
)}
{linked && ✓ {linked.tenantName.split(' ')[0]}}
{m.replied && Beantwortet}
{m.starred && }
);
})}
);
}
// ── Request-focused cards ──
return (
{emails.map(m => {
const isReq = m.kind === 'request' && !m.linkedRentalId;
const linked = m.linkedRentalId && rentals.find(r => r.id === m.linkedRentalId);
return (
onOpen(m)} scale={0.98}>
{isReq && (
NEUE MIET-ANFRAGE
{m.unread && }
)}
{m.from}
{mailDate(m.dateISO, m.time)}
{m.subject}
{isReq && m.parsed ? (
{m.parsed.equipmentHint && {m.parsed.equipmentHint}}
{m.parsed.startISO && {fmtRange(m.parsed.startISO, m.parsed.endISO)}}
{m.parsed.deliveryAddress && Lieferung}
) : (
{m.body.split('\n').filter(Boolean)[0]}
)}
{linked &&
✓ Verknüpft · {linked.tenantName.split(' ')[0]}}
{m.replied &&
Beantwortet}
{isReq ? 'Bearbeiten' : 'Lesen'}
);
})}
);
}
function Tag({ children, color, soft }) {
const t = useTheme();
return (
{children}
);
}
function Chip({ children, icon }) {
const t = useTheme();
return (
{icon && {icon}}{children}
);
}
// ─────────────────────────────────────────────────────────────
// Email detail
// ─────────────────────────────────────────────────────────────
function EmailDetail({ email, equipment, rentals, onBack, onConvert, onReply, onLink, onArchive, onDelete, onToggleStar, toast }) {
const t = useTheme();
const m = email;
const isReq = m.kind === 'request';
const linked = m.linkedRentalId && rentals.find(r => r.id === m.linkedRentalId);
const eqGuess = isReq && m.parsed ? equipment.find(e => e.id === matchEquipmentId(m.parsed.equipmentHint, equipment)) : null;
return (
{/* top bar */}
onToggleStar(m)} scale={0.85}>
onArchive(m)} scale={0.85}>
onDelete(m)} scale={0.85}>
{/* subject + sender */}
{m.subject}
{fmtDateDE(m.dateISO)}{m.time ? ' · ' + m.time : ''}
{/* linked banner */}
{linked && (
Verknüpft mit {linked.tenantName} · {linked.equipmentName}
)}
{/* parsed request summary */}
{isReq && m.parsed && (
ERKANNTE ANFRAGE
{m.parsed.startISO && 1 ? 'e' : ''}`}/>}
{m.parsed.purpose && }
{m.parsed.deliveryAddress && }
{m.parsed.phone && }
)}
{/* body */}
{m.body}
{m.replied && (
Du hast auf diese E-Mail bereits geantwortet.
)}
{/* action bar */}
{isReq && !m.linkedRentalId && (
onConvert(m)} scale={0.98} style={{ marginBottom: 8 }}>
In Vermietung umwandeln
)}
onReply(m)} scale={0.97} style={{ flex: 1 }}>
Antworten
onLink(m)} scale={0.95}>
{linked ? 'Ändern' : 'Verknüpfen'}
);
}
function SummaryRow({ label, value, sub, hint }) {
const t = useTheme();
return (
{label}
{value}
{sub &&
{sub}
}
{hint &&
{hint}
}
);
}
// ─────────────────────────────────────────────────────────────
// Reply composer sheet
// ─────────────────────────────────────────────────────────────
function ReplySheet({ open, email, onClose, onSend }) {
const t = useTheme();
const [text, setText] = useState('');
useEffect(() => {
if (open && email) {
const first = email.from.split(' ')[0];
setText(`Hallo ${first},\n\nvielen Dank für Ihre Nachricht.\n\n\n\nViele Grüße\nRentFlow Manager`);
}
}, [open, email && email.id]);
if (!email) return null;
const quick = [
'Gerne — der Termin ist bei uns frei. ✅',
'Leider ist das Equipment in dem Zeitraum schon vergeben.',
'Anbei unser Angebot. Bei Fragen melden Sie sich gerne.',
];
return (
{/* quick replies */}
{quick.map((q, i) => (
setText(prev => {
const sig = '\n\nViele Grüße\nRentFlow Manager';
const base = prev.replace(sig, '');
return base.replace(/\n+$/, '') + '\n\n' + q + sig;
})} scale={0.96}>
{q.length > 28 ? q.slice(0, 26) + '…' : q}
))}
);
}
function Row({ label, value }) {
const t = useTheme();
return (
);
}
// ─────────────────────────────────────────────────────────────
// Link-to-tenant sheet
// ─────────────────────────────────────────────────────────────
function LinkSheet({ open, email, rentals, equipment, onClose, onPick, onUnlink }) {
const t = useTheme();
if (!email) return null;
// Suggest rentals whose email matches
const suggested = rentals.filter(r => r.email && email.fromEmail && r.email.toLowerCase() === email.fromEmail.toLowerCase());
const rest = rentals.filter(r => !suggested.includes(r));
const ordered = [...suggested, ...rest];
return (
Mit Mieter verknüpfen
E-Mail einem bestehenden Mietvertrag zuordnen.
Fertig
{email.linkedRentalId && (
{ onUnlink(email); onClose(); }} scale={0.98} style={{ marginTop: 14 }}>
Verknüpfung aufheben
)}
{ordered.map(r => {
const eq = equipment.find(e => e.id === r.equipmentId);
const isSug = suggested.includes(r);
const isCur = email.linkedRentalId === r.id;
const m = statusMeta(r.status);
return (
{ onPick(email, r); onClose(); }} scale={0.98}>
{r.tenantName}
{isSug &&
passt}
{eq ? eq.name : r.equipmentName} · {fmtRange(r.start, r.end)}
● {m.label}
);
})}
);
}
// ─────────────────────────────────────────────────────────────
// Main inbox screen (rendered inside the Akten "Posteingang" tab)
// ─────────────────────────────────────────────────────────────
function Posteingang({ emails, setEmails, equipment, rentals, onConvert, toast }) {
const t = useTheme();
const [viewStyle, setViewStyle] = useLocal('rf-inbox-style', 'karten'); // 'liste' | 'karten'
const [filter, setFilter] = useState('alle'); // alle | anfragen | verknuepft
const [selected, setSelected] = useState(null);
const [reply, setReply] = useState(null);
const [link, setLink] = useState(null);
const visible = emails.filter(m => !m.archived).filter(m => {
if (filter === 'anfragen') return m.kind === 'request' && !m.linkedRentalId;
if (filter === 'verknuepft') return !!m.linkedRentalId;
return true;
});
const unreadCount = emails.filter(m => !m.archived && m.unread).length;
const reqCount = emails.filter(m => !m.archived && m.kind === 'request' && !m.linkedRentalId).length;
const open = (m) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, unread: false } : x));
setSelected({ ...m, unread: false });
};
const sendReply = (m, text) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, replied: true } : x));
setSelected(s => s && s.id === m.id ? { ...s, replied: true } : s);
toast('Antwort gesendet');
};
const doLink = (m, r) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, linkedRentalId: r.id } : x));
setSelected(s => s && s.id === m.id ? { ...s, linkedRentalId: r.id } : s);
toast(`Verknüpft mit ${r.tenantName}`);
};
const doUnlink = (m) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, linkedRentalId: null } : x));
setSelected(s => s && s.id === m.id ? { ...s, linkedRentalId: null } : s);
toast('Verknüpfung aufgehoben');
};
const archive = (m) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, archived: true } : x));
setSelected(null);
toast('Archiviert');
};
const [confirmDel, setConfirmDel] = useState(null);
const del = (m) => setConfirmDel(m);
const doDelete = (m) => {
setEmails(prev => prev.filter(x => x.id !== m.id));
setConfirmDel(null);
setSelected(null);
toast('Gelöscht');
};
const toggleStar = (m) => {
setEmails(prev => prev.map(x => x.id === m.id ? { ...x, starred: !x.starred } : x));
setSelected(s => s && s.id === m.id ? { ...s, starred: !s.starred } : s);
};
// Detail view replaces the list
if (selected) {
const live = emails.find(e => e.id === selected.id) || selected;
return (
<>
setSelected(null)}
onConvert={(m) => { onConvert(m); }}
onReply={(m) => setReply(m)} onLink={(m) => setLink(m)}
onArchive={archive} onDelete={del} onToggleStar={toggleStar} toast={toast}/>
setReply(null)} onSend={sendReply}/>
setLink(null)} onPick={doLink} onUnlink={doUnlink}/>
setConfirmDel(null)} onConfirm={doDelete}/>
>
);
}
return (
{/* filter + view-style controls */}
{[['alle', 'Alle'], ['anfragen', `Anfragen${reqCount ? ' · ' + reqCount : ''}`], ['verknuepft', 'Verknüpft']].map(([id, label]) => (
setFilter(id)} scale={0.95}>
{label}
))}
{/* view-style toggle */}
{[['karten', 'M9 4h11M9 9h11M9 14h11M4 4.5v0M4 9.5v0M4 14.5v0'], ['liste', 'M4 6h16M4 12h16M4 18h16']].map(([id, d]) => (
setViewStyle(id)} scale={0.92}>
))}
setReply(null)} onSend={sendReply}/>
setLink(null)} onPick={doLink} onUnlink={doUnlink}/>
setConfirmDel(null)} onConfirm={doDelete}/>
);
}
// ────────────────────────────────────────────────────────
// Delete confirm sheet (iOS-style action sheet)
// ────────────────────────────────────────────────────────
function DeleteEmailSheet({ open, email, onClose, onConfirm }) {
const t = useTheme();
if (!open || !email) return null;
return (
e.stopPropagation()} style={{ padding: '0 8px 8px' }}>
E-Mail löschen?
„{email.subject}“ von {email.from} wird endgültig entfernt.
onConfirm(email)} scale={0.99}>
Löschen
Abbrechen
);
}
Object.assign(window, { Posteingang, matchEquipmentId });