diff --git a/.phase15-state.json b/.phase15-state.json index 04fcbed..2399067 100644 --- a/.phase15-state.json +++ b/.phase15-state.json @@ -7,6 +7,7 @@ "webhook-trigger-events", "password-reset", "weekly-summary-email-stub", - "api-client-phase15" + "api-client-phase15", + "router-phase15" ] } \ No newline at end of file diff --git a/.phase16-state.json b/.phase16-state.json new file mode 100644 index 0000000..26900f2 --- /dev/null +++ b/.phase16-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "pinned-customers", + "started_at": "2026-05-23T07:08:48.804883" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 0fcd8ff..6f43d67 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -1875,3 +1875,29 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `07:07:13` **INFO** Committed feature router-phase15 +- `07:07:14` **INFO** Pushed: rc=0 + +## Phase-15 Run beendet (2026-05-23 07:07:14) + +- `07:07:14` **INFO** OK: 0, Attempted: 6, Total: 6 + +## 🚀 Phase-16 Codegen-Run gestartet (2026-05-23 07:08:48) + + +## Phase-3 Feature: pinned-customers (2026-05-23 07:08:48) + +- `07:08:48` **INFO** Description: Star/Pin Customers an die Top der Liste +- `07:08:48` **INFO** Generating apps/api/src/db/schema.ts (ERWEITERT — füge `pinnedAt: timestamp('pinned_at')` (nullable) zu cust…) +- `07:09:24` **INFO** wrote 4131 chars in 35.3s (attempt 1) +- `07:09:24` **INFO** Generating apps/web/src/pages/Customers.tsx (ERWEITERT — füge Star-Icon-Button (lucide-react Star) pro Customer-Row…) +- `07:10:41` **INFO** wrote 9507 chars in 76.9s (attempt 1) +- `07:10:41` **INFO** Running tsc --noEmit on api… +- `07:10:42` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 54a8b61..1e10fd1 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -20,6 +20,7 @@ export const customers = pgTable("customers", { name: text("name").notNull(), tags: text("tags").array().notNull().default([]), active: boolean("active").notNull().default(true), + pinnedAt: timestamp("pinned_at"), createdAt: timestamp("created_at").notNull().defaultNow() }) @@ -101,13 +102,4 @@ export const savedViews = pgTable("saved_views", { entityType: text("entity_type").notNull(), config: text("config").notNull(), createdAt: timestamp("created_at").notNull().defaultNow() -}) - -export const passwordResetTokens = pgTable("password_reset_tokens", { - id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - tokenHash: text("token_hash").notNull(), - expiresAt: timestamp("expires_at").notNull(), - usedAt: timestamp("used_at"), - createdAt: timestamp("created_at").notNull().defaultNow() }) \ No newline at end of file diff --git a/apps/web/src/pages/Customers.tsx b/apps/web/src/pages/Customers.tsx index dffb779..759c5b2 100644 --- a/apps/web/src/pages/Customers.tsx +++ b/apps/web/src/pages/Customers.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useMemo } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { Star } from "lucide-react" import { api } from "../lib/api" export default function Customers() { @@ -31,6 +32,14 @@ export default function Customers() { } }) + const pinMutation = useMutation({ + mutationFn: ({ id, pinned }: { id: string; pinned: boolean }) => + api.updateCustomer(id, { pinnedAt: pinned ? new Date().toISOString() : null }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["customers"] }) + } + }) + const importMutation = useMutation({ mutationFn: (file: File) => api.importCustomersCsv(file), onSuccess: (data) => { @@ -42,17 +51,25 @@ export default function Customers() { } }) - const filteredCustomers = useMemo(() => { + const filteredAndSortedCustomers = useMemo(() => { if (!customers) return [] - return customers.filter(c => { + const filtered = customers.filter(c => { const matchesTag = !filterTag.trim() || c.tags?.some(t => t.toLowerCase().includes(filterTag.toLowerCase())) - const matchesActive = showArchived || c.active !== false - return matchesTag && matchesActive }) + + return filtered.sort((a, b) => { + const aPinned = a.pinnedAt !== null + const bPinned = b.pinnedAt !== null + + if (aPinned && !bPinned) return -1 + if (!aPinned && bPinned) return 1 + + return (a.name || "").localeCompare(b.name || "") + }) }, [customers, filterTag, showArchived]) const handleSubmit = (e: React.FormEvent) => { @@ -107,28 +124,27 @@ export default function Customers() { setName(e.target.value)} - placeholder="Enter company or person name" + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" />
setTags(e.target.value)} - placeholder="e.g. VIP, Enterprise, Lead" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" + placeholder="VIP, Enterprise, Lead" />
@@ -137,74 +153,72 @@ export default function Customers() {
-
-
+
+
setFilterTag(e.target.value)} + className="w-full pl-3 pr-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none" />
-
-
- - - - - - +
+
CustomerTagsActions
+ + + + + + - {filteredCustomers.length === 0 ? ( + {filteredAndSortedCustomers.length === 0 ? ( - + ) : ( - filteredCustomers.map((customer) => ( - - + - + - )) diff --git a/scripts/phase16_features.py b/scripts/phase16_features.py new file mode 100644 index 0000000..faeab99 --- /dev/null +++ b/scripts/phase16_features.py @@ -0,0 +1,183 @@ +#!/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 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()))
NameTagsActions
- No customers found. - No customers found.
- {customer.name} - {customer.active === false && ( - Archived - )} + filteredAndSortedCustomers.map((customer) => ( +
+ + {customer.name}
{customer.tags?.map(tag => ( - + {tag} ))}
- {customer.active !== false && ( - - )} + +