184 lines
7.2 KiB
Python
184 lines
7.2 KiB
Python
#!/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 <OnboardingTour /> 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 '<orig> (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()))
|