(function(){
// ============================================================
// agenda.jsx — Universelle Traktandenliste (wiederkehrende Besprechungspunkte)
// Eigenständige Entität, getrennt von Todos/Habits/Exams.
// Depends on (window): todayISO, localISO, fmtDate, nextInternalRuleDate, DatePicker
// ============================================================
const { useState } = React;
const WEEKDAY_LABELS = [{ dow: 1, l: "Mo" }, { dow: 2, l: "Di" }, { dow: 3, l: "Mi" }, { dow: 4, l: "Do" }, { dow: 5, l: "Fr" }, { dow: 6, l: "Sa" }, { dow: 7, l: "So" }];
function anchorLabel(a){
if (!a) return "";
if (a.anchorType === "outlook") return a.anchor?.subject ? `Outlook · ${a.anchor.subject}` : "Outlook";
if (a.anchorType === "internalRule") {
const days = (a.anchor?.days || []).map(d => WEEKDAY_LABELS.find(w => w.dow === d)?.l).filter(Boolean).join("/");
const everyN = a.anchor?.everyNWeeks > 1 ? `alle ${a.anchor.everyNWeeks} Wochen` : "wöchentlich";
return `${everyN}${days ? " · " + days : ""}`;
}
return "Manuell";
}
function computeSuggestedNextDate(agenda){
if (agenda.anchorType === "internalRule") return nextInternalRuleDate(agenda.anchor, todayISO());
return null;
}
// ── Anker-Auswahl beim Anlegen ───────────────────────────────────────
function AnchorTypePicker({ value, onChange }){
const types = [
{ id: "outlook", label: "Outlook-Termin" },
{ id: "internalRule", label: "Eigene Regel" },
{ id: "manual", label: "Manuell" },
];
return (
{types.map(ty => (
))}
);
}
function OutlookEventPicker({ events, value, onChange }){
const upcoming = (events || []).filter(ev => ev.start?.dateTime).slice(0, 30);
if (upcoming.length === 0) {
return Keine Outlook-Termine geladen (nächste 14 Tage).
;
}
return (
{upcoming.map(ev => {
const sel = value === ev.id;
const dt = ev.start?.dateTime ? new Date(ev.start.dateTime) : null;
return (
);
})}
);
}
function WeekdayPicker({ value, onChange }){
return (
{WEEKDAY_LABELS.map(w => {
const sel = (value || []).includes(w.dow);
return (
);
})}
);
}
// ── Neue Traktandenliste anlegen ─────────────────────────────────────
function NewAgendaForm({ calEvents, onCreate, onCancel }){
const [title, setTitle] = useState("");
const [anchorType, setAnchorType] = useState("manual");
const [outlookEv, setOutlookEv] = useState(null);
const [ruleDays, setRuleDays] = useState([1]);
const [everyNWeeks, setEveryNWeeks] = useState(1);
const [manualDate, setManualDate] = useState(null);
function create(){
if (!title.trim()) return;
let anchor = {};
if (anchorType === "outlook" && outlookEv) {
anchor = { eventId: outlookEv.id, seriesMasterId: outlookEv.seriesMasterId || null, subject: outlookEv.subject || null };
} else if (anchorType === "internalRule") {
anchor = { days: ruleDays, everyNWeeks: everyNWeeks || 1 };
}
const nextDate = anchorType === "internalRule"
? nextInternalRuleDate(anchor, todayISO())
: anchorType === "manual" ? manualDate : (outlookEv?.start?.dateTime ? outlookEv.start.dateTime.slice(0, 10) : null);
onCreate({
id: Date.now() + Math.random(), title: title.trim(), anchorType, anchor,
nextDate, items: [], createdDate: todayISO(),
createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null,
});
}
return (
Neue Traktandenliste
setTitle(e.target.value)}
style={{ width: "100%", boxSizing: "border-box", fontSize: 12, marginBottom: 8 }} autoFocus />
{anchorType === "outlook" &&
}
{anchorType === "internalRule" && (
)}
{anchorType === "manual" &&
}
);
}
// ── Verschieben-Popup beim Abschliessen ──────────────────────────────
function MoveOpenItemsPopup({ agenda, calEvents, openCount, onConfirm, onClose }){
const suggested = computeSuggestedNextDate(agenda);
const [date, setDate] = useState(suggested);
const [outlookEv, setOutlookEv] = useState(null);
function confirm(){
if (agenda.anchorType === "outlook" && outlookEv) {
onConfirm({ nextDate: outlookEv.start?.dateTime?.slice(0, 10) || null, anchor: { ...agenda.anchor, eventId: outlookEv.id, seriesMasterId: outlookEv.seriesMasterId || null, subject: outlookEv.subject || null } });
} else {
onConfirm({ nextDate: date, anchor: agenda.anchor });
}
}
return (
e.stopPropagation()}>
{openCount} Punkt{openCount === 1 ? "" : "e"} nicht besprochen
Auf den nächsten Termin verschieben?
{agenda.anchorType === "outlook"
?
:
}
);
}
// ── Detailansicht ─────────────────────────────────────────────────────
function AgendaDetail({ agenda, calEvents, onUpdate, onDelete, onClose, isMobile }){
const [newItem, setNewItem] = useState("");
const [showMovePopup, setShowMovePopup] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const items = agenda.items || [];
const openItems = items.filter(i => !i.done);
const doneItems = items.filter(i => i.done);
const outlookMissing = agenda.anchorType === "outlook" && agenda.anchor?.eventId && !(calEvents || []).some(ev => String(ev.id) === String(agenda.anchor.eventId));
function addItem(){
if (!newItem.trim()) return;
onUpdate({ ...agenda, items: [...items, { id: Date.now() + Math.random(), text: newItem.trim(), done: false, createdDate: todayISO() }], updatedAt: Date.now() });
setNewItem("");
}
function toggleItem(id){
onUpdate({ ...agenda, items: items.map(i => String(i.id) === String(id) ? { ...i, done: !i.done } : i), updatedAt: Date.now() });
}
function deleteItem(id){
onUpdate({ ...agenda, items: items.filter(i => String(i.id) !== String(id)), updatedAt: Date.now() });
}
function finishMeeting(){
if (openItems.length > 0) { setShowMovePopup(true); return; }
setShowMovePopup(true);
}
function applyMove({ nextDate, anchor }){
onUpdate({ ...agenda, nextDate, anchor, updatedAt: Date.now() });
setShowMovePopup(false);
}
function decoupleFromOutlook(){
onUpdate({ ...agenda, anchorType: "manual", anchor: { nextDate: null }, updatedAt: Date.now() });
}
return (
{agenda.title}
{anchorLabel(agenda)}
{agenda.nextDate &&
Nächster Termin: {fmtDate(agenda.nextDate)}
}
{outlookMissing && (
Termin existiert nicht mehr in Outlook.
)}
setNewItem(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") addItem(); }}
style={{ flex: 1, fontSize: 12 }} />
{openItems.map(i => (
))}
{openItems.length === 0 &&
Keine offenen Punkte.
}
{doneItems.length > 0 && (
{doneItems.map(i => (
toggleItem(i.id)} style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer", opacity: 0.55 }}>
✓
{i.text}
))}
)}
Termin abgeschlossen
{!confirmDelete
? setConfirmDelete(true)} style={{ padding: "8px 10px", fontSize: 12, borderRadius: 8, border: "1px solid var(--col-border)", color: "var(--col-text-sec)", cursor: "pointer" }}>Löschen
: { onDelete(agenda.id); onClose(); }} style={{ padding: "8px 10px", fontSize: 11, borderRadius: 8, background: "#b91c1c", color: "#fff", border: "none", fontWeight: 600, cursor: "pointer" }}>Sicher?}
{showMovePopup &&
setShowMovePopup(false)} />}
);
}
// ── Hauptansicht ──────────────────────────────────────────────────────
function AgendaView({ agendas, calEvents, isMobile, onAdd, onUpdate, onDelete }){
const [selectedId, setSelectedId] = useState(null);
const [showNew, setShowNew] = useState(false);
const list = agendas || [];
const sel = list.find(a => String(a.id) === String(selectedId)) || null;
return (
Traktandenlisten ({list.length})
setShowNew(x => !x)} style={{ padding: "5px 12px", fontSize: 11, borderRadius: 20, background: showNew ? "var(--col-bg-sec)" : "var(--col-accent)", color: showNew ? "var(--col-text-sec)" : "var(--col-bg)", border: showNew ? "1px solid var(--col-border)" : "none", fontWeight: 500, cursor: "pointer" }}>{showNew ? "Abbrechen" : "Neue Liste"}
{showNew &&
{ onAdd(a); setShowNew(false); setSelectedId(a.id); }} onCancel={() => setShowNew(false)} />}
{list.length === 0 && !showNew && (
Noch keine Traktandenliste.
Lege eine neue Liste an, z.B. für eine wiederkehrende Besprechung.
)}
{list.map(a => {
const isSel = sel?.id === a.id;
const openCount = (a.items || []).filter(i => !i.done).length;
return (
setSelectedId(isSel ? null : a.id)} style={{
padding: "10px 12px", borderRadius: 8, cursor: "pointer",
background: isSel ? "var(--col-accent-wash)" : "var(--col-bg)",
border: isSel ? "1.5px solid var(--col-accent)" : "1px solid var(--col-border-soft)",
}}>
{a.title}
{a.nextDate && {fmtDate(a.nextDate)}}
{anchorLabel(a)}
{openCount > 0 && {openCount} offen}
);
})}
{sel && isMobile &&
setSelectedId(null)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.35)", zIndex: 300 }} />}
{sel &&
setSelectedId(null)} isMobile={isMobile} />}
);
}
Object.assign(window, { AgendaView });
})();