feat: implement monthly targets and work-day-aware balance calculation

Add support for monthly and weekly targets with work-day selection:
- Users can now set targets as 'monthly' or 'weekly'
- Users select which days of the week they work
- Balance is calculated per working day, evenly distributed
- Target hours = (periodHours / workingDaysInPeriod)
- Corrections are applied at period level
- Daily balance tracking replaces weekly granularity

Database changes:
- Rename weekly_hours -> target_hours
- Add period_type (weekly|monthly, default=weekly)
- Add work_days (integer array of ISO weekdays, default=[1,2,3,4,5])
- Add constraints for valid period_type and non-empty work_days

Backend:
- Rewrite balance calculation in ClientTargetService
- Support monthly period enumeration
- Calculate per-day targets based on selected working days
- Update Zod schemas for new fields
- Update TypeScript types

Frontend:
- Add period type selector in target form (weekly/monthly)
- Add work days multi-checkbox selector
- Conditional start date input (week vs month)
- Update DashboardPage to show 'this period' instead of 'this week'
- Update ClientsPage to display working days and period type
- Support both weekly and monthly targets in UI

Migration:
- Add migration to extend client_targets table with new columns
- Backfill existing targets with default values (weekly, Mon-Fri)
This commit is contained in:
2026-02-24 18:11:45 +01:00
parent 5b7b8e47cb
commit 4f23c1c653
8 changed files with 481 additions and 145 deletions

View File

@@ -0,0 +1,17 @@
-- Extend client_targets with period_type and work_days, rename weekly_hours to target_hours
-- This migration adds support for monthly targets and work-day-aware balance calculation
-- Rename weekly_hours to target_hours
ALTER TABLE "client_targets" RENAME COLUMN "weekly_hours" TO "target_hours";
-- Add period_type column with default 'weekly' for backwards compatibility
ALTER TABLE "client_targets" ADD COLUMN "period_type" VARCHAR(10) NOT NULL DEFAULT 'weekly';
-- Add work_days column with default Mon-Fri (1-5) for backwards compatibility
ALTER TABLE "client_targets" ADD COLUMN "work_days" INTEGER[] NOT NULL DEFAULT '{1,2,3,4,5}';
-- Add check constraint to ensure period_type is valid
ALTER TABLE "client_targets" ADD CONSTRAINT "period_type_check" CHECK ("period_type" IN ('weekly', 'monthly'));
-- Add check constraint to ensure at least one work day is selected
ALTER TABLE "client_targets" ADD CONSTRAINT "work_days_not_empty_check" CHECK (array_length("work_days", 1) > 0);

View File

@@ -100,12 +100,14 @@ model OngoingTimer {
}
model ClientTarget {
id String @id @default(uuid())
weeklyHours Float @map("weekly_hours")
startDate DateTime @map("start_date") @db.Date // Always a Monday
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
id String @id @default(uuid())
targetHours Float @map("target_hours")
periodType String @default("weekly") @map("period_type") @db.VarChar(10) // 'weekly' | 'monthly'
workDays Int[] @default([1, 2, 3, 4, 5]) @map("work_days") // ISO weekday numbers (1=Mon, 7=Sun)
startDate DateTime @map("start_date") @db.Date // Monday for weekly, 1st of month for monthly
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
userId String @map("user_id") @db.VarChar(255)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)