Transport
Transport is the operational screen for a route after route-picking delivery sheets exist. It creates the route header, attaches route-picking sheets, confirms that each sheet is ready, prepares the loading step, starts the trip, lets the driver process every customer stop, records shipping/COD payments and evidence, then closes the route after bank and oil actions are complete.
The Mobile app is the behavior reference. POS Terminal uses the same service contract and should keep the same business rules; differences are mainly layout and navigation.
App Entry Points
| App | Target | Notes |
|---|---|---|
| Mobile | TransportList -> TransportDetail | Stack-based list/detail screen. |
| POS Terminal | Home menu transport -> TransportDetailForm | Split-pane menu under the Home screen. |
End-to-End Workflow
Route Header
| Field / behavior | Rule |
|---|---|
| Transport date | Defaults to today when creating a new transport. |
| Branch / warehouse group | Taken from the selected branch context and sent as branchId, whGrpCode, and whGrpName. |
| Driver | Defaults from current user first/last name when creating. |
| Vehicle | Required before saving and before pulling delivery sheets. Selecting a vehicle fills the license plate and driver-related context. The picker shows the two test vehicles assigned to the selected branch above API vehicles. |
| Company | Uses the selected branch company code where available. |
| Route | Can be copied from the first selected delivery sheet if the transport is created while selecting a sheet. |
| Bank / oil flags | flagBank and flagGas are stored on the transport header and are required before finishing. |
Validation before save:
| Condition | Error behavior |
|---|---|
| Missing license plate | Blocks save/open delivery picker. |
| Missing driver name | Blocks save. |
| Missing ship date | Blocks save. |
Test vehicles in the picker
Mobile and POS Terminal share branch-scoped READY test vehicles. Each supported branch sees only its own two plates: Dummy-ACR001/002, Dummy-UBN001/002, Dummy-BRM001/002, Dummy-KKN001/002, Dummy-MDH001/002, Dummy-SRN001/002, Dummy-NMA001/002, Dummy-PLK001/002, Dummy-SSK001/002, Dummy-UDN001/002, or Dummy-ROI001/002. They are shown before real vehicles in the picker and are filtered by the same search query as real vehicles. A search that does not match a branch dummy-vehicle field does not show it; unsupported branches do not receive dummy vehicles.
Test vehicles are not valid for an operational trip. If a dummy license plate is selected, pressing Start transport is blocked with Please select a real vehicle first. The license plate remains editable until the transport has started (startedAt is set); it is then locked.
Delivery Sheet Pulling
Delivery sheets are route-picking documents created from the Shipping Route menu. The transport screen attaches them to a transport round.
Rules:
| Action | Rule |
|---|---|
| Add delivery sheet | Allowed only before the transport leaves draft. A new transport is created automatically if the user selects a sheet before saving. |
| Remove delivery sheet | Allowed only in draft, before startedAt, and only for a user who can edit the transport. See the detailed delete rules below. |
| Finalize one sheet | Marks that delivery sheet as FINISHED by calling the delivery status update. |
| Finalize all pending sheets | Loops through all sheets with empty or NULL delivery status and marks them FINISHED. The transport owner or override user code 999999 can run this action. |
| Prepare transport | Enabled only after at least one delivery sheet exists and every attached sheet is finalized. |
Removing A Delivery Sheet From Transport
Deleting here means detaching a route-picking delivery sheet from the transport round. It does not delete the original Shipping Route delivery document itself.
Rules:
| Condition | Behavior |
|---|---|
| User is not the transport owner/editor | Remove button is not shown. Existing transport edit is owner-based through userCode. |
| Transport is not draft | Remove button is hidden; handler also blocks with the message that loading/transport already started. |
startedAt exists | Terminal hides the remove button; Mobile is already blocked because the workflow is no longer draft. |
Missing transportId or deliveryId | No delete request is sent. |
| User confirms delete | Calls deleteTransportDelivery and refreshes the transport detail. |
| Delivery sheet was already finalized | It can still be removed only while the transport is still draft and editable. After prepare/start, it is locked. |
Status Model
| Status | Meaning | Main allowed actions |
|---|---|---|
| New | Unsaved route header. | Save or select a delivery sheet, which creates the header first. |
| Draft | Transport exists but loading has not started. | Edit header, add/remove delivery sheets, finalize sheets, prepare. |
| Preparing | Goods are being loaded onto the vehicle. | Start transport or unlock back to draft by authorized departments. |
| In progress | startedAt exists. | Process customer stops, add shipping rows before payment, retry integration rows, handle oil/bank, finish. |
| Finished | finishedAt exists. | Read-only operational history. |
| Canceled | Canceled transport. | Read-only. |
Prepare And Unlock
| Action | Required conditions | Result |
|---|---|---|
| Prepare transport | Transport owner or override user code 999999; status is draft; all delivery sheets are finalized; not canceled. Before confirmation, the app checks for eligible, unassigned delivery sheets in the transport route. | Calls POST /transports/{id}/prepare, moves workflow to loading/preparing. |
| Unlock transport | Transport is preparing and not canceled; user department is in 8, 12, 22, 28, 29, 4, 2; note is required. | Calls /cancel-prepare with note and returns the route to an editable draft-style state. |
Unlock Flow
Unlock is used when a transport has already entered the loading/preparing step, but the team needs to correct the route before starting the trip. It does not cancel the transport; it cancels the prepare/loading state.
Unlock rules:
| Condition | Behavior |
|---|---|
Transport is not PREPARING | Unlock action is not available. |
| Transport is canceled | Unlock action is not available. |
| User department is not allowed | Shows "no unlock permission" error and does not open the note modal. |
| Note is blank | Blocks submit and asks for a note. |
| Submit succeeds | Calls unprepareTransport(transportId, { note }), clears the modal state, refreshes detail, and lets the owner edit the route again. |
| Submit fails | Keeps the route unchanged and shows the API error. |
Start Transport
Starting transport is separate from preparing/loading. It records mileage and creates shipping/COD rows for each customer when enabled.
In PRD, before the starting-mileage validation, the app calls GET https://api-checklistv2.boonsiri.co.th/api/v1/transportation with the logged-in user's userCode, status=false, and the configured x-api-key. An empty data array permits the start flow to continue. A non-empty data array stops the flow and shows: ยังมีงานที่ไม่เสร็จบนระบบ Check List กรุณาไปดำเนินการให้เสร็จสิ้นทั้งหมดก่อน. This check is not called in UAT. If the Checklist request fails or has an invalid response shape, the start flow is stopped and the API error is shown.
Start payload:
| Field | Source |
|---|---|
transportId | Current transport. |
licensePlate | Transport header. |
mileage | Starting mileage modal. |
userCode | Transport owner/current user. |
shippings | One row per customer with customerCode, customer lat/lng, and shippingAmount. Mobile sends amount only when COD is enabled; POS Terminal currently sends the calculated shipping amount. |
If SAP returns row errors, the transport still starts, and the user gets an error list so the failed shipping rows can be retried later.
Customer Stop Workflow
Customer stops are loaded from the attached delivery sheets through /transports/{id}/customers. The stop list shows payment totals, delivery progress, and special footer actions for oil and bank after the trip has started. A customer card remains the draft light-blue color until its status is FINISHED; only then does its color reflect the recorded payment type.
Card Colors
| Card | Condition | Color |
|---|---|---|
| Customer | status is not FINISHED | Draft light blue, regardless of payment type. |
| Customer | status is FINISHED, paymentType: CASH | Cash green. |
| Customer | status is FINISHED, paymentType: TRANSFER | Transfer purple. |
| Customer | status is FINISHED, paymentType: BILL_PAYMENT | Bill-payment yellow. |
| Customer | status is FINISHED, paymentType: OTHER | Other-payment red/pink. |
| Customer | status is FINISHED, paymentType: SHIPPING | Shipping purple. |
| Oil / Bank footer card | Respective flagGas / flagBank is 0 | Draft light blue. |
| Oil / Bank footer card | Respective flagGas / flagBank is 1 | Confirmed green. |
Customer Card Amount Formula
One card represents one delivery stop, and can therefore contain several invoices even for the same customerCode. The client first derives the product amount from all invoices assigned to that stop:
net invoice amount = amount - discount - useCreditNote
reference product amount = sum of net invoice amounts at the stop
shipping/fee amount = sum of shippingAmount across shipping-invoice rows
(or customer.shippingAmount when no shipping invoice exists)
card total = reference product amount + shipping amount
Every shipping-invoice row on the customer card uses its own shippingAmount for the displayed amount. docType: FEE is labelled Fee and docType: SHIPPING is labelled Shipping; both are displayed even when COD is disabled. If an older API response omits shippingAmount, the client falls back to that row's amount.
When the API provides grandAmount, the payable amount and the amount passed to the payment flow use grandAmount + shipping amount instead. grandAmount: 0 is a valid, deliberate total used by the zero-value delivery-confirmation/bill-payment flow; it is not a missing value.
The values at the right of the card mean:
| Visible value | Calculation / meaning |
|---|---|
| Muted amount (when shown) | Card total: net invoice amounts plus shipping. |
Primary amount for CASH, TRANSFER, BILL_PAYMENT, OTHER | Actual collected amount, cash + transfer, capped at the card/payable total. |
| Primary amount for other states | Payable amount: grandAmount when supplied, otherwise the reference product amount, plus shipping. |
| Amount on an invoice row | invoice.amount before its discount and useCreditNote, retained as a source-document reference. |
Consequently, invoice-row values need not add up to the card total when discounts or credit notes exist. They must not be treated as the collection amount without those deductions.
Data contract: the transport-customer endpoint should supply grandAmount explicitly whenever it is the source-system total, and distinguish an absent value from 0 so a client does not unintentionally interpret a missing product total as zero.
Available customer actions:
| Action | Behavior |
|---|---|
| Call customer | Logs the customer call if customer reference exists, then opens the phone dialer. |
| Confirm delivery / payment | Opens the payment type step. Hidden when limited access or customer is already finished. |
| Save location | Loads customer addresses and lets the user select or update GPS-related delivery location data. |
| Navigate | Opens navigation using the customer coordinates/address. |
| View products | Builds a product summary from invoices assigned to the customer stop. |
| Add shipping amount | Creates an extra transport shipping invoice for that customer. Blocked after the customer is paid/finished. |
Refreshing An Unfinished Customer Stop
When an unfinished customer card is opened, the app shows loading and requests GET /api/transports/{transportId}/customers?customerCode={customerCode} before opening the customer modal. The returned row is matched to the card by all three fields: customerCode, U_BFP_Latitude, and U_BFP_Longitude. This prevents a response for one delivery location from updating another stop that has the same customer code. The matched row's current grandAmount and paymentType update that card and are then used by the existing modal flow.
For a matched row where grandAmount is 0:
Current paymentType | Step-2 choice | Submitted paymentType |
|---|---|---|
Empty / null | Only Confirm delivery | SHIPPING |
BILL_PAYMENT | Only Confirm delivery | BILL_PAYMENT |
All other payment cases retain the existing choices and validation. A zero-value BILL_PAYMENT confirmation is allowed for this specific grandAmount: 0 case.
Customer payment access is normally restricted to the transport owner. The debug user code 999999 may also open an unpaid customer card and complete its payment flow after the transport has started; this exception does not grant permission to edit the transport header, delivery sheets, or route actions.
| Generate QR | Generates QR for product amount and, when present, shipping amount. Limited access can still generate QR for transfer-paid stops. |
Shipping Amount Rows
| Action | Rule |
|---|---|
| Auto shipping rows at start | Built from each customer and sent with startTransport. |
| Add shipping amount manually | Requires customer code, positive integer amount, lat/lng, user code, and user name. |
| Delete shipping row | Allowed only before the customer is paid/finished. |
| Retry shipping to SAP | Calls /shipping/{shippingInvoiceId}/retry for failed B1 posting. |
| Retry incoming | Service exists as /retry-incoming; use for incoming-side integration retry where surfaced by UI/API. |
| Retry cancel to SAP | Calls /shipping/{shippingInvoiceId}/retry-cancel for failed cancellation posting. |
Payment And Evidence
Payment submits to /transports/{id}/payment. It updates the customer stop state; the old explicit customer-status update is no longer used by the screen.
Payment types:
| Type | Input behavior |
|---|---|
CASH | Cash amount must be positive. The screen auto-fills/locks the cash field for normal cash collection. Requires drop-off fee check. |
TRANSFER | After generating QR, the user must select the transfer-collection outcome described below. |
BILL_PAYMENT | Same validation pattern as transfer, with bill payment slip. |
OTHER | Requires notes. Used for partial receive, not receive, split payment, mixed cash/transfer, or exceptional cases. Requires drop-off fee check unless configured off. |
SHIPPING | Used when payable amount is zero and the user only confirms delivery. |
QR Transfer Collection Choice
After choosing TRANSFER / QR Code, the screen requires one of these radio choices before submit:
| Choice | Screen behavior | Submitted payment data |
|---|---|---|
| Customer already transferred to the delivery staff | Shows an editable transfer-amount input. The amount must be greater than zero and the normal TRANSFER flow is used. | paymentType: TRANSFER with the entered transfer amount. |
| Customer will transfer to Sales later | Hides the transfer-amount input and uses the full payable amount as its transfer value. | paymentType: OTHER, cash: 0, transfer equal to the full payable amount, and a system note: ลูกค้าจะโอนชำระกับฝ่ายขายเองภายหลัง. |
If neither choice is selected, the screen blocks submit and asks the user to choose the transfer payment status. Every transfer choice requires a positive transfer amount.
Required evidence and validations:
| Requirement | Rule |
|---|---|
| Delivery photo | Required for every payment/delivery confirmation. |
| Shipping slip | Required when there is a shipping amount and the product transfer/bill slip exists. |
| Signature | Required unless payment type is TRANSFER or BILL_PAYMENT. |
| GPS | Current device coordinates are captured at submit time. Customer lat/lng is also sent in the payload. |
| Drop-off fee | Required check for CASH and OTHER; added to cash when fee config is enabled. |
| Other payment amount | Cash + transfer cannot exceed payable amount. If not marked partial receive, cash + transfer must cover the payable amount. |
| Not receive | For OTHER, not receive clears cash/transfer to zero and still records details. |
| Sales-later transfer | The special QR choice submits OTHER with cash at zero, transfer equal to the full payable amount, and the Sales-later system note. |
Evidence Upload
The delivery photo, transfer/bill-payment slip, shipping slip, and customer signature use the same endpoint:
POST /api/transports/{transportId}/files
Content-Type: multipart/form-data
The request body is multipart/form-data and includes the customer context currently open in the customer modal:
| Field | Type | Source / meaning |
|---|---|---|
files | File | The evidence file being attached; the current UI uploads one file/image at a time. |
whGrpCode | string | Warehouse group from the transport detail. |
customerCode | string | customerCode of the selected customer stop. |
U_BFP_Latitude | string | Latitude of the selected customer stop. |
U_BFP_Longitude | string | Longitude of the selected customer stop. |
transportId is in the URL, not the body. The response returns the uploaded file path, which is then placed into the payment payload field that matches the evidence type: photo, slipPath, shippingSlipPath, or signature.
customerCode, U_BFP_Latitude, and U_BFP_Longitude must all exist and be non-empty for every upload. If any is missing, the app shows:
ไม่พบ customerCode หรือพิกัดลูกค้าสำหรับอัปโหลดไฟล์
The upload is stopped and the file API is not called. A missing whGrpCode is also blocked with a missing-warehouse upload alert.
Payment payload fields:
| Field | Meaning |
|---|---|
customerCode | Customer stop being processed. |
paymentType | One of cash, transfer, bill payment, other, shipping confirmation. |
amount | Payable amount including shipping amount. |
cash, transfer | Resolved collection amounts; cash can include drop-off fee. |
notes | User notes plus system summary for OTHER. |
U_BFP_Latitude, U_BFP_Longitude | Customer destination coordinates. |
currentLatitude, currentLongitude | Device coordinates at payment time. |
signature, slipPath, shippingSlipPath, photo | Uploaded file paths. |
Oil And Bank Flags
After the trip starts, the stop list adds two special action items: oil and bank.
| Action | Behavior |
|---|---|
| Oil | User can open Expense Bill prefilled from the transport or mark "no oil". Either path sets flagGas = 1. |
| Bank | User can open Deposit prefilled from the transport cash total or mark "no bank". Either path sets flagBank = 1. In the current Mobile modal, the deposit button is hidden and only "no bank" is active; POS Terminal still has the context builder for deposit. |
| Finish dependency | Finish transport is blocked until both flagGas and flagBank are 1. |
Finish Transport
Finish payload contains transportId, licensePlate, and ending mileage.
Permissions And Guards
| Topic | Mobile | POS Terminal |
|---|---|---|
| Menu visibility | getTransportAccess().canOpenTransportMenu allows deptCode -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 12, 13, 16, 14, 22, 27, 28, 30. | MENU_TRANSPORT uses the same department set. |
| Create transport | Mobile create button allows deptCode -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 13, 16, 14, 30, or full non-main warehouse access. | canCreateTransport uses the create dept set plus full non-main warehouse access. |
| Branch rule | Uses selected branch context. Non-main behavior depends on shared warehouse access helpers. | Menu config sets requiresMainBranch = true for transport; create helper still supports full non-main access where invoked. |
| Edit route | Existing transport can be edited only by the transport owner (userCode). | |
| Delete from list | Draft transport only, owner only, and create permission/full warehouse access required. | |
| Unlock loading | Preparing transport only; deptCode 8, 12, 22, 28, 29, 4, 2; note required. | |
| Customer action after finish | Finished/canceled transport is read-only for operational actions. |
Mobile vs POS Terminal
| Area | Mobile | POS Terminal |
|---|---|---|
| Layout | Stack screen with list, detail, modals, and bottom action bar. | Split-pane Home screen detail form. |
| Core API | apps/mobile/src/services/transport.ts. | apps/pos-terminal/src/services/transport.ts. |
| Core behavior | Reference behavior for guards and customer workflow. | Should mirror Mobile business rules. |
| Start shipping amount | Sends shipping amount when COD is enabled; otherwise 0. | Sends the calculated shipping amount. Keep this difference intentional or align with Mobile if COD gating is required. |
| Error display | Mobile shows a SAP error modal after start. | Terminal shows an alert with formatted failed rows. |
| Bank modal | Mobile currently hides the deposit action in the bank modal and allows "no bank". | Terminal keeps deposit context and skip flag action. |
Developer Handoff Map
| Area | Code |
|---|---|
| Mobile list | apps/mobile/src/screens/transportStack/transportListScreen/index.tsx |
| Mobile detail | apps/mobile/src/screens/transportStack/transportDetailScreen/index.tsx |
| Mobile customer modal | apps/mobile/src/screens/transportStack/transportDetailScreen/TransportCustomerActionModal.tsx |
| Mobile helpers | apps/mobile/src/screens/transportStack/transportDetailScreen/helpers.ts |
| POS Terminal detail | apps/pos-terminal/src/screens/HomeScreen/TransportDetailForm.tsx |
| POS Terminal helpers | apps/pos-terminal/src/screens/HomeScreen/TransportDetailHelpers.ts |
| Transport services | apps/mobile/src/services/transport.ts, apps/pos-terminal/src/services/transport.ts |
| Menu/permission config | apps/mobile/src/utils/transport.ts, apps/pos-terminal/src/screens/HomeScreen/config.ts |
| Shipping Route source docs | Shipping Route |
| Related operational docs | Deposit, Fuel Bill / Expense Bill, Vehicles |