diff --git a/.phase5-state.json b/.phase5-state.json index 326d92d..0bb1f23 100644 --- a/.phase5-state.json +++ b/.phase5-state.json @@ -1,5 +1,7 @@ { - "completed_features": [], - "current_feature": "dark-mode-toggle", + "completed_features": [ + "dark-mode-toggle" + ], + "current_feature": "customer-detail-page", "started_at": "2026-05-23T05:20:11.407108" } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 191aa73..871efa1 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -549,3 +549,15 @@ undefined - `05:21:06` **INFO** wrote 4485 chars in 40.0s (attempt 1) - `05:21:06` **INFO** Running tsc --noEmit on api… - `05:21:08` **INFO** tsc clean ✓ +- `05:21:08` **INFO** Committed feature dark-mode-toggle +- `05:21:08` **INFO** Pushed: rc=0 + +## Phase-3 Feature: customer-detail-page (2026-05-23 05:21:08) + +- `05:21:08` **INFO** Description: Customer-Detail: zeigt Projekte + letzte Time-Entries des Kunden +- `05:21:08` **INFO** Generating apps/api/src/routes/customers.ts (ERWEITERT — behalte CRUD. Füge GET /:id/projects (alle Projekte zum Ku…) +- `05:21:37` **INFO** wrote 3254 chars in 29.2s (attempt 1) +- `05:21:37` **INFO** Generating apps/web/src/pages/CustomerDetail.tsx (CustomerDetail-Page. Liest customerId aus URL-Param. Zeigt: Customer-H…) +- `05:22:17` **INFO** wrote 5138 chars in 39.8s (attempt 1) +- `05:22:17` **INFO** Running tsc --noEmit on api… +- `05:22:18` **INFO** tsc clean ✓ diff --git a/apps/api/src/routes/customers.ts b/apps/api/src/routes/customers.ts index bf3e76a..2c79e73 100644 --- a/apps/api/src/routes/customers.ts +++ b/apps/api/src/routes/customers.ts @@ -1,7 +1,7 @@ import { FastifyInstance } from "fastify" import { db } from "../db" -import { customers } from "../db/schema" -import { eq, and } from "drizzle-orm" +import { customers, projects, timeEntries } from "../db/schema" +import { eq, and, inArray, desc } from "drizzle-orm" import { z } from "zod" const CustomerSchema = z.object({ @@ -48,6 +48,42 @@ export default async function customerRoutes(fastify: FastifyInstance) { return customer }) + fastify.get("/:id/projects", async (request, reply) => { + const { id } = request.params as { id: string } + + const results = await db + .select() + .from(projects) + .where(eq(projects.customerId, id)) + .orderBy(projects.name) + + return results + }) + + fastify.get("/:id/time-entries", async (request, reply) => { + const { id } = request.params as { id: string } + + const customerProjects = await db + .select({ id: projects.id }) + .from(projects) + .where(eq(projects.customerId, id)) + + if (customerProjects.length === 0) { + return [] + } + + const projectIds = customerProjects.map((p) => p.id) + + const results = await db + .select() + .from(timeEntries) + .where(inArray(timeEntries.projectId, projectIds)) + .orderBy(desc(timeEntries.startTime)) + .limit(50) + + return results + }) + fastify.post("/", async (request, reply) => { const body = CustomerSchema.parse(request.body) diff --git a/apps/web/src/pages/CustomerDetail.tsx b/apps/web/src/pages/CustomerDetail.tsx new file mode 100644 index 0000000..0f1f47e --- /dev/null +++ b/apps/web/src/pages/CustomerDetail.tsx @@ -0,0 +1,118 @@ +import { useQuery } from "@tanstack/react-query" +import { useParams, Link } from "@tanstack/react-router" +import { api } from "../lib/api" +import type { Customer, Project, TimeEntry } from "@emberclone/shared" + +export default function CustomerDetail() { + const { customerId } = useParams({ from: "/customers/$customerId" }) + + const { data: customer, isLoading: customerLoading, isError: customerError } = useQuery({ + queryKey: ["customer", customerId], + queryFn: () => api.getCustomer(customerId!) + }) + + const { data: projects, isLoading: projectsLoading } = useQuery({ + queryKey: ["customerProjects", customerId], + queryFn: () => api.getCustomerProjects(customerId!), + enabled: !!customerId + }) + + const { data: entries, isLoading: entriesLoading } = useQuery({ + queryKey: ["customerEntries", customerId], + queryFn: () => api.getCustomerTimeEntries(customerId!), + enabled: !!customerId + }) + + if (customerLoading || projectsLoading || entriesLoading) { + return
Loading customer details...
+ } + + if (customerError || !customer) { + return
Customer not found or error loading data.
+ } + + return ( +
+
+
+ + ← Back to Customers + +

{customer.name}

+

Customer ID: {customer.id}

+
+
+ Active +
+
+ +
+
+
+

Projects

+ {projects && projects.length > 0 ? ( +
    + {projects.map((project: Project) => ( +
  • + + {project.name} + +
  • + ))} +
+ ) : ( +

No projects assigned to this customer.

+ )} +
+
+ +
+
+
+

Recent Time Entries

+

Showing the last 20 entries

+
+
+ + + + + + + + + + {entries && entries.length > 0 ? ( + entries.map((entry: TimeEntry) => ( + + + + + + )) + ) : ( + + + + )} + +
DateProjectHours
+ {new Date(entry.date).toLocaleDateString()} + + {entry.projectName || "Unassigned"} + + {entry.hours}h +
+ No time entries found for this customer. +
+
+
+
+
+
+ ) +} \ No newline at end of file