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)
Deleting a target or correction sets deletedAt instead of hard-deleting.
Creating a target for a user+client that has a soft-deleted record
reactivates it (clears deletedAt, applies new weeklyHours/startDate)
rather than failing the unique constraint. All reads filter deletedAt = null
on the target, its corrections, and the parent client.
Direct database deletes should still cascade to avoid orphaned records.
The migration now only adds the three deleted_at columns without touching
the existing FK constraints.
findAll and findById filter on client.deletedAt = null so targets
belonging to a soft-deleted client are invisible. The create guard
also rejects soft-deleted clients. The raw SQL balance query now
excludes soft-deleted time entries and projects from tracked totals.
Replace hard deletes with deletedAt timestamp flags on all three entities.
Deleting a client or project only sets its own deletedAt; child records are
excluded implicitly by filtering on parent deletedAt in every read query.
Raw SQL statistics queries also filter out soft-deleted parents.
FK ON DELETE CASCADE removed from Project→Client and TimeEntry→Project.
- Add breakMinutes field to TimeEntry model and database migration
- Users can now add break duration (minutes) to time entries
- Break time is subtracted from total tracked duration
- Validation ensures break time cannot exceed total entry duration
- Statistics and client target balance calculations account for breaks
- Frontend UI includes break time input in TimeEntryFormModal
- Duration displays show break time deduction (e.g., '7h (−1h break)')
- Both project/client statistics and weekly balance calculations updated
Allows users to discard a running timer without creating a time entry.
A trash icon in the timer widget reveals a confirmation step ('Discard / Keep')
to prevent accidental data loss. Backend exposes a new DELETE /api/timer
endpoint that simply deletes the ongoingTimer row.
Allows users to retroactively correct the start time of an ongoing timer
without stopping it. A pencil icon in the timer widget opens an inline
time input pre-filled with the current start time; confirming sends the
new time to the backend which validates it is in the past before persisting.
The /login route was not passing an explicit redirect_uri to the IDP for
the web flow, so openid-client would silently pick a default which could
resolve to localhost:3001 if OIDC_REDIRECT_URI was not set.
- AuthSession.redirectUri is now required (non-optional)
- createAuthSession() requires a redirectUri; detects native vs web via
the timetracker:// scheme prefix instead of presence/absence of the arg
- /login route resolves the URI explicitly: request param for native
flows, config.oidc.redirectUri for web flows
- getAuthorizationUrl() reads redirect_uri from session, no longer
accepts it as a separate argument
- handleCallback() uses session.redirectUri directly, removing the
fallback to config.oidc.redirectUri
iOS clients now exchange the OIDC authorization code for a backend-signed
HS256 JWT via POST /auth/token. All subsequent API requests authenticate
using this JWT as a Bearer token, verified locally — no per-request IDP
call is needed. Web frontend session-cookie auth is unchanged.