#!/usr/bin/env python3 """Phase-17: drag-drop-reorder, calendar-month, batch-rename, customer-merge, smart-filter-suggestions.""" 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 PHASE17_STATE = ROOT / ".phase17-state.json" FEATURES: list[Feature] = [ Feature( name="calendar-month-view", description="Monatsansicht für TimeEntries (Grid 6 weeks × 7 days)", files=[ FileGen( path="apps/web/src/pages/Calendar.tsx", purpose=( "ERWEITERT — behalte Week-View. Füge View-Toggle (Week / Month). " "Month-View: Grid 7 Spalten, 6 Reihen, jeder Cell zeigt Tagesnummer + total-hours Badge. " "Vor/zurück-Buttons für Monat-Navigation. Klick auf Day-Cell wechselt zu Week-View dieser Woche." ), refs=["apps/web/src/pages/Calendar.tsx"], ), ], ), Feature( name="batch-rename-projects", description="Bulk-Select + rename Projects via Mutation", files=[ FileGen( path="apps/web/src/pages/Projects.tsx", purpose=( "ERWEITERT — füge Checkbox-Spalte. Wenn min 1 selektiert, Action-Bar oben: " "'Rename mit Prefix...' Input → bei Submit ruft api.bulkRenameProjects(ids, prefix). " "Plus Bulk-Delete-Button." ), refs=["apps/web/src/pages/Projects.tsx"], ), FileGen( path="apps/api/src/routes/projects.ts", purpose=( "ERWEITERT — füge POST /bulk-rename (body: {ids, prefix}): updated jedes Project's name = prefix + ' ' + currentName. " "Plus POST /bulk-delete (ids[]) → delete all. Behalte alles." ), refs=["apps/api/src/routes/projects.ts"], ), ], ), Feature( name="customer-merge", description="Merge zwei Customers: source-Projects auf target umhängen, dann source löschen", files=[ FileGen( path="apps/api/src/routes/customers.ts", purpose=( "ERWEITERT — füge POST /:id/merge (body: {targetId}): " "alle projects vom source-customer (id) bekommen targetId als customerId, dann delete source-customer. " "Admin-only. Behalte alles." ), refs=["apps/api/src/routes/customers.ts"], ), FileGen( path="apps/web/src/pages/Customers.tsx", purpose=( "ERWEITERT — Merge-Button pro Row (admin-only). Modal mit Target-Customer-Picker, Submit → " "api.mergeCustomers(sourceId, targetId), refetch." ), refs=["apps/web/src/pages/Customers.tsx"], ), ], ), Feature( name="smart-filter-suggestions", description="Saved-Views-Vorschläge basierend auf häufig benutzten Filters", files=[ FileGen( path="apps/web/src/components/SmartFilters.tsx", purpose=( "SmartFilters-Component. Zeigt 3-4 vorgeschlagene Filter-Buttons: " "'Diese Woche', 'Letzter Monat', 'Heute', 'Nur ohne Projekt'. " "Props: onApply(filterObject). Klick wendet Filter sofort an." ), ), FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — behalte alles. Füge oberhalb Filter-Bar." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="time-entry-quick-edit", description="Inline-Edit für TimeEntry-Description (Klick auf Description = Input)", files=[ FileGen( path="apps/web/src/pages/TimeEntries.tsx", purpose=( "ERWEITERT — in der Liste: Description-Zelle wird beim Klick zu Input (useState editingId). " "On Blur oder Enter: api.updateTimeEntry(id, {description}). Cancel via Escape." ), refs=["apps/web/src/pages/TimeEntries.tsx"], ), ], ), Feature( name="api-client-phase17", description="API um bulk-rename, customer-merge erweitert", files=[ FileGen( path="apps/web/src/lib/api.ts", purpose=( "ERWEITERT — behalte ALLES. Füge: bulkRenameProjects(ids, prefix), bulkDeleteProjects(ids), " "mergeCustomers(sourceId, targetId)." ), refs=["apps/web/src/lib/api.ts"], ), ], ), ] def load_state() -> dict: if PHASE17_STATE.exists(): return json.loads(PHASE17_STATE.read_text()) return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} def save_state(state: dict) -> None: PHASE17_STATE.write_text(json.dumps(state, indent=2)) async def main() -> int: log_section("🚀 Phase-17 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-17 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()))