// Desktop Kalender — month grid w/ spanning multi-day bars + Plan (Belegungs-
// plan: equipment × week timeline) + side-panel day timeline mirroring mobile.
const mondayOfISO = (iso) => {
const d = fromISO(iso);
const dow = (d.getDay() + 6) % 7; // 0 = Mon
d.setDate(d.getDate() - dow);
return fmtDateISO(d);
};
const shiftISO = (iso, days) => {const d = fromISO(iso);d.setDate(d.getDate() + days);return fmtDateISO(d);};
const parseTime = (s) => {if (!s || !/^\d{1,2}:\d{2}$/.test(s)) return null;const [h, m] = s.split(':').map(Number);return h + m / 60;};
const fmtHr = (hr) => {const h = Math.floor(hr),m = Math.round((hr - h) * 60);return String(h).padStart(2, '0') + ':' + (m === 60 ? '00' : String(m).padStart(2, '0'));};
// Tint any CSS color (#rgb, #rrggbb, rgb(), rgba()) to rgba with the given alpha.
// Hex-suffix concat (`color + '1a'`) breaks if color isn't a 6-char hex — this is safe.
function tintColor(c, a) {
if (!c) return `rgba(120,120,128,${a})`;
if (c[0] === '#') {
let h = c.slice(1);
if (h.length === 3) h = h.split('').map((x) => x + x).join('');
if (h.length === 6) {
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r},${g},${b},${a})`;
}
}
const m = c.match(/rgba?\(\s*(\d+)[ ,]+(\d+)[ ,]+(\d+)/i);
if (m) return `rgba(${m[1]},${m[2]},${m[3]},${a})`;
return c; // unknown format — return as-is
}
// Lane-pack a set of {start,end} intervals so they don't overlap in a row.
function assignLanes(items) {
const sorted = [...items].sort((a, b) => a.start.localeCompare(b.start) || a.end.localeCompare(b.end));
const laneEnds = []; // last 'end' of each lane (exclusive: a new item must start AFTER this)
sorted.forEach((it) => {
let lane = laneEnds.findIndex((end) => it.start > end);
if (lane === -1) {lane = laneEnds.length;laneEnds.push(it.end);} else
laneEnds[lane] = it.end;
it.lane = lane;
});
return { sorted, laneCount: Math.max(1, laneEnds.length) };
}
function ScreenCalendar({ go, equipment, rentals, setRentals, events, setEvents }) {
const t = useTheme();
const toast = useToast();
const today = todayISO();
const [mode, setMode] = useState('month'); // 'month' | 'plan'
const [labelMode, setLabelMode] = useLocal('rf-d-cal-labelmode', 'name'); // 'name' | 'equipment' (month view bar text)
const [cursor, setCursor] = useState(() => {const d = fromISO(today);return { y: d.getFullYear(), m: d.getMonth() };});
const [weekStart, setWeekStart] = useState(() => mondayOfISO(today));
const [selDay, setSelDay] = useState(today);
const [evtSheet, setEvtSheet] = useState(null);
const [peek, setPeek] = useState(null); // { kind, ref }
const allBars = [
...rentals.map((r) => {
// Equipment label for the alternative month-view bar text
const eq = equipment.find((e) => e.id === r.equipmentId);
const items = Array.isArray(r.items) && r.items.length ? r.items : null;
const eqName = items ?
(items[0].equipmentName || eq && eq.name || r.equipmentName) + (items.length > 1 ? ' +' + (items.length - 1) : '') :
eq && eq.name || r.equipmentName || '';
const eqTimes = (r.startTime ? '\u2191' + r.startTime : '') + (r.startTime && r.endTime ? ' ' : '') + (r.endTime ? '\u2193' + r.endTime : '');
return { kind: 'rental', id: r.id, title: r.tenantName, eqName, eqTimes, start: r.start, end: r.end, startTime: r.startTime, endTime: r.endTime, color: r.color || statusMeta(r.status).color, equipmentId: r.equipmentId, ref: r };
}),
...events.map((e) => ({ kind: 'event', id: e.id, title: e.title, eqName: e.title, eqTimes: '', start: e.start, end: e.end, startTime: e.startTime, endTime: e.endTime, color: e.color, equipmentId: e.equipmentId, ref: e }))];
const move = (d) => {
if (mode === 'month') setCursor((c) => {let m = c.m + d,y = c.y;if (m < 0) {m = 11;y--;}if (m > 11) {m = 0;y++;}return { y, m };});else
setWeekStart((w) => shiftISO(w, d * 7));
};
const goToday = () => {
const d = fromISO(today);setCursor({ y: d.getFullYear(), m: d.getMonth() });
setWeekStart(mondayOfISO(today));setSelDay(today);
};
const saveEvent = (ev) => {setEvents((prev) => {const ex = prev.find((x) => x.id === ev.id);return ex ? prev.map((x) => x.id === ev.id ? ev : x) : [...prev, ev];});toast('Termin gespeichert');};
const delEvent = (id) => {setEvents((prev) => prev.filter((x) => x.id !== id));toast('Termin gelöscht');};
const openPeek = (bar) => setPeek({ kind: bar.kind, ref: bar.ref });
const setEntryColor = (color) => {
if (!peek) return;
if (peek.kind === 'rental') {
setRentals && setRentals((prev) => prev.map((x) => x.id === peek.ref.id ? { ...x, color } : x));
setPeek((p) => p && { ...p, ref: { ...p.ref, color } });
} else {
setEvents((prev) => prev.map((x) => x.id === peek.ref.id ? { ...x, color } : x));
setPeek((p) => p && { ...p, ref: { ...p.ref, color } });
}
};
const editFromPeek = () => {
if (!peek) return;
if (peek.kind === 'rental') {const id = peek.ref.id;setPeek(null);go('rentals', { rentalId: id });} else
{const ev = peek.ref;setPeek(null);setEvtSheet(ev);}
};
const subtitle = mode === 'month' ?
`${DE_MONTHS[cursor.m]} ${cursor.y}` :
`${fmtDateDE(weekStart)} – ${fmtDateDE(shiftISO(weekStart, 6))}`;
return (
<>
move(-1)} title={mode === 'month' ? 'Vorheriger Monat' : 'Vorherige Woche'} />
move(1)} title={mode === 'month' ? 'Nächster Monat' : 'Nächste Woche'} />
{/* day side panel — vertical timeline */}
setEvtSheet({ id: '', title: '', equipmentId: '', start: selDay, end: selDay, color: '#5856D6', startTime: '09:00', endTime: '11:00', note: '' })} />
setPeek(null)}
onColor={setEntryColor}
onEdit={editFromPeek}
onDelete={() => {if (peek && peek.kind === 'event') {delEvent(peek.ref.id);setPeek(null);}}} />
setEvtSheet(null)} onSave={saveEvent} onDelete={delEvent} />
>);
}
// ── Month view with spanning multi-day bars ──
function MonthView({ t, cursor, bars, selDay, today, onPickDay, onPickBar, labelMode, setLabelMode }) {
const firstDow = (new Date(cursor.y, cursor.m, 1).getDay() + 6) % 7;
const daysInMonth = new Date(cursor.y, cursor.m + 1, 0).getDate();
const prevDays = new Date(cursor.y, cursor.m, 0).getDate();
// 6 week rows of 7 days
const weeks = [];
for (let w = 0; w < 6; w++) {
const days = [];
for (let i = 0; i < 7; i++) {
const cellIdx = w * 7 + i;
const dayNum = cellIdx - firstDow + 1;
let date,inMonth = true;
if (dayNum < 1) {date = new Date(cursor.y, cursor.m - 1, prevDays + dayNum);inMonth = false;} else
if (dayNum > daysInMonth) {date = new Date(cursor.y, cursor.m + 1, dayNum - daysInMonth);inMonth = false;} else
date = new Date(cursor.y, cursor.m, dayNum);
days.push({ iso: fmtDateISO(date), date, inMonth });
}
weeks.push(days);
}
const BAR_H = 19,BAR_GAP = 3,HEADER_H = 26;
return (
{DE_DAYS_SHORT.slice(1).concat(DE_DAYS_SHORT[0]).map((d) =>
{d}
)}
{/* label-mode toggle (only shown in month view) */}
{weeks.map((week, wi) => {
const weekStart = week[0].iso,weekEnd = week[6].iso;
// Find bars intersecting this week, clipped to week bounds
const segs = bars.
filter((b) => b.end >= weekStart && b.start <= weekEnd).
map((b) => ({
...b,
segStart: b.start < weekStart ? weekStart : b.start,
segEnd: b.end > weekEnd ? weekEnd : b.end,
startsHere: b.start >= weekStart,
endsHere: b.end <= weekEnd
}));
// For lane packing, use segment bounds
const { sorted, laneCount } = assignLanes(segs.map((s) => ({ ...s, start: s.segStart, end: s.segEnd })));
return (
{/* day cells (background + day number) */}
{week.map((c) => {
const isToday = c.iso === today;
const isSel = c.iso === selDay;
return (
onPickDay(c.iso)} scale={0.997}>
);
})}
{/* spanning bars overlay */}
{(() => {
const MAX_VIS = 3; // lanes 0..MAX_VIS-1 show bars; lane MAX_VIS reserved for "+N weitere"
const visible = sorted.filter((s) => s.lane < MAX_VIS);
const hidden = sorted.filter((s) => s.lane >= MAX_VIS);
// For each day in the week, count how many hidden bars cover it
const hiddenPerDay = week.map((c) => hidden.filter((s) => s.segStart <= c.iso && s.segEnd >= c.iso).length);
return (
<>
{visible.map((s) => {
const startCol = (fromISO(s.segStart) - fromISO(weekStart)) / 86400000;
const span = (fromISO(s.segEnd) - fromISO(s.segStart)) / 86400000 + 1;
const topPx = s.lane * (BAR_H + BAR_GAP);
return (
{e.stopPropagation && e.stopPropagation();onPickBar(s);}} scale={0.98}>
{(() => {
const prefix = s.startsHere ? '' : '…';
if (labelMode === 'equipment') {
const name = prefix + (s.eqName || s.title);
return (
<>
{name}
{s.eqTimes && {' · ' + s.eqTimes}}
>);
}
return {prefix + s.title};
})()}
);
})}
{/* per-day "+N weitere" pills */}
{week.map((c, di) => {
const n = hiddenPerDay[di];
if (!n) return null;
return (
{ e.stopPropagation && e.stopPropagation(); onPickDay(c.iso); }} scale={0.96}>
+{n} weitere
);
})}
>
);
})()}
);
})}
{setLabelMode &&
}
);
}
// ── Plan view: equipment × week timeline (Belegungsplan) ──
function PlanView({ t, weekStart, bars, equipment, today, selDay, onPickDay, onPickBar }) {
const days = Array.from({ length: 7 }, (_, i) => {
const iso = shiftISO(weekStart, i);
return { iso, date: fromISO(iso) };
});
const COL_LABEL_W = 200;
const BAR_H = 22,BAR_GAP = 4,ROW_PAD = 10,BASE_ROW_H = 50;
const planByEq = {};
equipment.forEach((eq) => {
const eqBars = bars.filter((b) => b.equipmentId === eq.id && b.end >= weekStart && b.start <= shiftISO(weekStart, 6)).
map((b) => ({
...b,
segStart: b.start < weekStart ? weekStart : b.start,
segEnd: b.end > shiftISO(weekStart, 6) ? shiftISO(weekStart, 6) : b.end,
startsHere: b.start >= weekStart
}));
planByEq[eq.id] = assignLanes(eqBars.map((b) => ({ ...b, start: b.segStart, end: b.segEnd })));
});
const rowH = (eqId) => {
const lc = (planByEq[eqId] || { laneCount: 1 }).laneCount;
return Math.max(BASE_ROW_H, ROW_PAD * 2 + lc * BAR_H + (lc - 1) * BAR_GAP);
};
return (
{/* day header */}
Equipment
{days.map((d) => {
const isToday = d.iso === today,isSel = d.iso === selDay;
return (
onPickDay(d.iso)} scale={0.98}>
{DE_DAYS_SHORT[d.date.getDay()]}
{d.date.getDate()}
);
})}
{/* rows */}
{equipment.map((eq) => {
const { sorted } = planByEq[eq.id] || { sorted: [] };
const h = rowH(eq.id);
return (
{/* day column separators */}
{days.map((d, i) =>
)}
{/* bars */}
{sorted.map((s) => {
const startCol = (fromISO(s.segStart) - fromISO(weekStart)) / 86400000;
const span = (fromISO(s.segEnd) - fromISO(s.segStart)) / 86400000 + 1;
return (
onPickBar(s)} scale={0.98} style={{
position: 'absolute', top: ROW_PAD + s.lane * (BAR_H + BAR_GAP), height: BAR_H,
left: `calc(${startCol / 7 * 100}% + 3px)`, width: `calc(${span / 7 * 100}% - 6px)`
}}>
{(() => {
const prefix = s.startsHere ? '' : '…';
const fromISOD = (iso) => { const d = fromISO(iso); return `${d.getDate()}.${d.getMonth() + 1}.`; };
const pickup = s.startTime ? `↑${fromISOD(s.start)} ${s.startTime}` : '';
const ret = s.endTime ? `↓${fromISOD(s.end)} ${s.endTime}` : '';
const meta = [pickup, ret].filter(Boolean).join(' ');
return (
<>
{prefix + s.title}
{meta && {' · ' + meta}}
>
);
})()}
);
})}
);
})}
);
}
// ── Day side panel: vertical hour timeline + multi-day strip ──
// Mirrors the mobile day view: colored icon-circle nodes sit ON a central
// strand at each event's start; pill cards extend to the right, height ∝ duration.
function DaySidePanel({ t, dayISO, bars, equipment, today, onPickBar, onAdd }) {
const dayBars = bars.filter((b) => dayISO >= b.start && dayISO <= b.end);
const timed = [],multi = [];
dayBars.forEach((b) => {
const isFirst = b.start === dayISO,isLast = b.end === dayISO;
const sHr = isFirst ? parseTime(b.startTime) : null;
const eHr = isLast ? parseTime(b.endTime) : null;
const sameDay = b.start === b.end;
if (sameDay && sHr != null && eHr != null && eHr > sHr) timed.push({ b, sHr, eHr });else
if (sameDay) multi.push({ b, isFirst: true, isLast: true, untimed: true });else
multi.push({ b, isFirst, isLast });
});
timed.sort((a, c) => a.sHr - c.sHr || c.eHr - a.eHr);
// Lane-pack timed events
const tLaneEnds = [];
timed.forEach((s) => {
let lane = tLaneEnds.findIndex((end) => s.sHr >= end - 0.001);
if (lane === -1) {lane = tLaneEnds.length;tLaneEnds.push(s.eHr);} else
tLaneEnds[lane] = s.eHr;
s.lane = lane;
});
const LANE_COUNT = Math.max(1, tLaneEnds.length);
const minHr = timed.length ? Math.max(0, Math.floor(Math.min(...timed.map((s) => s.sHr))) - 1) : 8;
const maxHr = timed.length ? Math.min(24, Math.ceil(Math.max(...timed.map((s) => s.eHr))) + 1) : 18;
const HOUR_H = 64;
const TIMELINE_H = (maxHr - minHr) * HOUR_H + 32;
const LINE_X = 78; // central strand x
const NODE_R = 20; // node radius
const PILL_LEFT = LINE_X + NODE_R + 4;
const PILL_RIGHT_PAD = 14;
const isToday = dayISO === today;
const nowFrac = isToday ? new Date().getHours() + new Date().getMinutes() / 60 : -1;
const day = fromISO(dayISO);
const iconFor = (ev) => ev.kind === 'rental' ? icons.doc : icons.cal;
return (
{DE_DAYS[day.getDay()]}{isToday ? ' · Heute' : ''}
{day.getDate()}. {DE_MONTHS[day.getMonth()]}
{dayBars.length} {dayBars.length === 1 ? 'Eintrag' : 'Einträge'}
{timed.length > 0 && <>·{timed.length} mit Uhrzeit>}
{multi.length > 0 && <>·{multi.length} mehrtägig>}
{/* ── Timed strand ── */}
{(timed.length > 0 || dayBars.length === 0) &&
{/* Hour rail labels + dashed grid */}
{Array.from({ length: maxHr - minHr + 1 }, (_, i) => {
const h = minHr + i,top = i * HOUR_H + 16;
return (
{String(h).padStart(2, '0')}:00
);
})}
{/* Central vertical strand */}
{/* Now-marker */}
{nowFrac >= minHr && nowFrac <= maxHr &&
}
{/* Event pills + nodes */}
{timed.map((s) => {
const ev = s.b;
const eq = equipment.find((e) => e.id === ev.equipmentId);
const top = (s.sHr - minHr) * HOUR_H + 16;
const dur = s.eHr - s.sHr;
const minutes = Math.round(dur * 60);
const durLabel = minutes < 60 ? minutes + ' min' : dur % 1 === 0 ? dur + ' h' : dur.toFixed(1).replace('.', ',') + ' h';
const PILL_MIN_H = 60;
const pillH = Math.max(dur * HOUR_H, PILL_MIN_H);
const lanePct = 100 / LANE_COUNT;
return (
{/* Node on the strand */}
{/* Pill, lane-split */}
onPickBar(ev)} scale={0.99} style={{
position: 'absolute', top: 0, height: pillH,
left: `calc(${PILL_LEFT}px + ${s.lane * lanePct}% - ${s.lane * (PILL_LEFT + PILL_RIGHT_PAD) / LANE_COUNT}px)`,
width: `calc(${lanePct}% - ${(PILL_LEFT + PILL_RIGHT_PAD) / LANE_COUNT}px - 4px)`
}}>
{fmtHr(s.sHr)}–{fmtHr(s.eHr)} · {durLabel}
{ev.kind === 'rental' && MIETE}
{ev.title}
{eq &&
{eq.name}
}
);
})}
{timed.length === 0 &&
Keine Termine an diesem Tag.
}
}
{/* ── Mehrtägig / Ganztägig strand ── */}
{multi.length > 0 && (() => {
const heading = multi.every((s) => s.untimed) ? 'Ganztägig' :
multi.some((s) => s.untimed) ? 'Mehrtägig & ganztägig' : 'Mehrtägig';
return (
0 ? 8 : 0, padding: '14px 16px 0' }}>
{heading} · {multi.length} {multi.length === 1 ? 'Eintrag' : 'Einträge'}
{multi.map((s) => {
const ev = s.b;
const eq = equipment.find((e) => e.id === ev.equipmentId);
const totalDays = daysBetween(ev.start, ev.end);
const curIdx = daysBetween(ev.start, dayISO);
return (
onPickBar(ev)} scale={0.99}>
{s.untimed ? 'Ganztägig' : `Tag ${curIdx + 1} / ${totalDays}`}
{ev.kind === 'rental' && MIETE}
{ev.title}
{eq ? eq.name : ''}{s.untimed ? '' : (eq ? ' · ' : '') + fmtRange(ev.start, ev.end)}
);
})}
);
})()}
{/* Add-entry CTA */}
Eintrag an diesem Tag hinzufügen
);
}
// ── Entry preview sheet — quick info + color picker + "Bearbeiten" ──
function EntryPreviewSheet({ open, entry, equipment, onClose, onColor, onEdit, onDelete }) {
const t = useTheme();
if (!entry) return null;
const isRental = entry.kind === 'rental';
const ref = entry.ref;
const eq = equipment.find((e) => e.id === ref.equipmentId);
const days = daysBetween(ref.start, ref.end);
const color = ref.color || (isRental ? statusMeta(ref.status).color : '#5856D6');
const sm = isRental ? statusMeta(ref.status) : null;
const COLORS = ['#34C759', '#FF9500', '#5856D6', '#FF2D55', '#AF52DE', '#007AFF', '#1c1c1e', '#8E8E93'];
const hasTime = ref.startTime || ref.endTime;
// ── Rich rental view ──
if (isRental) {
const items = typeof getRentalItems === 'function' ? getRentalItems(ref, equipment) : ref.items || [{ equipmentName: ref.equipmentName, quantity: ref.quantity || 1, dailyRate: ref.dailyRate || 0 }];
const total = typeof rentalTotal === 'function' ? rentalTotal(ref) : 0;
const fee = typeof rentalDeliveryFee === 'function' ? rentalDeliveryFee(ref) : 0;
const paid = ref.paymentStatus === 'bezahlt';
const payDot = paid ? t.green : t.orange;
const payLabel = paid ? 'Bezahlt' : 'Offen';
const dShort = (iso) => {const d = fromISO(iso);return `${d.getDate()}. ${DE_MONTHS[d.getMonth()]}`;};
const SectionLbl = ({ children, style }) =>
{children}
;
return (
{/* Header */}
{/* Customer name */}
{ref.tenantName}
{ref.purpose &&
{ref.purpose}
}
{/* Zeitraum + Gesamtpreis */}
Zeitraum
{fmtRange(ref.start, ref.end)}
{days} Tag{days > 1 ? 'e' : ''}
Gesamtpreis
{total.toFixed(2).replace('.', ',')} €
{/* Abholung + Rückgabe */}
{hasTime &&
Abholung
{ref.startTime || '–'}{ref.startTime ? ' Uhr' : ''}
{dShort(ref.start)}
Rückgabe
{ref.endTime || '–'}{ref.endTime ? ' Uhr' : ''}
{dShort(ref.end)}
}
{/* Mieter */}
Mieter
{ref.tenantName}
{ref.phone &&
{ref.phone}
}
{ref.email &&
{ref.email}
}
{ref.address &&
{ref.address}
}
{/* Equipment */}
Equipment · {items.length} {items.length === 1 ? 'Position' : 'Positionen'}
{items.map((it, i) => {
const eqx = it.equipmentId && equipment.find((e) => e.id === it.equipmentId);
const qty = Math.max(1, Number(it.quantity) || 1);
const sum = days * (Number(it.dailyRate) || 0) * qty;
return (
{eqx && eqx.name || it.equipmentName}
{qty} Stück · {Number(it.dailyRate || 0).toFixed(2).replace('.', ',')} €/Tag (Stück)
{sum.toFixed(2).replace('.', ',')} €
);
})}
Summe · {days} Tag{days > 1 ? 'e' : ''}{fee > 0 ? ` · inkl. ${fee.toFixed(2).replace('.', ',')} € Lieferung` : ''}{Number(ref.discount) ? ` · −${Number(ref.discount)} € Rabatt` : ''}
{total.toFixed(2).replace('.', ',')} €
{/* Lieferung */}
{ref.delivery && ref.delivery.enabled &&
Lieferung
{ref.delivery.address || ref.address}
{(Number(ref.delivery.km) > 0 || fee > 0) &&
{ref.delivery.km ? `${ref.delivery.km} km · ` : ''}{fee.toFixed(2).replace('.', ',')} € Liefergebühr
}
}
{/* Farbe */}
Farbe
{COLORS.map((c) =>
onColor(c)} scale={0.86}>
)}
{/* Action */}
Im Vermietungen-Tab öffnen
);
}
// ── Event (non-rental) view — simpler ──
return (
{/* meta rows */}
1 ? 'e' : ''}`} />
{hasTime && }
{eq && }
{ref.note && }
{/* color picker */}
Farbe
{COLORS.map((c) =>
onColor(c)} scale={0.86}>
)}
{/* actions */}
);
}
function PeekRow({ t, icon, label, value, mono }) {
return (
);
}
// ── Event dialog ──
function EventSheet({ open, initial, equipment, onClose, onSave, onDelete }) {
const t = useTheme();
const [form, setForm] = useState(initial || {});
useEffect(() => {if (open) setForm(initial || {});}, [open, initial]);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const isEdit = !!(initial && initial.id);
const COLORS = ['#5856D6', '#FF2D55', '#AF52DE', '#34C759', '#FF9500', '#007AFF'];
const valid = form.title && form.title.trim();
if (!open) return null;
return (
{isEdit ? 'Termin bearbeiten' : 'Neuer Termin'}
Abbrechen
set('title', e.target.value)} placeholder="z.B. Werkstatt-Termin" />
set('start', e.target.value)} />
set('end', e.target.value)} />
{(() => {
const allDay = !form.startTime && !form.endTime;
const toggleAllDay = () => {
if (allDay) setForm((f) => ({ ...f, startTime: '09:00', endTime: '11:00' }));
else setForm((f) => ({ ...f, startTime: '', endTime: '' }));
};
return (
<>
Ganzer Tag
{allDay ? 'Ohne feste Uhrzeit' : 'Mit Start- & Endzeit'}
{!allDay && (
set('startTime', e.target.value)} />
set('endTime', e.target.value)} />
)}
>
);
})()}
set('note', e.target.value)} placeholder="optional" />
{COLORS.map((c) =>
set('color', c)} scale={0.85}>)}
{isEdit &&
{onDelete(form.id);onClose();}} scale={0.97}>Löschen
}
{if (!valid) return;onSave({ ...form, id: form.id || 'g-' + Date.now(), color: form.color || '#5856D6' });onClose();}} scale={0.98} style={{ flex: 1 }}>
{isEdit ? 'Speichern' : 'Anlegen'}
);
}
Object.assign(window, { ScreenCalendar, EventSheet });