Skip to content

Latest commit

 

History

History
483 lines (306 loc) · 9.59 KB

File metadata and controls

483 lines (306 loc) · 9.59 KB

REST API Reference

Base URL: http://localhost:3001/api

All responses are JSON unless otherwise noted. Dates are serialised as ISO 8601 strings. Decimal fields are returned as strings to preserve precision.


Authentication

POST /auth/login

Log in with username and password. Returns an access token and sets a refresh cookie.

Auth: None

Request body:

{ "username": "admin", "password": "admin123" }

Response 200:

{
  "accessToken": "eyJhbG...",
  "user": {
    "id": "uuid",
    "username": "admin",
    "role": "admin",
    "status": "active"
  }
}

Also sets cookie: refreshToken (httpOnly, 7 days, path /).

Errors: 400 missing fields, 401 invalid credentials or inactive account.


POST /auth/refresh

Issue a new access token from the refresh cookie.

Auth: None (cookie-based)

Response 200:

{ "accessToken": "eyJhbG..." }

Also rotates the refreshToken cookie.

Errors: 401 no or invalid refresh token.


POST /auth/logout

Clear the refresh cookie.

Auth: None

Response 200:

{ "message": "Logged out" }

GET /auth/me

Return the current user's profile.

Auth: Bearer token

Response 200:

{ "id": "uuid", "username": "admin", "role": "admin", "status": "active" }

Clients (Admin only)

All routes require Bearer token + admin role.

GET /clients

List all clients, ordered by name ascending.

Response 200: Client[]

[{ "id": "uuid", "name": "Acme Corp", "status": "active" }]

GET /clients/:id

Get a single client.

POST /clients

Create a client.

Body: { "name": string, "status": "active" | "inactive" }

Response 201: Created Client

Errors: 400 validation, 409 duplicate name.

PUT /clients/:id

Update a client.

Body: { "name": string, "status": "active" | "inactive" }

DELETE /clients/:id

Delete a client.


Engagements (Admin only)

GET /engagements

List all engagements with nested client.name, ordered by code.

Response 200: Engagement[] (includes client: { name })

GET /engagements/:id

POST /engagements

Body:

{
  "clientId": "uuid",
  "code": "ACME-WEB-2026",
  "description": "Website Redesign",
  "type": "fixed_price",
  "budget": 50000,
  "startDate": "2026-01-01",
  "endDate": "2026-06-30"
}

type values: fixed_price or t_and_m.

Errors: 400 validation (missing fields, start >= end), 409 duplicate code.

PUT /engagements/:id

DELETE /engagements/:id


Users (Admin only)

Password is never included in any response.

GET /users

List all users (id, username, role, status), ordered by username.

GET /users/:id

POST /users

Body:

{
  "username": "john.doe",
  "password": "secret",
  "role": "employee",
  "status": "active"
}

Username is normalised to lowercase. Password is hashed with bcrypt cost 12.

Errors: 409 duplicate username.

PUT /users/:id

Update username, role, and/or status. Does not accept password.

Body: { "username"?: string, "role"?: string, "status"?: string }

PUT /users/:id/password

Change a user's password.

Body: { "password": "newpassword" }

DELETE /users/:id


Cost Rates (Admin only)

GET /cost-rates

List all cost rates with nested user: { id, username }, ordered by user then start date descending.

GET /cost-rates/:id

POST /cost-rates

Body:

{
  "userId": "uuid",
  "hourlyRate": 45.00,
  "startDate": "2026-01-01",
  "endDate": null
}

endDate can be null (valid indefinitely) or a date string.

PUT /cost-rates/:id

DELETE /cost-rates/:id


Assignments (Admin only)

GET /assignments

List all assignments with nested user and engagement (including client.name).

GET /assignments/:id

POST /assignments

Body:

{
  "userId": "uuid",
  "engagementId": "uuid",
  "billingRate": 95.00,
  "startDate": "2026-01-01",
  "endDate": "2026-12-31"
}

Validation rules:

  • startDate and endDate must fall within the engagement's date range.
  • billingRate is required if engagement type = t_and_m, ignored for fixed_price.
  • Date range must not overlap with another assignment for the same user + engagement.

Errors: 400 validation, 404 engagement not found, 409 date overlap.

PUT /assignments/:id

DELETE /assignments/:id


Timesheets (Authenticated)

GET /timesheets

Query params:

  • periodStart (optional) — e.g. 2026-04-01. If omitted, returns all timesheets for the user.
  • userId (optional, admin only) — view another user's timesheet.

Employees always see only their own data.

Response 200: Timesheet (single, if periodStart given) or Timesheet[]

POST /timesheets/:id/submit

Change status from draft to submitted. Employees can submit their own; admins can submit any.

Errors: 400 not in draft status, 403 not the owner (employee).

POST /timesheets/:id/approve (Admin only)

Change status from submitted to approved.

POST /timesheets/:id/send-back (Admin only)

Change status from submitted back to draft.


Time Entries (Authenticated)

GET /time-entries

Query params (required):

  • periodStart — e.g. 2026-04-01
  • userId (optional, admin only)

Returns all time entries for the user in the given month, with nested assignment.engagement.client and timesheet.status.

GET /time-entries/assignments

List active assignments for a user on a given date (used to populate the assignment dropdown in the entry form).

Query params:

  • date (required) — e.g. 2026-04-15
  • userId (optional, admin only)

POST /time-entries

Create a time entry. Auto-creates a draft timesheet if none exists for that month.

Body:

{
  "assignmentId": "uuid",
  "date": "2026-04-15",
  "actualHours": 4.0,
  "billedHours": 3.5,
  "description": "Worked on feature X",
  "userId": "uuid"
}

billedHours is required for T&M engagements. userId is only used by admins.

Permission rules:

  • Employee: can only create in draft timesheets they own.
  • Admin: can create in draft or submitted timesheets. Cannot create in approved.

Validation (blocking errors — return 400):

  • Total actual_hours on the same date for the user must be < 24.
  • Total billed_hours on the same date for the same client (T&M engagements only) must be < 24.

Validation (non-blocking warnings — included in response):

  • Total actual_hours on the same date > 8: warning returned in warnings[].
  • Total billed_hours on the same date for the same client > 8: warning returned.

Response 201:

{
  "id": "uuid",
  "timesheetId": "uuid",
  "assignmentId": "uuid",
  "date": "2026-04-15T00:00:00.000Z",
  "actualHours": "4",
  "billedHours": "3.5",
  "description": "Worked on feature X",
  "assignment": { ... },
  "timesheet": { "id": "uuid", "status": "draft" },
  "warnings": ["Total actual hours on this date exceed 8 hours"]
}

PUT /time-entries/:id

Update a time entry. Same validation and permission rules as create.

DELETE /time-entries/:id

Delete a time entry. Same permission rules.


Reports (Admin only)

GET /reports/user-monthly

Query params: userId, periodStart

Response 200:

[
  {
    "date": "2026-03-02T00:00:00.000Z",
    "actualHours": 4,
    "clientName": "Acme Corp",
    "engagementCode": "ACME-WEB-2026",
    "description": "Website redesign work"
  }
]

GET /reports/user-monthly/export

Same params. Returns an .xlsx file download.

Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet


GET /reports/user-engagement

Query params: userId, periodStart, engagementId (optional)

  • Without engagementId: returns { engagements: [...] } — list of T&M engagements with logged time.
  • With engagementId: returns { entries: [{ date, billedHours, description }] }.

GET /reports/user-engagement/export

Requires all three params. Returns .xlsx.


GET /reports/user-client

Query params: userId, periodStart, clientId (optional)

  • Without clientId: returns { clients: [...] } — clients with T&M time entries.
  • With clientId: returns { entries: [{ date, billedHours, engagementCode, description }] }.

GET /reports/user-client/export

Requires all three params. Returns .xlsx.


Margins (Admin only)

GET /margins

Returns margin data for all engagements.

Response 200:

[
  {
    "clientName": "Acme Corp",
    "engagementCode": "ACME-WEB-2026",
    "engagementType": "fixed_price",
    "revenue": 50000.00,
    "cost": 2970.00,
    "margin": 47030.00,
    "marginPerHour": 712.58
  }
]

Calculation rules:

Engagement type Revenue formula Cost formula
fixed-price engagement.budget SUM(actual_hours * cost_rate)
t&m SUM(billed_hours * assignment.billing_rate) SUM(actual_hours * cost_rate)

margin = revenue - cost margin_per_hour = margin / SUM(actual_hours) (0 if no hours)


Error Response Format

All error responses use this shape:

{ "error": "Human-readable error message" }

Common HTTP Status Codes

Code Meaning
200 Success
201 Created
400 Validation error
401 Unauthenticated
403 Forbidden (wrong role)
404 Resource not found
409 Conflict (duplicate, overlap)
500 Internal server error