#!/usr/bin/env python3 """Phase-11: onboarding-tour, pdf-export, time-entry-csv-import, customer-archive, project-cloning.""" from __future__ import annotations import asyncio import datetime import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402 from phase3_features import run_feature_v2 # noqa: E402 PHASE11_STATE = ROOT / ".phase11-state.json" FEATURES: list[Feature] = [ Feature( name="onboarding-tour", description="Onboarding-Tour-Component (intro.js-Style overlay)", files=[ FileGen( path="apps/web/src/components/OnboardingTour.tsx", purpose=( "Onboarding-Tour. Steps-Array mit {selector, title, body} (z.B. Dashboard nav, time-entries link, ⌘K-hint). " "Erkennt 'onboarding-done' in localStorage; wenn nicht: zeigt overlay mit Spotlight auf target-element + Next/Skip. " "Tailwind: fixed inset-0 with pointer-events-auto bg-black/40, popover-card." ), ), FileGen( path="apps/web/src/App.tsx", purpose="ERWEITERT — mount global. Behalte alles.", refs=["apps/web/src/App.tsx"], ), ], ), Feature( name="time-entry-csv-import", description="TimeEntries-CSV-Import (multipart)", files=[ FileGen( path="apps/api/src/routes/time-entries.ts", purpose=( "ERWEITERT — behalte alles. Füge POST /import (multipart CSV: description,startTime,endTime,projectId optional). " "Parse rows, insert für user. Return {imported, errors[]}." ), refs=["apps/api/src/routes/time-entries.ts"], ), FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — füge 'Import CSV'-Button rechts im Filter-Bar (neben Export). " "File-Input → api.importTimeEntriesCsv(file), Toast Ergebnis, refetch." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="customer-archive", description="Soft-archive von Customers (toggle active=false) + Filter", files=[ FileGen( path="apps/web/src/pages/Customers.tsx", purpose=( "ERWEITERT — füge Archive-Button (statt Delete) pro Customer (PATCH active=false). " "Filter-Bar: Toggle 'Auch archivierte anzeigen' (default off). " "Archivierte Rows visually muted (opacity-50). Behalte alles." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], ), Feature( name="project-cloning", description="Project-Clone Endpoint + UI-Button", files=[ FileGen( path="apps/api/src/routes/projects.ts", purpose=( "ERWEITERT — behalte alles. Füge POST /:id/clone (body optional: {name}): " "kopiert existing project, neuer name (default ' (Kopie)'), gleiches customerId. Return new project." ), refs=["apps/api/src/routes/projects.ts"], ), FileGen( path="apps/web/src/pages/Projects.tsx", purpose=( "ERWEITERT — füge Clone-Button (Copy-Icon lucide-react) pro Row. " "Mutation api.cloneProject(id), refetch + toast." ), refs=["apps/web/src/pages/Projects.tsx"], ), ], ), Feature( name="pdf-export-stub", description="PDF-Export-Endpoint für Reports (Stub — generiert text mit .pdf header)", files=[ FileGen( path="apps/api/src/routes/reports.ts", purpose=( "Fastify-Plugin /api/reports. Auth required. " "GET /pdf?from=...&to=... → returnt text/plain stub mit pdf-Header (application/pdf), " "filename=report-YYYY-MM-DD.pdf. Inhalt: 'EmberClone Report\\n\\nUser: ...\\nPeriod: ... to ...\\nEntries: ...' " "(echtes PDF-Rendering in v2). Generate from time_entries des Users." ), refs=["apps/api/src/routes/time-entries.ts"], ), FileGen( path="apps/web/src/pages/Dashboard.tsx", purpose=( "ERWEITERT — behalte alles. Füge 'Report exportieren' Button (download-icon) oben rechts. " "Klick: window.open('/api/reports/pdf?from=...&to=...') mit dieser Woche als Default." ), refs=["apps/web/src/pages/Dashboard.tsx"], ), ], ), Feature( name="api-client-phase11", description="API um phase11 endpoints erweitern", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: importTimeEntriesCsv(file), cloneProject(id, name?), " "archiveCustomer(id), unarchiveCustomer(id)." ), refs=["apps/web/src/lib/api.ts"], ), ], ), Feature( name="router-phase11", description="routes/index.ts mount reports", files=[ FileGen( path="apps/api/src/routes/index.ts", purpose="ERWEITERT — füge reportsRoutes ('/api/reports'). Behalte alles.", refs=["apps/api/src/routes/index.ts"], ), ], ), ] def load_state() -> dict: if PHASE11_STATE.exists(): return json.loads(PHASE11_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE11_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-11 Codegen-Run gestartet") state = load_state() for feature in FEATURES: if feature.name in state.get("completed_features", []): continue state["current_feature"] = feature.name; save_state(state) try: success = await run_feature_v2(feature) if success: state.setdefault("completed_features", []).append(feature.name) else: state.setdefault("attempted_features", []).append(feature.name) save_state(state) except Exception as e: log(f"❌ {feature.name} crashed: {e}", level="ERROR") state.setdefault("attempted_features", []).append(feature.name); save_state(state) log_section("Phase-11 Run beendet") log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))