The ClientTargetPanel component now properly uses the client prop to access
client.id when creating a new target, instead of trying to access target?.clientId
which would be undefined during creation. This ensures new targets are properly
associated with the correct client.
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.
Eliminates the eager instantiation of 2001 view bodies by keeping only
three pages (previous, current, next) alive at all times. After each
swipe settles, dayOffset is shifted and tabSelection is silently reset
to the middle page, preserving the native paging animation.
- Replace 5-tab layout with 4 tabs: Dashboard, Timer, Entries, Settings
- Dashboard: add Work Time Balance section using /client-targets API, showing
per-client weekly progress bar, overtime/undertime label and expandable week breakdown
- Time Entries: replace flat list with UICalendarView month grid; tap a day to see
that day's entries; add filter sheet (date range, project, client); new
TimeEntryDetailSheet for creating and editing entries; duration shown as Xh Ymin
- Settings tab: user info header, navigation to Clients and Projects, logout button
- ClientsListView: list with NavigationLink to ClientDetailView
- ClientDetailView: inline client editing + full work time target CRUD (create,
edit, delete target; add/delete balance corrections with date, hours, description)
- ProjectsListView: grouped by client, NavigationLink to ProjectDetailView
- ProjectDetailView: edit name, description, colour, client assignment
- Add ClientTarget, WeekBalance, BalanceCorrection models and APIEndpoints for
/client-targets routes
- Update TimeInterval formatter: add formattedShortDuration (Xh Ymin / Xmin / <1min)
used throughout app; keep formattedDuration for live timer display
The widget was failing to decode the cached timer data because WidgetTimer
struct didn't match the OngoingTimer JSON structure saved by the app.
Changes:
- Added missing fields (project, createdAt, updatedAt) to WidgetTimer
- Added WidgetProjectReference struct for nested project data
- Fixed project name to use project.name instead of projectId
- Added project color support
- Increased refresh interval from 15 min to 1 min for live updates
- Add confirmation alert when deleting clients with warning about
deleted dependencies (projects and time entries)
- Add confirmation alert when deleting projects with warning about
deleted time entries
- Add confirmation alert when deleting time entries
- All alerts include item name and emphasize action cannot be undone
Times were always showing 0 because ISO8601DateFormatter with default
options does not parse fractional seconds, but Prisma/Node.js serialises
dates as "2026-02-20T09:00:00.000Z" (with .000). Every date(from:) call
silently returned nil, so elapsedTime and duration always fell back to 0.
- Date+Extensions: fromISO8601 now tries .withFractionalSeconds first,
then falls back to whole seconds — single place to maintain
- OngoingTimer.elapsedTime: use Date.fromISO8601() instead of bare formatter
- TimeEntry.duration: use Date.fromISO8601() instead of bare formatters
- TimerView: add TimerUnitLabels view showing h/min/sec column headers
under the monospaced clock digits
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
Add backend.oidc.iosRedirectUri (default: timetracker://oauth/callback) and
backend.jwt.secret to values.yaml and wire them into the backend deployment
as OIDC_IOS_REDIRECT_URI and JWT_SECRET env vars. Update NOTES.txt to surface
both values post-install.
GET /clients and GET /projects return bare arrays, not wrapped objects.
Remove ClientListResponse and ProjectListResponse wrapper structs and
update ClientsViewModel, ProjectsViewModel, and TimerViewModel to decode
[Client] and [Project] directly.
Keychain writes silently failed (missing keychain-access-groups entitlement
on simulator), causing the token to disappear between handleTokenResponse
and the first API call. The in-memory cache ensures the token is always
available within the session; the keychain still persists it across launches
when entitlements allow.