Index / Work
Chrono — Time tracking & leave management
Internal tooling SaaS

ChronoTime tracking & leave management

Chrono is a multi-tenant time-tracking and leave-management application — a Laravel API behind a Vue single-page app where teams log hours against activities, request and approve leave, and let the system do the date arithmetic. The hard parts aren't the forms; they're the rules underneath: counting real working days, keeping every leave balance reconcilable from signed transactions, and locking finished timesheet periods so the past stays the past.

Chrono started from a simple frustration: timesheet and leave tools either store balances as mutable counters that quietly drift, or treat “five days off” as five calendar days regardless of weekends and holidays. So the design choice underneath the whole app is that the interesting numbers are never stored — they’re computed. A leave balance is an allocation plus the signed sum of a transaction ledger. A leave duration is a walk over a date range that skips weekends and the right set of non-working days. Both can be re-derived from source at any time, which makes them auditable instead of mysterious.

Above that domain core sits a fairly ordinary, deliberately boring shape: a Laravel 13 API secured with Sanctum, and a Vue 3 TypeScript SPA that talks to it through TanStack Query, packaged as an installable PWA. It’s multi-tenant from the first migration — organisations scope everything, roles gate access, and people are onboarded through tokenised, expiring invitations. The genuinely opinionated parts — working-day math, the balance and carryover engine, period locking, and an idempotent hand-off to a separate ticketing service — are isolated in a thin services layer so the controllers stay thin and the rules stay testable.

The live spine — time entry, the leave workflow, balances and the working-day calendar — is built and working end to end. The period-locking layer is modelled and migrated, with enforcement still being wired into the write path.

RoleDesign + full-stack
Year2026
Core stackLaravel 13 · Vue 3 · TypeScript
SurfaceREST API + installable PWA
StatusIn active development
Chrono's year-overview screen — a calendar grid of logged hours per day with weekends and public holidays shaded out, a leave-balance panel beside it
Screenshot soon
Time entry

Log hours the way the org works

Each organisation picks how its people record time — start/end ranges or plain durations — and the entry form and validation adapt to that mode. A batch endpoint accepts a whole week of entries in one request so the client can save a grid in a single round trip.

  • Per-organisation range or duration mode
  • Single and batch (/time-entries/batch) writes
  • Durations capped and validated server-side
Screenshot soon
Leave requests

Request, approve, deduct

Members file leave against a leave type for a date span; admins approve or reject from an org-wide queue. On approval the system expands the span into actual working days and records the deduction, so the request and the balance never disagree.

  • Status workflow with org-wide admin view
  • Spans expanded to working days on approval
  • Standalone usage entries supported too
Screenshot soon
Balances

Every balance is a sum, not a stored number

A user's balance for a leave type in a year is the year's allocation plus the signed total of every leave transaction — accruals positive, usage negative. Nothing is mutated in place, so any balance can be re-derived and audited from its transaction history.

  • Allocation + signed-transaction sum
  • Decimal amounts for half-days
  • Carryover with an expiry date
Screenshot soon
Calendars

Working days, not calendar days

The day calculator walks a date range skipping weekends and any non-working day that applies to the user — their organisation's custom closures plus their country's public holidays, resolved by country code. The same map drives both deductions and the calendar UI.

  • Org-specific and country-specific non-working days
  • Holidays resolved per organisation country_code
  • One source of truth for math and display
Screenshot soon
Administration

Multi-tenant from the first migration

Every record is scoped to an organisation. Access is role-based, onboarding runs through tokenised invitations with an expiry, and period locks per org-and-month exist to freeze submitted timesheets — with explicit unlock records when a closed period legitimately needs reopening.

  • Organisation scoping + role-based access
  • Token invitations with accept + expiry
  • Period lock / unlock records per org month
Screenshot soon
Integration

In-app feedback becomes a ticket

Feedback submitted in Chrono is posted to a separate ticketing app (TickIt) over an authenticated HTTP call. The request carries a client-supplied Idempotency-Key, so a retried submission can never create a duplicate ticket across the two systems.

  • Bearer-authenticated cross-app POST
  • Idempotency-Key validated then forwarded end to end
  • Connection and 5xx failures surfaced distinctly
Architecture

A conventional API/SPA split, but the weight sits in a thin domain layer between the controllers and the database — the date and balance rules live in services, not scattered through endpoints.

Client
Vue 3 SPA / PWA
TypeScript components with TanStack Query for server state and TanStack Table for grids; shadcn-vue and Tailwind for UI; installable as a PWA.
Application
Laravel REST API
56 Sanctum-protected routes. Form requests validate and adapt per organisation; admin and super-admin middleware gate privileged actions.
Domain logic
Calculation services
LeaveDaysCalculator, LeaveBalanceService and LeaveTransactionService own the working-day, balance and ledger rules; TicketingClient owns the outbound integration.
Data
MariaDB via Eloquent
13 models across 19 migrations — organisations, users, roles, activities, time entries, and the leave allocation / transaction / non-working-day tables the services read.
Challenges solved
01

Counting a working day

Problem

"How many days is this leave?" has no fixed answer — it depends on weekends, the organisation's own closures, and the public holidays of the country that organisation operates in. Getting it wrong silently corrupts every balance downstream.

Solution

A single LeaveDaysCalculator resolves the applicable non-working days once — org-specific plus country-code-matched public holidays — then walks the range skipping weekends and that set. The exact same map feeds the calendar UI, so the number a user sees and the number deducted are computed the same way.

02

Balances that always reconcile

Problem

A stored "days remaining" counter drifts the moment an approval, adjustment or correction is missed, and there's no way to explain how it got there.

Solution

Balances are never stored. Each is computed as the year's allocation plus the signed sum of every leave transaction for that user and type, with decimal amounts for half-days. The ledger is the source of truth, so any balance can be re-derived and audited from its history.

03

Expiring carryover

Problem

Days carried over from last year shouldn't live forever — they expire on a date, and only the days not yet used should still count.

Solution

Carryover is held on the allocation with an expiry date; the service returns zero once that date passes, and otherwise nets the carried amount against the year's usage so only genuinely unspent days remain — clamped so usage can never push it negative.

04

Idempotent cross-app tickets

Problem

Chrono's feedback feeds a separate ticketing service over the network. A dropped response or a user double-click could otherwise file the same complaint twice, in a system Chrono doesn't own.

Solution

The feedback endpoint requires an Idempotency-Key (validated to a strict format), then forwards it as a header on the Bearer-authenticated POST to TickIt's integration API. Retries reuse the key, so the receiving side can collapse duplicates; connection failures and 5xx responses are raised as distinct, retryable errors rather than swallowed.

How it's built
API & auth
PHP 8.3, Laravel 13, Sanctum, MariaDB
Single-page client
Vue 3, TypeScript, Pinia, TanStack Query, TanStack Table, shadcn-vue
Build & delivery
Vite, Tailwind CSS, PWA

Forms on top, a ledger and a calendar underneath — and the discipline to make every number reproducible from its source.