- Enhanced readability by restructuring code formatting in various components, including API documentation, user management, and sales modules. - Standardized table and object definitions for better clarity in API documentation. - Improved layout and indentation in React components for better maintainability. - Updated unit tests to reflect changes in formatting and ensure consistency. These changes enhance the overall code quality and maintainability of the project, making it easier for developers to navigate and understand the codebase.
31 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 PRIVILEGES view):
| code | label |
|---|---|
PRIVILEGES |
Privileges |
USERS |
Users |
CONFIGURATION.DIVISION |
Divisions |
CONFIGURATION.BRANCH |
Branches |
CONFIGURATION.CUSTOMER |
Customers |
CONFIGURATION.EMPLOYEE |
Employees |
CONFIGURATION.PRODUCT |
Products |
SALES.REQUEST |
Sales requests |
SALES.ORDER |
Sales orders |
SALES.PACKING_SLIP |
Packing slips |
SALES.INVOICE |
Sales invoices |
SALES.PAYMENT |
Sales payments |
CONFIGURATION.SETTING |
Company settings |
SALES.CYCLE |
Sales cycles |
SALES.PLAN |
Sales plans |
LOGISTICS.CYCLE |
Logistics cycles |
LOGISTICS.PLAN |
Logistics plans |
Field purpose
Cycles and plans do not use a single key. Privilege is resolved from purpose:
| purpose | cycle key | plan key |
|---|---|---|
sales |
SALES.CYCLE |
SALES.PLAN |
logistics |
LOGISTICS.CYCLE |
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).