- Refactored privilege keys in `api.md` to use a more structured naming convention, aligning with the new `Group.Parent.Module` format. - Updated various module configurations across the application to reflect the new privilege key structure, ensuring consistent access control. - Removed deprecated keys and streamlined the privilege management process, enhancing clarity and maintainability. - Added new tests for privilege key parsing and grouping functionalities to ensure reliability and correctness. These changes significantly improve the application's privilege management system, providing a clearer structure for access control and enhancing overall security.
32 KiB
TrackGo HTTP API — frontend reference
Static contract for a frontend agent or UI. Source of truth is this backend’s 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)
- Send
Authorization: Bearer <accessToken>on every route exceptGET /andPOST /auth/register|login|refresh|revoke. - List endpoints return
{ data, meta }. Detail, create, update, status, import, and bulk return the bare DTO or a small object.DELETEis 204 with an empty body (except nested plan destinations, which return the plan). - Request dates as ISO datetime or
YYYY-MM-DD. Response timestamps are unix milliseconds (number). - Phones are E.164 (
+6281234567890). Money, quantities, and prices are decimal strings (for example"12500.0000"), never floats. - Never send
statusonPATCH /:id. UsePATCH /:id/statusorPOST /bulk-status. - Unknown JSON fields are rejected (
400). Do not send snake_case aliases. - Gate UI with
GET /auth/me→isSuperadminorpermissions[KEY][action]. Superadmin bypasses the matrix. - Cycles and plans are purpose-scoped (
sales|logistics) and use different privilege keys. Passpurposeon writes (body) and preferably on lists (query). - Cycles and plans
DELETE/bulk-deletearchive (status: "archived"); they do not hard-delete. - Default new-record status is
draftwhen 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: 3–32 chars,^[a-zA-Z0-9_]+$, stored lowercasedpassword: 8–72 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 |
1–200 | 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 calendarYYYY-MM-DD. Naive ISO (noZ/offset) is interpreted inDEFAULT_TIMEZONE(defaultGMT+7). - Out: unix milliseconds UTC for
date,createdAt,updatedAt,cycleStartDate, plandate. - 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.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 |
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/privilege — 200
{ "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: monday … sunday. 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|logisticscycleNumber: integer ≥ 1weekdays: object keyed bymonday…sunday. Omitted days are days off. Any present day must includestartBranchId,endBranchId, and a non-emptycustomerIds. 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 +
packingSlipIds→400 Sales plans cannot include packing slips - Logistics +
invoiceIds→400 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 employee’s cycles over [from, to] (inclusive calendar days).
Requires settings (GET /settings). Errors:
404 Settings not configured400 Invalid date range(tobeforefrom)400 Date is before the cycle start date400 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:
customerIdrequiredafterDestinationIdoptional: insert after that stop; omit to append- Duplicate customer →
409 Customer is already on this plan - Unknown
afterDestinationId→400 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}/import — multipart/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
statuscolumn; omitted →draft createdBy/updatedBy= current user- Plans have no import
25. Suggested frontend flows
Session
POST /auth/login→ store both tokens.- Attach access token to every request.
- On
401,POST /auth/refresh; if that fails, logout. - Logout:
POST /auth/revokethen drop tokens. - After login,
GET /auth/meand cachepermissions/isSuperadminfor 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)
- Ensure
PATCH /settingshas acycleStartDate. - Create cycles per employee + purpose (
weekdaysobject in, array out). POST /plans/generatefor a date range, orPOST /plansfor a one-off day.- Edit stops with destination add/remove; do not send
statuson plan PATCH. - Scope lists with
?purpose=salesorlogisticsso privilege and filters match the screen.
Privilege editor
GET /privilege-keys?limit=200for the matrix axes.GET /privileges/:idfor cells (details).PATCH /privileges/:idwith the fulldetailsarray.- 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).