Skip to content

Latest commit

 

History

History
280 lines (189 loc) · 9.33 KB

File metadata and controls

280 lines (189 loc) · 9.33 KB

Features


1. Authentication

Login

  • Users submit a username and password at /login.
  • The backend normalises the username to lowercase before lookup.
  • The password is verified against the bcrypt hash (cost 12).
  • If the user's status is not active, login is rejected.
  • On success the API returns a JWT access token (15 min expiry) and sets an HTTP-only refresh cookie (7 days).
  • The frontend stores the access token in JavaScript memory (not localStorage) and attaches it as a Bearer header on every API call.

Silent refresh

  • On page load, the frontend calls POST /api/auth/refresh to restore a session from the cookie.
  • When an API call returns 401, the Axios response interceptor automatically attempts a refresh before retrying the failed request.
  • Concurrent 401 responses share a single refresh promise to avoid race conditions.

Logout

  • POST /api/auth/logout clears the refresh cookie.
  • The frontend clears the in-memory access token and redirects to /login.

2. Admin CRUD

All admin pages follow the same pattern: a data table with sortable columns, an "Add New" button, and an "Edit" action per row. Create and edit forms open in a modal dialog.

2.1 Clients (/admin/clients)

Column Description
Name Unique client name
Status active or inactive

Form fields: name (text), status (select).

Validation: name is required and must be unique (409 on duplicate).

2.2 Engagements (/admin/engagements)

Column Description
Client Parent client name
Code Unique engagement code
Description Short description
Type Fixed Price or T&M
Budget Total engagement value in EUR
Start Date / End Date Engagement period

Form fields: client (searchable select of active clients), code, description, type, budget, start date, end date.

Validation: start date must be before end date. Code must be unique.

2.3 Users (/admin/users)

Column Description
Username Always lowercase
Role admin or employee
Status active or inactive

Form fields: username, password (only on create), role, status.

Separate action: "Password" button opens a modal to change a user's password.

Backend rules: username is normalised to lowercase and trimmed. Password is hashed with bcrypt cost 12. Password is never returned in any API response.

2.4 Cost Rates (/admin/cost-rates)

Column Description
Username Employee name
Hourly Rate Internal cost rate in EUR
Start Date When this rate becomes effective
End Date When this rate expires (- = indefinite)

Form fields: user (searchable select), hourly rate, start date, end date (optional).

Filter: dropdown to filter the table by user.

2.5 Assignments (/admin/assignments)

Column Description
Username Assigned employee
Engagement Code + client name
Billing Rate Per-hour billing rate (T&M only)
Start Date / End Date Assignment period

Form fields: user (searchable select), engagement (searchable select showing code + client + type), billing rate (shown only for T&M engagements), start date, end date.

Auto-fill: selecting an engagement auto-fills start/end dates from the engagement's date range.

Backend validation rules:

  1. Assignment dates must fall within the engagement's date range.
  2. billing_rate is required if and only if the engagement type is t&m.
  3. For the same (user, engagement) pair, date ranges must not overlap.

3. Employee Timesheet (/timesheets)

Month selector

A left/right arrow calendar picker defaults to the current month.

Time entries table

Displays all entries for the authenticated user in the selected month:

Column Description
Date Entry date (DD/MM/YYYY)
Assignment Engagement code + client name
Actual Hours Hours worked
Billed Hours Hours billed (T&M only, - for fixed-price)
Description Free text
Actions Edit / Delete (only in draft)

Entries are sorted by date ascending.

Creating / editing entries

The form includes:

  • Date: date picker constrained to the selected month.
  • Assignment: select from the user's active assignments whose date range covers the selected entry date.
  • Actual Hours: number input with step 0.25.
  • Billed Hours: number input with step 0.25 (shown only for T&M engagements).
  • Description: text area.

Auto-create timesheet

When the user creates the first entry for a month that has no timesheet, a draft timesheet is automatically created.

Hour validation

Rule Scope Threshold Behaviour
Max actual hours Per user, per date >= 24 Blocking — entry is rejected
High actual hours Per user, per date > 8 Warning — yellow toast, entry is saved
Max billed hours Per client, per date (T&M only) >= 24 Blocking — entry is rejected
High billed hours Per client, per date (T&M only) > 8 Warning — yellow toast, entry is saved

Submit

When the timesheet status is draft and at least one entry exists, a "Submit Timesheet" button is shown. On click, status changes to submitted and all entries become read-only for the employee.

Status badge

The current timesheet status is shown as a colour-coded badge:

  • Draft — yellow
  • Submitted — blue
  • Approved — green

4. Admin Timesheet Management (/admin/timesheets)

Selectors

  • User: searchable select to pick any user.
  • Month: same calendar month picker.

Viewing entries

Same table as the employee view, showing entries for the selected user and month.

Editing entries (when status = submitted)

Admins can add, edit, and delete entries on submitted timesheets. The same validation rules apply (hour thresholds, assignment ownership, billed hours for T&M).

If the admin creates the first entry for a month with no timesheet, a draft timesheet is auto-created.

Status transitions

Current status Available actions
draft (no admin actions — employee must submit first)
submitted Send Back to Draft, Approve
approved (read-only for everyone)

5. Reports

All three reports are admin-only and follow the same pattern: select a user and month, optionally drill into a specific engagement or client, view a data table, and export to Excel.

5.1 User Monthly Report (/admin/reports/user-monthly)

Shows all time entries for a selected user in a selected month.

Column Description
Date Entry date
Actual Hours Hours worked
Client Name Client of the engagement
Engagement Code Engagement code
Description Entry description

Export: downloads an .xlsx file with the same columns.

5.2 User Monthly Report by T&M Engagement (/admin/reports/user-engagement)

  1. Select a user and month.
  2. A dropdown appears listing T&M engagements the user logged time against in that month.
  3. Select an engagement to see entries.
Column Description
Date Entry date
Billed Hours Hours billed
Description Entry description

Export: .xlsx with the same columns.

5.3 User Monthly Report by Client — T&M only (/admin/reports/user-client)

  1. Select a user and month.
  2. A dropdown appears listing clients that have T&M engagements with logged time.
  3. Select a client to see entries.
Column Description
Date Entry date
Billed Hours Hours billed
Engagement Code Engagement code
Description Entry description

Export: .xlsx with the same columns.

Excel export implementation

All exports use the exceljs library. The backend constructs a workbook in memory, writes it to the HTTP response stream with the appropriate Content-Type and Content-Disposition headers, and the browser triggers a download. The frontend fetches the export URL with the Bearer token via a fetch + Blob pattern (since Axios does not handle binary downloads as cleanly).


6. Margin Dashboard (/admin/margins)

Calculation rules

Fixed-price engagements:

revenue = engagement.budget
cost    = SUM(time_entry.actual_hours * cost_rate.hourly_rate)
margin  = revenue - cost

T&M engagements:

revenue = SUM(time_entry.billed_hours * assignment.billing_rate)
cost    = SUM(time_entry.actual_hours * cost_rate.hourly_rate)
margin  = revenue - cost

Margin per worked hour:

margin_per_hour = margin / SUM(actual_hours)    // 0 if no hours logged

The cost rate lookup finds the cost_rates row where user_id matches the time entry's user, start_date <= entry.date, and either end_date >= entry.date or end_date IS NULL.

Dashboard table

Column Sortable Description
Client Name No
Engagement Code No
Engagement Type No Fixed Price or T&M badge
Revenue No Formatted with 2 decimals + EUR symbol
Cost No Formatted with 2 decimals + EUR symbol
Margin Yes Click header to sort. Green if > 0, red if < 0
Margin/Hour Yes Click header to sort. Same colour coding