import { useEffect, useMemo, useState } from "react"; import { SUGGESTION_STATS, WEEK_THEMES, type Suggestion } from "./data/suggestions"; import { formatDE, getDayInfo, typeLabel, weekWorkdays, } from "./lib/calendar"; import { exportWeekDocx } from "./lib/exportDocx"; import { exportWeekPdf } from "./lib/exportPdf"; import { pickRandomSuggestions, pickWeekSet, suggestionCount, weekNeedsSchool, } from "./lib/suggest"; import { createEmptyWeek, ensureWeek, loadState, nextWeekMonday, pickTheme, prevWeekMonday, saveState, STORAGE_KEY, STORAGE_KEY_LEGACY, type AppState, type WeekEntry, } from "./lib/storage"; import { missingExportFields } from "./lib/weekExport"; import { TRAINEE } from "./data/holidays-bw"; import "./App.css"; const OFFER_COUNT = 4; const TARGET_ACTIVITIES = 8; const MIN_BETRIEB = 4; function withWeek(state: AppState, monday: string): AppState { if (state.weeks[monday]) return state; return { ...state, weeks: { ...state.weeks, [monday]: createEmptyWeek(monday) }, }; } function selectionFromWeek(w: WeekEntry | undefined): Suggestion[] { if (!w) return []; return [ ...w.activities.map((text, i) => ({ id: -1000 - i, text, type: "betrieb" as const, category: "Gespeichert", weight: 1, })), ...w.schoolTopics .filter((t) => t !== "Ferien") .map((text, i) => ({ id: -2000 - i, text, type: "schule" as const, category: "Gespeichert", weight: 1, })), ]; } function draftFields(monday: string, selected: Suggestion[]) { const activities = selected.filter((s) => s.type === "betrieb").map((s) => s.text); let schoolTopics = selected.filter((s) => s.type === "schule").map((s) => s.text); if (!weekNeedsSchool(monday)) schoolTopics = ["Ferien"]; return { activities, schoolTopics }; } function App() { const [state, setState] = useState(() => { const initial = loadState(); return withWeek(initial, initial.currentWeekMonday); }); const [offer, setOffer] = useState([]); const [selected, setSelected] = useState([]); const [flash, setFlash] = useState(""); const [exporting, setExporting] = useState(false); const monday = state.currentWeekMonday; const week = state.weeks[monday] ?? createEmptyWeek(monday); const needsSchool = weekNeedsSchool(monday); const betriebCount = selected.filter((s) => s.type === "betrieb").length; const canExport = betriebCount >= MIN_BETRIEB && !exporting; useEffect(() => { setState((prev) => withWeek(prev, prev.currentWeekMonday)); }, [state.currentWeekMonday]); useEffect(() => { saveState(state); }, [state]); // Auswahl + Vorschläge laden, wenn die offene Woche wechselt useEffect(() => { const w = state.weeks[monday]; const fromSaved = selectionFromWeek(w); setSelected(fromSaved); setOffer( pickRandomSuggestions( monday, OFFER_COUNT, state.usedSuggestionIds, fromSaved.map((s) => s.text), ), ); // nur bei Wochenwechsel // eslint-disable-next-line react-hooks/exhaustive-deps }, [monday]); const dayPills = useMemo( () => weekWorkdays(monday).map((d) => { const info = getDayInfo(d); return { date: d, info }; }), [monday], ); const showToast = (msg: string) => { setFlash(msg); window.setTimeout(() => setFlash(""), 2200); }; const persistDraft = (nextSelected: Suggestion[]) => { setSelected(nextSelected); const { activities, schoolTopics } = draftFields(monday, nextSelected); setState((prev) => { const base = withWeek(prev, monday); return { ...base, weeks: { ...base.weeks, [monday]: { ...base.weeks[monday], activities, schoolTopics, }, }, }; }); }; const refreshOffer = (count = OFFER_COUNT) => { setOffer( pickRandomSuggestions( monday, count, state.usedSuggestionIds, selected.map((s) => s.text), ), ); }; const toggleSelect = (s: Suggestion) => { const exists = selected.some((x) => x.text === s.text); const next = exists ? selected.filter((x) => x.text !== s.text) : [...selected, s]; persistDraft(next); }; const removeSelected = (text: string) => { persistDraft(selected.filter((s) => s.text !== text)); }; const autoFillWeek = () => { const picks = pickWeekSet( monday, state.usedSuggestionIds, selected.map((s) => s.text), ); const texts = new Set(selected.map((s) => s.text)); const merged = [...selected]; for (const p of picks) { if (!texts.has(p.text)) { merged.push(p); texts.add(p.text); } } persistDraft(merged); setOffer( pickRandomSuggestions( monday, OFFER_COUNT, state.usedSuggestionIds, merged.map((s) => s.text), ), ); showToast("Woche mit Vorschlägen gefüllt"); }; const buildWeekFromSelection = (): WeekEntry => { const { activities, schoolTopics: schoolFromSel } = draftFields(monday, selected); let schoolTopics = schoolFromSel; if (needsSchool && schoolTopics.length === 0) { const schoolPick = pickRandomSuggestions( monday, 4, state.usedSuggestionIds, [], ).filter((s) => s.type === "schule"); schoolTopics = schoolPick.slice(0, 3).map((s) => s.text); } return { ...week, activities, schoolTopics, weekTheme: week.weekTheme || pickTheme(), done: true, }; }; const finishWeek = async (andExport: "docx" | "pdf" | "both") => { if (betriebCount < MIN_BETRIEB) { showToast(`Mindestens ${MIN_BETRIEB} Betriebs-Vorschläge auswählen`); return; } setExporting(true); try { const finished = buildWeekFromSelection(); if (!finished.weekTheme.trim()) finished.weekTheme = pickTheme(); if (!finished.frameworkRefs.length) { finished.frameworkRefs = [...week.frameworkRefs]; } if (!needsSchool && finished.schoolTopics.length === 0) { finished.schoolTopics = ["Ferien"]; } const missing = missingExportFields(finished); if (missing.length) { showToast(`Export unvollständig: ${missing.join(", ")}`); return; } const usedIds = [...state.usedSuggestionIds]; for (const s of selected) { if (s.id > 0 && !usedIds.includes(s.id)) usedIds.push(s.id); } const nextMon = nextWeekMonday(monday); setState((prev) => { const base = { ...prev, usedSuggestionIds: usedIds, weeks: { ...prev.weeks }, }; ensureWeek(base, monday); base.weeks[monday] = finished; base.currentWeekMonday = nextMon; ensureWeek(base, nextMon); return base; }); // Ein Klick: gleicher Inhalt (WeekExportContent) in DOCX und PDF if (andExport === "docx" || andExport === "both") await exportWeekDocx(finished); if (andExport === "pdf" || andExport === "both") exportWeekPdf(finished); showToast(`Woche ${finished.number} exportiert – weiter zur nächsten`); } catch (err) { console.error(err); showToast("Export fehlgeschlagen – bitte erneut versuchen"); } finally { setExporting(false); } }; const goWeek = (mon: string) => { setState((prev) => withWeek({ ...prev, currentWeekMonday: mon }, mon)); }; const rerollTheme = () => { setState((prev) => { const base = withWeek(prev, monday); const themes = WEEK_THEMES.filter((t) => t !== base.weeks[monday].weekTheme); const next = themes[Math.floor(Math.random() * themes.length)] || pickTheme(); return { ...base, weeks: { ...base.weeks, [monday]: { ...base.weeks[monday], weekTheme: next }, }, }; }); }; const progressPct = Math.min(100, Math.round((betriebCount / TARGET_ACTIVITIES) * 100)); return (
Berichtsheft

{TRAINEE.name} · Büromanagement · Kfz · Abt. {TRAINEE.department}

{suggestionCount()} Vorschläge aus {SUGGESTION_STATS.sourceFiles} Heften ·{" "} {Math.round(SUGGESTION_STATS.originalShareBetrieb * 100)}% Originalton

{flash &&
{flash}
}

Woche Nr. {week.number}

{week.done && Fertig}

{formatDE(week.from)} – {formatDE(week.to)}

{week.yearLabel}

{dayPills.map(({ date, info }) => ( {info.weekdayLabel.slice(0, 2)} · {typeLabel(info.type)} {info.holidayName ? ` (${info.holidayName})` : ""} {info.schoolHolidayName && info.type !== "feiertag" ? " · Ferien" : ""} ))}
Thema der Woche: {week.weekTheme}

{needsSchool ? "Di/Do Berufsschule – Schul-Vorschläge können mit gewählt werden" : "Ferien / keine Schule diese Woche (alles Betrieb)"}


Vorschläge wählen

Betrieb {betriebCount}/{TARGET_ACTIVITIES} {betriebCount < MIN_BETRIEB ? ` · mind. ${MIN_BETRIEB} für Export` : " · bereit"}

Antippen zum Auswählen oder Abwählen · kein Tippen nötig

{offer.map((s) => { const active = selected.some((x) => x.text === s.text); return ( ); })}

Gewählt ({selected.length})

{selected.length === 0 ? (

Noch nichts gewählt – Vorschläge antippen oder automatisch füllen.

) : (
    {selected.map((s) => (
  • {s.type === "schule" ? "Schule" : "Betrieb"} {s.text}
  • ))}
)}
); } export default App;