EmberClone/scripts/phase16_features.py

184 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Phase-16: pinned-customers, smart-suggestions, recent-projects-quick-access, time-entry-templates, dark-mode-improvements."""
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
PHASE16_STATE = ROOT / ".phase16-state.json"
FEATURES: list[Feature] = [
Feature(
name="pinned-customers",
description="Star/Pin Customers an die Top der Liste",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose="ERWEITERT — füge `pinnedAt: timestamp('pinned_at')` (nullable) zu customers. Behalte alles.",
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/web/src/pages/Customers.tsx",
purpose=(
"ERWEITERT — füge Star-Icon-Button (lucide-react Star) pro Customer-Row. "
"Klick toggled pinnedAt (PATCH /customers/:id). "
"Liste sortiert: erst alle pinned (pinnedAt!=null), dann unpinned alphabetisch."
),
refs=["apps/web/src/pages/Customers.tsx"],
),
],
),
Feature(
name="smart-suggestions",
description="Auto-suggest Description basierend auf letzten Einträgen",
files=[
FileGen(
path="apps/web/src/components/SuggestionInput.tsx",
purpose=(
"SuggestionInput-Component. Text-input mit dropdown suggestions unten. "
"Props: value, onChange, suggestions: string[]. "
"Bei Focus + Typing: filter suggestions by startsWith (case-insensitive), zeige top 5. "
"Tab/Enter wählt erste; Klick wählt direkt."
),
),
FileGen(
path="apps/web/src/pages/TimeEntries.tsx",
purpose=(
"ERWEITERT — behalte alles. Description-Input nutzt jetzt SuggestionInput, suggestions = "
"useMemo(() => Array.from(new Set(entries?.map(e=>e.description) || [])).slice(0,50), [entries])."
),
refs=["apps/web/src/pages/TimeEntries.tsx"],
),
],
),
Feature(
name="recent-projects-quick-access",
description="Recent-Projects-Widget für schnellen Project-Select",
files=[
FileGen(
path="apps/web/src/components/RecentProjects.tsx",
purpose=(
"RecentProjects-Widget. Zeigt die letzten 5 unique projects aus den letzten TimeEntries des Users. "
"Pro Project Klick → Quick-Add-Modal (oder QuickAdd-Component) mit projectId pre-filled."
),
),
FileGen(
path="apps/web/src/pages/Dashboard.tsx",
purpose=(
"ERWEITERT — behalte alles. Füge <RecentProjects /> als Section unter Stats."
),
refs=["apps/web/src/pages/Dashboard.tsx"],
),
],
),
Feature(
name="time-entry-templates",
description="Wiederverwendbare TimeEntry-Templates (gespeicherte description+project)",
files=[
FileGen(
path="apps/api/src/db/schema.ts",
purpose=(
"ERWEITERT — füge `timeEntryTemplates` pgTable: id, userId, name (label), description (text), "
"projectId (uuid nullable references projects), defaultDurationMinutes (integer nullable). Behalte alles."
),
refs=["apps/api/src/db/schema.ts"],
),
FileGen(
path="apps/api/src/routes/time-entry-templates.ts",
purpose=(
"Fastify-Plugin /api/time-entry-templates. CRUD GET/POST/PATCH/DELETE. Auth required. "
"User sees only own templates. Use FastifyInstance NOT FastifyPluginAsync."
),
refs=["apps/api/src/routes/customers.ts"],
),
],
),
Feature(
name="dark-mode-improvements",
description="Dark-Mode CSS-Polish (bessere Kontraste in Tables, Forms)",
files=[
FileGen(
path="apps/web/src/index.css",
purpose=(
"ERWEITERT — behalte @tailwind base/components/utilities. "
"Füge dark-mode-specific overrides für common patterns: "
"dark:bg-slate-900 als body-default, dark:text-slate-100 für text. "
"Plus subtle dark borders dark:border-slate-700 für Cards."
),
refs=["apps/web/src/index.css"],
),
],
),
Feature(
name="api-client-phase16",
description="API um time-entry-templates erweitert",
files=[
FileGen(
path="apps/web/src/lib/api.ts",
purpose=(
"ERWEITERT — behalte ALLES. Füge: listTimeEntryTemplates(), createTimeEntryTemplate(data), "
"updateTimeEntryTemplate(id, data), deleteTimeEntryTemplate(id), "
"pinCustomer(id), unpinCustomer(id) (PATCH /customers/:id mit pinnedAt:new Date()/null)."
),
refs=["apps/web/src/lib/api.ts"],
),
],
),
Feature(
name="router-phase16",
description="Mount time-entry-templates route",
files=[
FileGen(
path="apps/api/src/routes/index.ts",
purpose="ERWEITERT — füge timeEntryTemplateRoutes ('/api/time-entry-templates').",
refs=["apps/api/src/routes/index.ts"],
),
],
),
]
def load_state() -> dict:
if PHASE16_STATE.exists():
return json.loads(PHASE16_STATE.read_text())
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
def save_state(state: dict) -> None:
PHASE16_STATE.write_text(json.dumps(state, indent=2))
async def main() -> int:
log_section("🚀 Phase-16 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-16 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()))