Files
timetracker/backend/src/schemas/index.ts
Simon Franken 7101f38bc8 feat: implement client targets v2 (weekly/monthly periods, working days, pro-ration)
- Add PeriodType enum and working_days column to ClientTarget schema
- Rename weekly_hours -> target_hours; remove Monday-only constraint
- Add migration 20260224000000_client_targets_v2
- Rewrite computeBalance() to support weekly/monthly periods, per-spec
  pro-ration for first period, ongoing vs completed period logic, and
  elapsed working-day counting (§4–§6 of requirements doc)
- Update Zod schemas and TypeScript input types for new fields
- Frontend: replace WeekBalance with PeriodBalance; update
  ClientTargetWithBalance to currentPeriod* fields
- ClientTargetPanel: period type radio, working-day toggles, free date
  picker, dynamic hours label
- DashboardPage: rename widget to Targets, dynamic This week/This month
  label
2026-02-24 19:02:32 +01:00

98 lines
3.3 KiB
TypeScript

import { z } from 'zod';
export const IdSchema = z.object({
id: z.string().uuid(),
});
export const CreateClientSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().max(1000).optional(),
});
export const UpdateClientSchema = z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().max(1000).optional(),
});
export const CreateProjectSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().max(1000).optional(),
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
clientId: z.string().uuid(),
});
export const UpdateProjectSchema = z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().max(1000).optional(),
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().nullable(),
clientId: z.string().uuid().optional(),
});
export const CreateTimeEntrySchema = z.object({
startTime: z.string().datetime(),
endTime: z.string().datetime(),
breakMinutes: z.number().int().min(0).optional(),
description: z.string().max(1000).optional(),
projectId: z.string().uuid(),
});
export const UpdateTimeEntrySchema = z.object({
startTime: z.string().datetime().optional(),
endTime: z.string().datetime().optional(),
breakMinutes: z.number().int().min(0).optional(),
description: z.string().max(1000).optional(),
projectId: z.string().uuid().optional(),
});
export const TimeEntryFiltersSchema = z.object({
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
projectId: z.string().uuid().optional(),
clientId: z.string().uuid().optional(),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(50),
});
export const StatisticsFiltersSchema = z.object({
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
projectId: z.string().uuid().optional(),
clientId: z.string().uuid().optional(),
});
export const StartTimerSchema = z.object({
projectId: z.string().uuid().optional(),
});
export const UpdateTimerSchema = z.object({
projectId: z.string().uuid().optional().nullable(),
startTime: z.string().datetime().optional(),
});
export const StopTimerSchema = z.object({
projectId: z.string().uuid().optional(),
});
const WorkingDayEnum = z.enum(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']);
export const CreateClientTargetSchema = z.object({
clientId: z.string().uuid(),
targetHours: z.number().positive().max(168),
periodType: z.enum(['weekly', 'monthly']),
workingDays: z.array(WorkingDayEnum).min(1, 'At least one working day is required'),
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'startDate must be a date in YYYY-MM-DD format'),
});
export const UpdateClientTargetSchema = z.object({
targetHours: z.number().positive().max(168).optional(),
periodType: z.enum(['weekly', 'monthly']).optional(),
workingDays: z.array(WorkingDayEnum).min(1, 'At least one working day is required').optional(),
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'startDate must be a date in YYYY-MM-DD format').optional(),
});
export const CreateCorrectionSchema = z.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'date must be in YYYY-MM-DD format'),
hours: z.number().min(-1000).max(1000),
description: z.string().max(255).optional(),
});