diff --git a/.phase25-state.json b/.phase25-state.json index fd01b66..7b78acd 100644 --- a/.phase25-state.json +++ b/.phase25-state.json @@ -1,8 +1,9 @@ { "completed_features": [], - "current_feature": "default-project-per-customer", + "current_feature": "holiday-calendar", "started_at": "2026-05-23T08:41:41.344530", "attempted_features": [ - "working-hours-config" + "working-hours-config", + "default-project-per-customer" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index f218648..000a8ae 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2916,3 +2916,25 @@ 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, +- `08:44:06` **INFO** Committed feature default-project-per-customer +- `08:44:07` **INFO** Pushed: rc=0 + +## Phase-3 Feature: holiday-calendar (2026-05-23 08:44:07) + +- `08:44:07` **INFO** Description: Holiday-Tabelle + Page für admin (Feiertage definieren) +- `08:44:07` **INFO** Generating apps/api/src/db/schema.ts (WICHTIG: BEHALTE ALLE bisherigen Tabellen + Spalten. Füge nur Tabelle …) +- `08:44:44` **INFO** wrote 4331 chars in 37.3s (attempt 1) +- `08:44:44` **INFO** Generating apps/api/src/routes/holidays.ts (Fastify-Plugin /api/holidays. CRUD GET/POST/DELETE. Admin-only für POS…) +- `08:45:10` **INFO** wrote 2721 chars in 25.4s (attempt 1) +- `08:45:10` **INFO** Generating apps/web/src/pages/Holidays.tsx (Holidays-Page (admin-only). Liste + Create-Form (date, name). Tailwind…) +- `08:45:56` **INFO** wrote 5476 chars in 46.0s (attempt 1) +- `08:45:56` **INFO** Running tsc --noEmit on api… +- `08:45:57` **WARN** tsc errors: +src/db/schema.ts(37,14): error TS7022: 'customers' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. +src/db/schema.ts(43,59): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. +src/db/schema.ts(47,14): error TS7022: 'projects' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer. +src/db/schema.ts(51,56): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions. +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, diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 1f2d438..c637450 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, integer, customType } from "drizzle-orm/pg-core" +import { pgTable, text, timestamp, boolean, uuid, integer, customType, date } from "drizzle-orm/pg-core" const bytea = customType<{ data: Buffer }>({ dataType() { @@ -96,5 +96,12 @@ export const timeEntryTemplates = pgTable("time_entry_templates", { userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), description: text("description"), - projectId: uuid("project_id").references(() => projects.id), + createdAt: timestamp("created_at").notNull().defaultNow() +}) + +export const holidays = pgTable("holidays", { + id: uuid("id").primaryKey().defaultRandom(), + date: date("date").notNull(), + name: text("name").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow() }) \ No newline at end of file diff --git a/apps/api/src/routes/holidays.ts b/apps/api/src/routes/holidays.ts new file mode 100644 index 0000000..9df3c51 --- /dev/null +++ b/apps/api/src/routes/holidays.ts @@ -0,0 +1,110 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { holidays } from "../db/schema" +import { eq, and, desc } from "drizzle-orm" +import { z } from "zod" + +const HolidaySchema = z.object({ + date: z.string().datetime(), + description: z.string().min(1), + allDay: z.boolean().optional() +}) + +const HolidayUpdateSchema = HolidaySchema.partial() + +export default async function holidayRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/", async (request, reply) => { + const results = await db + .select() + .from(holidays) + .orderBy(desc(holidays.date)) + + return results + }) + + fastify.get("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + + const [holiday] = await db + .select() + .from(holidays) + .where(eq(holidays.id, id)) + .limit(1) + + if (!holiday) { + return reply.code(404).send({ message: "Holiday not found" }) + } + + return holiday + }) + + fastify.post("/", async (request, reply) => { + const user = request.user as any + if (user?.role !== "admin") { + return reply.code(403).send({ message: "Admin privileges required" }) + } + + const body = HolidaySchema.parse(request.body) + + const [holiday] = await db + .insert(holidays) + .values({ + date: body.date, + description: body.description, + allDay: body.allDay ?? true + }) + .returning() + + return reply.code(201).send(holiday) + }) + + fastify.patch("/:id", async (request, reply) => { + const user = request.user as any + if (user?.role !== "admin") { + return reply.code(403).send({ message: "Admin privileges required" }) + } + + const { id } = request.params as { id: string } + const body = HolidayUpdateSchema.parse(request.body) + + const [holiday] = await db + .update(holidays) + .set(body) + .where(eq(holidays.id, id)) + .returning() + + if (!holiday) { + return reply.code(404).send({ message: "Holiday not found" }) + } + + return holiday + }) + + fastify.delete("/:id", async (request, reply) => { + const user = request.user as any + if (user?.role !== "admin") { + return reply.code(403).send({ message: "Admin privileges required" }) + } + + const { id } = request.params as { id: string } + + const [holiday] = await db + .delete(holidays) + .where(eq(holidays.id, id)) + .returning() + + if (!holiday) { + return reply.code(404).send({ message: "Holiday not found" }) + } + + return reply.code(204).send() + }) +} \ No newline at end of file diff --git a/apps/web/src/pages/Holidays.tsx b/apps/web/src/pages/Holidays.tsx new file mode 100644 index 0000000..6212351 --- /dev/null +++ b/apps/web/src/pages/Holidays.tsx @@ -0,0 +1,138 @@ +import { useState } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { Calendar, Plus, Trash2, AlertCircle } from "lucide-react" +import { api } from "../lib/api" + +export default function Holidays() { + const queryClient = useQueryClient() + const [name, setName] = useState("") + const [date, setDate] = useState("") + + const { data: holidays, isLoading, isError } = useQuery({ + queryKey: ["holidays"], + queryFn: () => api.listHolidays() + }) + + const createMutation = useMutation({ + mutationFn: (payload: { name: string; date: string }) => api.createHoliday(payload), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["holidays"] }) + setName("") + setDate("") + } + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.deleteHoliday(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["holidays"] }) + } + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim() || !date) return + createMutation.mutate({ name, date }) + } + + if (isError) { + return ( +
+ +

Error loading holidays. Please try again later.

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

Public Holidays

+
+
+ +
+
+ + setName(e.target.value)} + placeholder="e.g. Christmas Day" + className="px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all" + /> +
+
+ + setDate(e.target.value)} + className="px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all" + /> +
+
+ +
+
+ +
+ + + + + + + + + + {isLoading ? ( + + + + ) : holidays?.length === 0 ? ( + + + + ) : ( + holidays?.map((h) => ( + + + + + + )) + )} + +
DateNameActions
Loading holidays...
No holidays configured.
+ {new Date(h.date).toLocaleDateString('en-GB', { + year: 'numeric', + month: 'short', + day: 'numeric' + })} + {h.name} + +
+
+
+ ) +} \ No newline at end of file