Files
trackgo-fe/api.md
T
shancheas e316afa51d refactor: clean up code formatting and improve readability
- Standardized table formatting in `api.md` for privilege keys, enhancing clarity.
- Removed unnecessary line breaks and improved inline formatting in various components, including `right-section.tsx`, `company-settings.remote.service.ts`, and `timeline-helpers.tsx`, to streamline code readability.
- Updated test cases in `privilege-key-hierarchy.test.ts` and `filter-menu-by-view-privilege.test.ts` for better alignment and consistency in assertions.

These changes improve the overall code quality and maintainability, making it easier for developers to navigate and understand the codebase.
2026-09-02 08:14:37 +07:00

33 KiB
Raw Blame History

TrackGo HTTP API — frontend reference

Static contract for a frontend agent or UI. Source of truth is this backends controllers and DTOs. Live OpenAPI (when the server is running and Swagger is enabled): GET /docs (UI) and GET /docs-json.

Base URL: http://localhost:{PORT} (default 3000). There is no global path prefix. JSON keys are camelCase. IDs are UUID v4.


1. Agent rules (read first)

  1. Send Authorization: Bearer <accessToken> on every route except GET / and POST /auth/register|login|refresh|revoke.
  2. List endpoints return { data, meta }. Detail, create, update, status, import, and bulk return the bare DTO or a small object. DELETE is 204 with an empty body (except nested plan destinations, which return the plan).
  3. Request dates as ISO datetime or YYYY-MM-DD. Response timestamps are unix milliseconds (number).
  4. Phones are E.164 (+6281234567890). Money, quantities, and prices are decimal strings (for example "12500.0000"), never floats.
  5. Never send status on PATCH /:id. Use PATCH /:id/status or POST /bulk-status.
  6. Unknown JSON fields are rejected (400). Do not send snake_case aliases.
  7. Gate UI with GET /auth/meisSuperadmin or permissions[KEY][action]. Superadmin bypasses the matrix.
  8. Cycles and plans are purpose-scoped (sales | logistics) and use different privilege keys. Pass purpose on writes (body) and preferably on lists (query).
  9. Cycles and plans DELETE / bulk-delete archive (status: "archived"); they do not hard-delete.
  10. Default new-record status is draft when omitted.

2. Auth

Tokens

Token Type Default lifetime Transport
Access JWT HS256 15m (JWT_ACCESS_EXPIRES_IN) Authorization: Bearer …
Refresh Opaque 64-char string 7 days (REFRESH_TOKEN_EXPIRES_IN_MS) JSON body refreshToken

Refresh rotates: each successful POST /auth/refresh returns a new pair; the old refresh token is invalid.

Rate limits: register/login 5 / 60s, refresh/revoke 10 / 60s, everything else 100 / 60s. Exceeded → 429.

Endpoints

POST /auth/register — public — 201

{ "username": "alice", "password": "password123" }
  • username: 332 chars, ^[a-zA-Z0-9_]+$, stored lowercased
  • password: 872 chars
  • Conflict: 409 { "statusCode": 409, "message": "Username already registered", "error": "Conflict" }

Response:

{ "accessToken": "eyJ…", "refreshToken": "aaaa…" }

New users have no privilege (privilegeId null). They cannot call protected CRUD until an admin assigns a privilege (PATCH /users/:id/privilege) or they are promoted to superadmin in the database.

POST /auth/login — public — 200

Same body and response as register. Invalid credentials → 401.

POST /auth/refresh — public — 200

{ "refreshToken": "<opaque>" }

refreshToken min length 32. Invalid → 401.

POST /auth/revoke — public — 204

Same body. Idempotent: already-invalid tokens still return 204.

GET /auth/me — bearer — 200

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "alice",
  "isSuperadmin": false,
  "privilege": { "id": "…", "name": "Sales Staff", "code": "SALES_STAFF" },
  "permissions": {
    "CONFIGURATION.BRANCH": {
      "view": true,
      "create": true,
      "update": true,
      "delete": false,
      "import": false
    }
  }
}

If the user has no privilege, privilege is null and permissions is {}.

Use permissions to hide buttons. Missing key or false → treat as denied (API returns 403 Insufficient privilege).


3. Global HTTP contract

Headers

Content-Type: application/json
Authorization: Bearer <accessToken>

CSV import uses multipart/form-data with field name file (max 1 MiB).

Pagination (list GET /resource only)

Query (all optional):

Param Rules Default
page integer ≥ 1 1
limit 1200 10
offset integer ≥ 0

If both page and offset are sent, page wins. offset maps to page = floor(offset / limit) + 1.

Public response:

{
  "data": [
    /* items */
  ],
  "meta": {
    "currentPage": 1,
    "itemCount": 10,
    "itemsPerPage": 10,
    "totalItems": 42,
    "totalPages": 5
  }
}

search is case-insensitive and AND-combined with other filters. Resource-specific filters are documented per module.

Non-list responses

Unwrapped resource object, or:

Operation Status Body
Create 201 resource DTO (detail shape)
Update / status 200 resource DTO
Delete 204 empty
Bulk delete 200 { "deleted": number }
Bulk status 200 { "updated": number }
Import 200 { "imported": number }
Generate plans 200 { "created": number, "skipped": number }

Errors (NestJS default)

{
  "statusCode": 400,
  "message": "status cannot be updated via PATCH",
  "error": "Bad Request"
}

message is a string or an array of validation strings.

Status Typical cause
400 Validation, extra fields, invalid VO (phone/date/status), status on PATCH
401 Missing/expired/revoked JWT, bad credentials, invalid refresh
403 Insufficient privilege
404 {Resource} not found (for example Branch not found)
409 Unique conflict (username, code, cycle/plan already exists)
429 Rate limit

CSV batch failure:

{
  "statusCode": 400,
  "message": "CSV validation failed",
  "errors": ["row 3: Invalid phone number"]
}

VO messages do not echo raw input: "Invalid phone number", "Invalid date time", "Invalid status".

Status

Core (configuration, privileges, cycles, plans, settings): draft | active | archived. Omit on create → draft.

Sales statuses (use only these on that resource):

Resource Allowed
Sales request draft, pending, approved, rejected
Sales order / packing slip draft, processed, completed, cancelled
Sales invoice draft, processed, partial, completed, cancelled
Sales payment draft, pending, approved, rejected

Dates

  • In: ISO datetime (2026-08-24T10:00:00+07:00) or calendar YYYY-MM-DD. Naive ISO (no Z/offset) is interpreted in DEFAULT_TIMEZONE (default GMT+7).
  • Out: unix milliseconds UTC for date, createdAt, updatedAt, cycleStartDate, plan date.
  • Display timezone is GMT+7 unless the backend env says otherwise. Persist/compare using the numeric ms, not a formatted string.

Phones

E.164 compact. Invalid → 400 Invalid phone number.

Shared write bodies

{ "status": "active" }
{ "ids": ["uuid", "uuid"] }
{ "ids": ["uuid"], "status": "archived" }

ids must be a non-empty UUID array.


4. Privileges

Actions: view | create | update | delete | import.

HTTP mapping:

Handler Action
GET list / detail view
POST / create, POST /plans/generate create
PATCH /:id, PATCH /:id/status, POST /bulk-status, nested customer contacts, plan destinations update
DELETE /:id, POST /bulk-delete delete
POST /import import

Catalog (GET /privilege-keys, needs ADMIN.SETTINGS.USER.PRIVILEGES view). Keys use Group.Parent.Module or Group.Parent.Module.Submodule:

code label
ADMIN.SETTINGS.USER.PRIVILEGES Privileges
ADMIN.SETTINGS.USER.USERS Users
ADMIN.SETTINGS.DATA.DIVISION Divisions
ADMIN.SETTINGS.DATA.BRANCH Branches
ADMIN.SETTINGS.DATA.CUSTOMER Customers
ADMIN.SETTINGS.DATA.PRODUCT Products
ADMIN.SETTINGS.DATA.SETTING Company settings
ADMIN.SALES.DATA.EMPLOYEE Employees
ADMIN.SALES.DATA.CYCLE Sales cycles
ADMIN.SALES.ACTIVITIES.REQUEST Sales requests
ADMIN.SALES.ACTIVITIES.ORDER Sales orders
ADMIN.SALES.ACTIVITIES.INVOICE Sales invoices
ADMIN.SALES.ACTIVITIES.PAYMENT Sales payments
ADMIN.SALES.ACTIVITIES.PLAN Sales plans
ADMIN.SALES.ACTIVITIES.TIMELINE Sales timeline
ADMIN.SALES.REPORT Sales reports
ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP Packing slips
ADMIN.LOGISTICS.DATA.CYCLE Logistics cycles
ADMIN.LOGISTICS.ACTIVITIES.PLAN Logistics plans
ADMIN.LOGISTICS.REPORT Logistics reports
MOBILE.SALES.PLAN Sales plans (mobile)
MOBILE.SALES.PLAN.ATTENDANCE Branch attendance
MOBILE.SALES.VISIT Customer visits
MOBILE.SALES.TIMELINE Sales timeline (mobile)

Field purpose

Cycles and plans do not use a single key. Privilege is resolved from purpose:

purpose cycle keys plan keys
sales ADMIN.SALES.DATA.CYCLE ADMIN.SALES.ACTIVITIES.PLAN, MOBILE.SALES.PLAN
logistics ADMIN.LOGISTICS.DATA.CYCLE ADMIN.LOGISTICS.ACTIVITIES.PLAN, MOBILE.LOGISTICS.PLAN

purpose is read from body (writes) or query (lists). If omitted, the user may proceed if they have the action on either purpose; list results are filtered to purposes they can view. Superadmin bypasses.

Sales plans may attach invoiceIds only. Logistics plans may attach packingSlipIds only.


5. Standard CRUD (most resources)

Unless a section says otherwise, each resource below implements:

Method Path Status Notes
GET /{resource} 200 Paginated { data, meta }
GET /{resource}/:id 200 Detail (may include nested arrays list omits)
POST /{resource} 201 Create
PATCH /{resource}/:id 200 Update — no status
PATCH /{resource}/:id/status 200 { status }
DELETE /{resource}/:id 204 Hard delete (cycles/plans: archive)
POST /{resource}/bulk-delete 200 { ids }{ deleted }
POST /{resource}/bulk-status 200 { ids, status }{ updated }
POST /{resource}/import 200 multipart file{ imported }

Audit fields on primary DTOs: createdAt, updatedAt (unix ms), createdBy, updatedBy (user UUID).

Name/code patterns used by configuration:

  • Code: ^[A-Za-z0-9_]+$ (no spaces). Max 16 except products (32).
  • Name (division, branch, customer, employee): letters with single spaces, max 64.
  • Product name: letters, digits, + - . / ( ), max 128.

6. Health

GET / — public — "Hello World!" (plain string).


7. Users

Privilege: USERS update only (no list/create API).

PATCH /users/:id/privilege200

{ "privilegeId": "uuid-or-null" }

null clears the assignment. Assigned privilege must be active. Response: { id, username, privilegeId }.


8. Privilege keys

Privilege: PRIVILEGES view.

GET /privilege-keys

Query: pagination + search.

Item:

{ "id": "uuid", "code": "PRIVILEGES", "label": "Privileges", "sortOrder": 1 }

Use this catalog when building the privilege-matrix editor.


9. Privileges

Privilege key: PRIVILEGES. Standard CRUD + import.

List filters: name, code, status, search (name/code).

Create POST /privileges:

{
  "name": "Sales Staff",
  "code": "SALES_STAFF",
  "status": "draft",
  "details": [{ "privilegeKeyId": "uuid", "action": "view", "value": true }]
}

name max 120, code max 64. details optional; omitted cells are treated as denied.

Update PATCH /privileges/:id: name?, code?, details? (replaces matrix when sent).

List item omits details. Detail / create / update include:

{
  "id": "uuid",
  "name": "Sales Staff",
  "code": "SALES_STAFF",
  "status": "active",
  "details": [
    {
      "id": "uuid",
      "privilegeKeyId": "uuid",
      "keyCode": "SALES.INVOICE",
      "keyLabel": "Sales invoices",
      "sortOrder": 11,
      "action": "view",
      "value": true
    }
  ],
  "createdAt": 1710000000000,
  "updatedAt": 1710000000000,
  "createdBy": "uuid",
  "updatedBy": "uuid"
}

Delete of a privilege assigned to users → 409.

CSV headers: name, code (optional status).


10. Divisions

Key: CONFIGURATION.DIVISION. Standard CRUD + import.

Create: { name, code, status? }

List filters: name, code, status, search.

DTO: { id, name, code, status, createdAt, updatedAt, createdBy, updatedBy }

CSV: name, code (optional status).


11. Branches

Key: CONFIGURATION.BRANCH. Standard CRUD + import.

Create (required): code, name, phone, address, workingDaysStart, workingDaysEnd, workingHoursStart, workingHoursEnd

Optional: latitude (90…90), longitude (180…180), nfcId (max 64), divisionId, status

Weekdays: mondaysunday. Hours: HH:mm 24-hour (08:00). Address max 255.

Update may set latitude, longitude, nfcId, divisionId to null to clear.

List filters: code, name, phone, address, divisionId, nfcId, status, workingDaysStart, workingDaysEnd, workingHoursStart, workingHoursEnd, search (code/name/address).

Response:

{
  "id": "uuid",
  "code": "JKT_01",
  "name": "Jakarta Pusat",
  "phone": "+6281234567890",
  "address": "Jl Sudirman No 1",
  "latitude": -6.2,
  "longitude": 106.8,
  "workingDaysStart": "monday",
  "workingDaysEnd": "friday",
  "workingHoursStart": "08:00",
  "workingHoursEnd": "17:00",
  "nfcId": "NFC-001",
  "divisionId": "uuid",
  "status": "active",
  "createdAt": 1710000000000,
  "updatedAt": 1710000000000,
  "createdBy": "uuid",
  "updatedBy": "uuid"
}

latitude / longitude / nfcId / divisionId may be null.

CSV required: code, name, phone, address, workingDaysStart, workingDaysEnd, workingHoursStart, workingHoursEnd. Optional: latitude, longitude, nfcId, divisionId, status.


12. Customers

Key: CONFIGURATION.CUSTOMER. Standard CRUD + import, plus nested contacts.

Create (required): code, name, phone, address

Optional: latitude, longitude, nfcId, status, contacts[]

Contact create: name required; jobTitle?, phone?, mobilePhone?, notes? (notes max 255).

List omits contacts. Detail / create / update / contact mutations include contacts.

Nested contacts (privilege = customer update)

Method Path Status Body Response
POST /customers/:id/contacts 200 create contact full CustomerDto
PATCH /customers/:id/contacts/:contactId 200 partial contact full CustomerDto
DELETE /customers/:id/contacts/:contactId 204 empty

List filters: code, name, phone, address, nfcId, status, search (code/name/address).

CSV: code, name, phone, address (optional lat/long/nfc/status). No contacts in CSV.


13. Employees

Key: CONFIGURATION.EMPLOYEE. Standard CRUD + import.

Create: { code, name, phone, position, status? }

position: sales | driver | crew

List filters: code, name, phone, position, status, search (code/name).

DTO: { id, code, name, phone, position, status, createdAt, updatedAt, createdBy, updatedBy }

CSV: code, name, phone, position.


14. Products

Key: CONFIGURATION.PRODUCT. Standard CRUD + import.

Create: { code, name, unit?, price?, brand?, status? }

price is a decimal string ("12500.0000"). unit letters/numbers, max 16. brand max 64.

List filters: code, name, unit, brand, status, search (code/name).

DTO: { id, code, name, unit, price, brand, status, …audit } with unit/price/brand nullable.

CSV: code, name (optional unit, price, brand, status).


15. Sales documents (shared)

Sales codes (optional on create; auto-generated if omitted): max 32, ^[A-Za-z0-9][A-Za-z0-9_-]*$.

Line input:

{ "productId": "uuid", "quantity": "2.0000", "price": "12500.0000" }

price optional on input; response always includes price as a decimal string.

Image input (requests, orders, payments):

{ "url": "https://cdn.example.com/a.png", "description": "optional" }

url max 2048, description max 255.

List items omit products / images / payment invoices. GET :id, POST, PATCH, PATCH status include them with generated id on each nested row.

Quantity/price/amount/balance are strings.


16. Sales requests

Key: SALES.REQUEST. Standard CRUD + import.

Create (required): date, salesPersonId, branchId, divisionId, customerId, address, products[]

Optional: code, latitude, longitude, notes (max 1024), images[], status

List filters: code, status, customerId, salesPersonId, branchId, divisionId, search

List DTO:

{
  "id": "uuid",
  "code": "SR-20260824-0001",
  "date": 1756000000000,
  "salesPersonId": "uuid",
  "branchId": "uuid",
  "divisionId": "uuid",
  "customerId": "uuid",
  "address": "Jl Sudirman 1",
  "latitude": null,
  "longitude": null,
  "notes": null,
  "status": "draft",
  "createdAt": 1756000000000,
  "updatedAt": 1756000000000,
  "createdBy": "uuid",
  "updatedBy": "uuid"
}

Detail adds:

{
  "products": [{ "id": "uuid", "productId": "uuid", "quantity": "2.0000", "price": "12500.0000" }],
  "images": [{ "id": "uuid", "url": "https://…", "description": null }]
}

Import CSV required: date, salesPersonId, branchId, divisionId, customerId, address. Products/images empty on import.


17. Sales orders

Key: SALES.ORDER. Same shape as requests, plus optional salesRequestId on create (copies missing fields from the request).

Response includes salesRequestId (string | null). Statuses: draft | processed | completed | cancelled.

List filters: code, status, customerId, salesPersonId, branchId, divisionId, search.


18. Packing slips

Key: SALES.PACKING_SLIP. Statuses: draft | processed | completed | cancelled.

Create fields are optional in JSON. If salesOrderId is set, missing date / customerId / address / products are copied from the order. Service still requires a resolved date, customer, and address.

Optional: code, salesOrderNumber, latitude, longitude, notes, status.

List filters: code, status, customerId, salesOrderId, search.

DTO: { id, code, salesOrderId, salesOrderNumber, date, customerId, address, latitude, longitude, notes, status, …audit }

Detail adds products[].

CSV required: date, customerId, address.


19. Sales invoices

Key: SALES.INVOICE. Statuses: draft | processed | partial | completed | cancelled.

Create fields optional. May derive from salesOrderId and/or packingSlipId.

List filters: code, status, customerId, salesPersonId, branchId, divisionId, salesOrderId, packingSlipId, search.

DTO extras vs order: salesOrderCode, packingSlipId, packingSlipCode, balance (decimal string). Detail adds products[].

CSV required: date, salesPersonId, branchId, divisionId, customerId.


20. Sales payments

Key: SALES.PAYMENT. Statuses: draft | pending | approved | rejected.

Create (required): date, invoices (non-empty allocations)

{
  "date": "2026-08-24T10:00:00+07:00",
  "invoices": [{ "invoiceId": "uuid", "amount": "10000.0000" }],
  "notes": null,
  "images": [],
  "status": "draft"
}

List filters: code, status, search.

List DTO: { id, code, date, notes, status, …audit }

Detail adds invoices[{ id, invoiceId, amount }] and images[{ id, url, description }].

CSV required: date (allocations empty on import).


21. Company settings

Key: CONFIGURATION.SETTING. Singleton — no list/CRUD.

Method Path Action Notes
GET /settings view 404 { "message": "Settings not configured" } if never patched
PATCH /settings update upserts

Patch body: { "cycleStartDate": "2026-01-05" } (YYYY-MM-DD only).

Response:

{
  "id": "uuid",
  "cycleStartDate": 1767546000000,
  "status": "active",
  "createdAt": 1710000000000,
  "updatedAt": 1710000000000,
  "createdBy": "uuid",
  "updatedBy": "uuid"
}

cycleStartDate is unix ms for start of that calendar day in DEFAULT_TIMEZONE. Plan generate rejects ranges that start before this date.


22. Cycles

Privilege: RequireFieldPrivilege('cycle', action)SALES.CYCLE or LOGISTICS.CYCLE.

No hard delete: DELETE / bulk-delete archive. Has CSV import. Standard list/detail/create/update/status otherwise.

Create:

{
  "employeeId": "uuid",
  "purpose": "sales",
  "cycleNumber": 1,
  "weekdays": {
    "monday": {
      "startBranchId": "uuid",
      "endBranchId": "uuid",
      "customerIds": ["uuid"]
    }
  },
  "status": "draft"
}
  • purpose: sales | logistics
  • cycleNumber: integer ≥ 1
  • weekdays: object keyed by mondaysunday. Omitted days are days off. Any present day must include startBranchId, endBranchId, and a non-empty customerIds. Incomplete day → 400 Weekday must be complete.
  • Unique per (employeeId, purpose, cycleNumber)409 Cycle already exists for this employee
  • All referenced branches/customers must have usable lat/lng or route build fails (400 Weekday route is incomplete)

Response weekdays is an array, not an object:

{
  "id": "uuid",
  "employeeId": "uuid",
  "purpose": "sales",
  "cycleNumber": 1,
  "weekdays": [
    {
      "id": "uuid",
      "weekday": "monday",
      "startBranchId": "uuid",
      "endBranchId": "uuid",
      "routeGeometry": {
        "type": "LineString",
        "coordinates": [
          [106.8456, -6.2088],
          [107.0, -6.3]
        ]
      },
      "destinations": [{ "id": "uuid", "customerId": "uuid", "sortOrder": 0 }]
    }
  ],
  "status": "draft",
  "createdAt": 1710000000000,
  "updatedAt": 1710000000000,
  "createdBy": "uuid",
  "updatedBy": "uuid"
}

routeGeometry.coordinates are GeoJSON [longitude, latitude] pairs.

List filters: employeeId, purpose, cycleNumber, status, search. Without purpose, results are limited to purposes the caller can view.

CSV required: employeeCode, purpose, cycleNumber, weekday, customerCodes, startBranchCode, endBranchCode. Multiple rows with the same employee/purpose/cycleNumber merge weekdays.


23. Plans

Privilege: RequireFieldPrivilege('plan', action)SALES.PLAN or LOGISTICS.PLAN.

No CSV import. Delete archives. Extra routes: generate, add/remove destinations.

Method Path Action Status Body / response
GET /plans view 200 paginated
GET /plans/:id view 200 PlanDto
POST /plans/generate create 200 { employeeId, purpose, from, to }{ created, skipped }
POST /plans create 201 see below
PATCH /plans/:id update 200 no status
PATCH /plans/:id/status update 200 { status }
POST /plans/:id/destinations update 200 { customerId, afterDestinationId? }PlanDto
DELETE /plans/:id/destinations/:destinationId update 200 PlanDto (not 204)
DELETE /plans/:id delete 204 archives
POST /plans/bulk-delete delete 200 { deleted }
POST /plans/bulk-status update 200 { updated }

Register static paths (generate, bulk-delete, bulk-status) before :id.

Create

{
  "employeeId": "uuid",
  "purpose": "sales",
  "date": "2026-01-12",
  "startBranchId": "uuid",
  "endBranchId": "uuid",
  "customerIds": ["uuid"],
  "invoiceIds": ["uuid"],
  "packingSlipIds": [],
  "status": "draft"
}

date is YYYY-MM-DD. customerIds must be non-empty. Unique per employee+date+purpose → 409 Plan already exists for this employee.

  • Sales + packingSlipIds400 Sales plans cannot include packing slips
  • Logistics + invoiceIds400 Logistics plans cannot include invoices

Response date is unix ms. Includes routeGeometry, destinations, invoiceIds, packingSlipIds.

Generate

{
  "employeeId": "uuid",
  "purpose": "sales",
  "from": "2026-01-12",
  "to": "2026-01-25"
}

Materializes active plans from the employees cycles over [from, to] (inclusive calendar days).

Requires settings (GET /settings). Errors:

  • 404 Settings not configured
  • 400 Invalid date range (to before from)
  • 400 Date is before the cycle start date
  • 400 User has no sales cycle / User has no logistics cycle

skipped counts days that already have a plan or have no matching weekday template.

Generated plans are created with status active.

Destinations

POST /plans/:id/destinations:

  • customerId required
  • afterDestinationId optional: insert after that stop; omit to append
  • Duplicate customer → 409 Customer is already on this plan
  • Unknown afterDestinationId400 Destination is not on this plan

DELETE …/destinations/:destinationId:

  • Last remaining stop → 400 A live plan must keep at least one destination
  • Unknown id → 404 Destination not found

Both return the full plan (route geometry is recomputed).

List filters: employeeId, purpose, date (YYYY-MM-DD), status, search.


24. CSV import

POST /{resource}/importmultipart/form-data, field file.

  • Max 1_048_576 bytes
  • Must be CSV mime or filename ending .csv
  • Fail the whole batch on row errors
  • Optional status column; omitted → draft
  • createdBy / updatedBy = current user
  • Plans have no import

25. Suggested frontend flows

Session

  1. POST /auth/login → store both tokens.
  2. Attach access token to every request.
  3. On 401, POST /auth/refresh; if that fails, logout.
  4. Logout: POST /auth/revoke then drop tokens.
  5. After login, GET /auth/me and cache permissions / isSuperadmin for nav and buttons.

Configuration screens

Standard list + drawer/form. Status chip uses /status and /bulk-status. Import uses the CSV headers in each section.

Sales documents

List without lines. Open detail for products / images. Creating an order from a request: POST /sales-orders with salesRequestId and any overrides. Packing slip / invoice can be seeded from parent IDs.

Field (cycles / plans)

  1. Ensure PATCH /settings has a cycleStartDate.
  2. Create cycles per employee + purpose (weekdays object in, array out).
  3. POST /plans/generate for a date range, or POST /plans for a one-off day.
  4. Edit stops with destination add/remove; do not send status on plan PATCH.
  5. Scope lists with ?purpose=sales or logistics so privilege and filters match the screen.

Privilege editor

  1. GET /privilege-keys?limit=200 for the matrix axes.
  2. GET /privileges/:id for cells (details).
  3. PATCH /privileges/:id with the full details array.
  4. Assign with PATCH /users/:id/privilege.

26. TypeScript shapes (copy)

type Uuid = string;
type UnixMs = number;
type DecimalString = string;
type CoreStatus = 'draft' | 'active' | 'archived';
type FieldPurpose = 'sales' | 'logistics';
type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
type PrivilegeAction = 'view' | 'create' | 'update' | 'delete' | 'import';

type PaginationMeta = {
  currentPage: number;
  itemCount: number;
  itemsPerPage: number;
  totalItems: number;
  totalPages: number;
};

type Paginated<T> = { data: T[]; meta: PaginationMeta };

type Audit = {
  createdAt: UnixMs;
  updatedAt: UnixMs;
  createdBy: Uuid;
  updatedBy: Uuid;
};

type TokenPair = { accessToken: string; refreshToken: string };

type Me = {
  id: Uuid;
  username: string;
  isSuperadmin: boolean;
  privilege: { id: Uuid; name: string; code: string } | null;
  permissions: Record<string, Record<PrivilegeAction, boolean>>;
};

type RouteGeometry = {
  type: 'LineString';
  coordinates: ReadonlyArray<readonly [number, number]>; // [lng, lat]
};

type HttpError = {
  statusCode: number;
  message: string | string[];
  error?: string;
  errors?: string[];
};

27. Endpoint index

Public: GET /, POST /auth/register, POST /auth/login, POST /auth/refresh, POST /auth/revoke.

Bearer: GET /auth/me, PATCH /users/:id/privilege, GET /privilege-keys, GET /settings, PATCH /settings.

CRUD families: /privileges, /divisions, /branches, /customers, /employees, /products, /sales-requests, /sales-orders, /packing-slips, /sales-invoices, /sales-payments, /cycles, /plans.

Extras: customer contacts, POST /plans/generate, plan destinations.

Live schema: GET /docs-json (non-production, or SWAGGER_ENABLED=true).