From 61317881830981a78df2a96b200fe8651e133148 Mon Sep 17 00:00:00 2001 From: "Dennis (via Claude+Gemma)" Date: Sat, 23 May 2026 05:24:10 +0200 Subject: [PATCH] feat(settings-page): App-Settings (workspace name, default-billable, etc.) [tsc:fail] --- .phase5-state.json | 5 +- GENERATION_LOG.md | 18 +++++ apps/api/src/db/schema.ts | 10 ++- apps/api/src/routes/settings.ts | 95 +++++++++++++++++++++++ apps/web/src/pages/Settings.tsx | 131 ++++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/routes/settings.ts create mode 100644 apps/web/src/pages/Settings.tsx diff --git a/.phase5-state.json b/.phase5-state.json index 93c434d..8199c64 100644 --- a/.phase5-state.json +++ b/.phase5-state.json @@ -1,8 +1,9 @@ { "completed_features": [ "dark-mode-toggle", - "customer-detail-page" + "customer-detail-page", + "project-detail-page" ], - "current_feature": "project-detail-page", + "current_feature": "settings-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 f000128..625eeee 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -571,3 +571,21 @@ undefined - `05:22:50` **INFO** wrote 3762 chars in 31.4s (attempt 1) - `05:22:50` **INFO** Running tsc --noEmit on api… - `05:22:52` **INFO** tsc clean ✓ +- `05:22:52` **INFO** Committed feature project-detail-page +- `05:22:52` **INFO** Pushed: rc=0 + +## Phase-3 Feature: settings-page (2026-05-23 05:22:52) + +- `05:22:52` **INFO** Description: App-Settings (workspace name, default-billable, etc.) +- `05:22:52` **INFO** Generating apps/api/src/db/schema.ts (ERWEITERT — behalte alle bestehenden Tabellen. Füge neue Tabelle `appS…) +- `05:23:07` **INFO** wrote 1815 chars in 15.4s (attempt 1) +- `05:23:07` **INFO** Generating apps/api/src/routes/settings.ts (Fastify-Plugin /api/settings. GET / (current settings, lazy-init if no…) +- `05:23:29` **INFO** wrote 2647 chars in 21.8s (attempt 1) +- `05:23:29` **INFO** Generating apps/web/src/pages/Settings.tsx (Settings-Page. Form mit workspaceName, defaultBillable (checkbox), wee…) +- `05:24:09` **INFO** wrote 4829 chars in 39.7s (attempt 1) +- `05:24:09` **INFO** Running tsc --noEmit on api… +- `05:24:10` **WARN** tsc errors: +src/routes/settings.ts(3,10): error TS2305: Module '"../db/schema"' has no exported member 'settings'. +undefined +/home/dark/Developer/EmberClone/apps/api: + ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL  Command failed with exit code 2: tsc --noEmit -p tsconfig.json diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 92c8522..35872f1 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, text, timestamp, boolean, uuid } from "drizzle-orm/pg-core" +import { pgTable, text, timestamp, boolean, uuid, integer } from "drizzle-orm/pg-core" export const users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), @@ -32,4 +32,12 @@ export const timeEntries = pgTable("time_entries", { startTime: timestamp("start_time").notNull(), endTime: timestamp("end_time"), createdAt: timestamp("created_at").notNull().defaultNow() +}) + +export const appSettings = pgTable("app_settings", { + id: uuid("id").primaryKey().defaultRandom(), + workspaceName: text("workspace_name").notNull().default("EmberClone"), + defaultBillable: boolean("default_billable").notNull().default(true), + weekStart: integer("week_start").notNull().default(1), + updatedAt: timestamp("updated_at").notNull().defaultNow() }) \ No newline at end of file diff --git a/apps/api/src/routes/settings.ts b/apps/api/src/routes/settings.ts new file mode 100644 index 0000000..8222d1f --- /dev/null +++ b/apps/api/src/routes/settings.ts @@ -0,0 +1,95 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { settings } from "../db/schema" +import { eq } from "drizzle-orm" +import { z } from "zod" + +const SettingsUpdateSchema = z.object({ + siteName: z.string().min(1).optional(), + maintenanceMode: z.boolean().optional(), + contactEmail: z.string().email().optional(), + allowRegistration: z.boolean().optional() +}) + +export default async function settingsRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + const isAdmin = (request: any) => { + return (request.user as { sub: string, role: string })?.role === "admin" + } + + fastify.get("/", async (request, reply) => { + const [currentSettings] = await db + .select() + .from(settings) + .limit(1) + + if (!currentSettings) { + // Lazy-init default settings if none exist + const [defaults] = await db + .insert(settings) + .values({ + siteName: "EmberClone", + maintenanceMode: false, + allowRegistration: true, + contactEmail: "admin@example.com" + }) + .returning() + + return defaults + } + + return currentSettings + }) + + fastify.patch("/", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const body = SettingsUpdateSchema.parse(request.body) + + // We assume there is only one settings row (singleton pattern) + const [updated] = await db + .update(settings) + .set(body) + .where(eq(settings.id, "global")) // Assuming a fixed ID for global settings + .returning() + + if (!updated) { + // If no row with ID 'global' exists, we create it or update the first available + const [existing] = await db.select().from(settings).limit(1) + + if (!existing) { + const [created] = await db + .insert(settings) + .values({ + id: "global", + ...body, + siteName: body.siteName ?? "EmberClone", + maintenanceMode: body.maintenanceMode ?? false, + allowRegistration: body.allowRegistration ?? true, + contactEmail: body.contactEmail ?? "admin@example.com" + }) + .returning() + return created + } + + const [finalUpdate] = await db + .update(settings) + .set(body) + .where(eq(settings.id, existing.id)) + .returning() + + return finalUpdate + } + + return updated + }) +} \ No newline at end of file diff --git a/apps/web/src/pages/Settings.tsx b/apps/web/src/pages/Settings.tsx new file mode 100644 index 0000000..c387d8e --- /dev/null +++ b/apps/web/src/pages/Settings.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { api } from '../lib/api'; +import { useToast } from '../components/Toast'; + +export default function Settings() { + const toast = useToast(); + const [workspaceName, setWorkspaceName] = useState(''); + const [defaultBillable, setDefaultBillable] = useState(false); + const [weekStart, setWeekStart] = useState('Monday'); + + const { data: settings, isLoading, refetch } = useQuery({ + queryKey: ['settings'], + queryFn: () => api.getSettings(), + }); + + const { data: user } = useQuery({ + queryKey: ['me'], + queryFn: () => api.getMe(), + }); + + const updateMutation = useMutation({ + mutationFn: async (data: { workspaceName: string; defaultBillable: boolean; weekStart: string }) => { + return api.updateSettings(data); + }, + onSuccess: async () => { + toast.success('Einstellungen erfolgreich aktualisiert'); + await refetch(); + }, + onError: () => { + toast.error('Fehler beim Aktualisieren der Einstellungen'); + }, + }); + + useEffect(() => { + if (settings) { + setWorkspaceName(settings.workspaceName || ''); + setDefaultBillable(settings.defaultBillable ?? false); + setWeekStart(settings.weekStart || 'Monday'); + } + }, [settings]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + updateMutation.mutate({ + workspaceName, + defaultBillable, + weekStart, + }); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (user?.role !== 'admin') { + return ( +
+

Zugriff verweigert

+

Diese Seite ist nur für Administratoren zugänglich.

+
+ ); + } + + return ( +
+
+
+

Workspace Einstellungen

+
+ +
+
+ {/* Workspace Name */} +
+ + setWorkspaceName(e.target.value)} + className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" + placeholder="z.B. Meine Agentur GmbH" + /> +
+ + {/* Default Billable */} +
+ setDefaultBillable(e.target.checked)} + className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 rounded cursor-pointer" + /> + +
+ + {/* Week Start */} +
+ + +
+
+ +
+ +
+
+
+
+ ); +} \ No newline at end of file