184 lines
7.2 KiB
Python
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()))
|