Customers
Customers is the SAP customer lookup and operational launcher. Users search branch-scoped customers, inspect profile/address/credit/point data, call the customer, start sales documents, request status changes, add delivery/billing addresses, mark default shipping addresses, and request GPS-coordinate updates. Editing customer master data itself is handed off to Customer Management.
App Entry Points
| App | Target |
|---|---|
| Mobile | CustomerList mode default, booking, borrow, or request -> CustomerDetail |
| POS Terminal | customer split menu -> CustomerDetailForm |
End-To-End Workflow
List Screen Controls
| Control | Mobile Behavior | POS Terminal Behavior |
|---|---|---|
| Back | navigation.goBack() from CustomerManagementListHeader. | Back is handled by the HomeScreen menu/pane shell, not by the customer list component itself. |
| Search input | Updates search; submit calls getCustomerSapList({ page: 1, search, shippingType, routeCode, isRequest }). | Updates shared listSearch; submit calls loadCustomerList({ page: 1, search, shippingType, routeCode }). |
| Clear search | Clears the local search value. A new API fetch happens on the next submit/refresh. | Clears shared list search through the split-pane controls. |
| Route filter | Opens a route selection modal. Selecting a route reloads page 1 with routeCode. | Opens route selector in ListPane; selecting a route clears selected detail and reloads page 1 with routeCode. |
| Shipping type tabs | สายส่ง sends shippingType: "1"; หน้าร้าน sends shippingType: "2"; tapping the active tab clears the filter. | Same behavior through customerShippingType; selecting a tab reloads page 1 and clears the selected detail. |
| Pull refresh | Reloads page 1 with current filters. | Reloads page 1 with current filters. |
| Infinite scroll | onEndReached loads the next page while hasMore is true. | Split list onLoadMore calls loadCustomerList() with customerPageRef.current. |
| Row tap | Opens CustomerDetail with customerId, mode, and origin. Missing customerId shows an error. | Selects split-list row and loads getCustomerSplitDetail(item) into the right pane. |
| Row swipe primary action | Hidden in request mode. Otherwise launches the mode action: sales order, booking price, or borrow. Sales-order mode checks blocked reason and existing order for the same branch/day. | Not implemented as row swipe for customer. Create-document actions are inside the detail pane. |
| Header menu: add customer | Visible only when canShowCreateCustomerMenu; opens CustomerManagementEdit with empty draft. | Customer creation is handled through Customer Management menu, not the customer split list header. |
Detail Screen Buttons And Actions
| Button / Action | Mobile | POS Terminal |
|---|---|---|
| Back | Closes address form if it is open; otherwise navigation.goBack(). | Detail pane close comes from the shared pane header. |
| Pull refresh | Reloads /customers/:customerId detail, addresses, and branches. | Refresh detail is called from HomeScreen via onRefreshCustomerDetail. |
| Phone chips / phone buttons | Calls callCustomer(customerId) to record /customers/:id/re-call, then opens tel:{phone}. | Same service call and tel: open; also validates customer ID first. |
| Create sales order | Checks create permission and getSalesOrderBlockedReason(). If an existing same-day branch order exists, asks whether to open the existing order or create a new one. | Checks blocked reason, then HomeScreen either prompts/opens sales-order draft or switches to sales-order detail. |
| Booking price | Checks create permission and blocked reason, then opens BookingPriceDetail with bookingPriceId: null and customerId. | Builds booking-price draft detail and switches active menu to bookingPrice. |
| Borrow fish | Checks create permission and blocked reason, then opens BorrowDetail with borrowId: null and customerId. | Builds borrow-fish draft detail and switches active menu to borrowFish. |
| Edit customer | Opens CustomerManagementEdit with the current customerId; guarded by canEditCustomerFromDetail. | Builds a customer-management edit draft in HomeScreen and switches to the customer-management form. |
| Request close status | Visible when status is ACTIVE and user can toggle customer status. Confirms, then calls updateCustomerStatus(customerCode, { status: "INACTIVE" }). | Same request with confirmation inside CustomerDetailForm. |
| Request open status | Visible when status is INACTIVE and user can toggle customer status. Confirms, then calls updateCustomerStatus(customerCode, { status: "ACTIVE" }). | Same request with confirmation inside CustomerDetailForm. |
| Add address | Opens address form if canManageCustomerAddresses. | Opens address modal if canManageAddresses(user). |
| Approve request | Mobile request mode only. Validates customer data with validateCustomerBeforeApproval({ customer, addresses, branches }), then calls approveCustomerRequest(customerCode). | Approval for status requests is handled by the separate Customer Activation menu, not normal Customer detail. |
| Reject request | Mobile request mode only. Calls rejectCustomerRequest(customerCode). | Handled by Customer Activation menu. |
| Back button at bottom | Returns to previous screen. | Not applicable; pane close/menu selection handles navigation. |
Address Actions
| Button / Action | Behavior |
|---|---|
| Open map | Requires address-management permission and existing latitude/longitude. Opens Google Maps search URL with U_BFP_Latitude,U_BFP_Longitude. |
| Set default address | Only for ShipTo addresses. Skips if already default. Requires customerCode and addressId, confirms, then calls setCustomerDefaultAddress(customerCode, addressId) and reloads detail. |
| Pin / request GPS update | Requires address-management permission and addressId. Opens GPS request form with current coordinates and optional note. Submit calls requestCustomerAddressGpsUpdate({ addressId, U_BFP_Latitude, U_BFP_Longitude, note }); approval happens in Customer Address Management. |
| Add address: address type | BillTo means billing address; ShipTo means delivery address. New address defaults to ShipTo. |
| Legal-entity billing branch | Mobile supports head-office/branch handling for BillTo: head office defaults to branch code 00000 and branch name สำนักงานใหญ่; branch entries require both code and name when provided. Terminal normalizes the same payload behavior. |
| Search address | Opens Thai address lookup (dbAddress) and fills tambon/amphur/province/zip/block fields. |
| Use current location | Requests location permission and fills latitude/longitude from the device GPS. |
| Choose from map | Mobile uses MapPickerModal; Terminal uses an embedded Google Maps WebView. The selected coordinate fills latitude/longitude. |
| Route picker | Required when customer shippingType === "1" and the new address is ShipTo; route is saved as U_ISS_RouteCode and U_ISS_RouteName. |
| Save address | Validates permission, customerCode, address type, required address fields, legal-entity branch rules, and route requirement for delivery customers. Then calls createCustomerAddressByPass(customerCode, payload), reloads detail, and closes the form. |
| Cancel / close | Closes the modal/form and resets unsaved address state. |
Add Address Form
| Field / Button | Mobile | POS Terminal |
|---|---|---|
| Add address entry | Button label เพิ่มที่อยู่ใหม่; calls handleOpenAddAddress(). | Action button label เพิ่มที่อยู่; calls handleOpenAddAddress(). |
| Permission guard | Uses access.canManageCustomerAddresses; otherwise shows คุณไม่มีสิทธิ์เพิ่มที่อยู่. | Uses canManageAddresses(user); otherwise shows the same permission error. |
| Initial state | Opens full-screen address form with addressType: "ShipTo" and clears address search. | Opens modal with addressType: "ShipTo" and clears address search. |
| Address type | Radio buttons: ที่อยู่ในการเปิดบิล -> BillTo, ที่อยู่สำหรับจัดส่ง -> ShipTo. | Buttons: เปิดบิล -> BillTo, จัดส่ง -> ShipTo. |
| Legal entity branch | For legal-entity BillTo, Mobile shows head-office/branch choices. Head office sets branchCode: "00000", branchName: "สำนักงานใหญ่". Branch mode requires both branch code and branch name if either is entered. | Terminal normalizes the payload the same way: non-legal entities and ShipTo clear branch fields; legal-entity BillTo defaults empty branch to head office. |
| Address name | Required. Saved as addressName. | |
| Street | Required. Saved as street. | |
| Extra note / landmark | Optional. Saved as checkAddressName. | |
| Search Thai address | Opens lookup from dbAddress; selecting an item fills block, city, zipCode, U_BFP_Amphur, and U_BFP_Tambon. | |
| Tambon / Amphur / Province / Zip | Required after address lookup or manual input. Mobile renders these as disabled fields after lookup; Terminal fields are editable text fields. | |
| GPS from device | Button ดึงพิกัดจากอุปกรณ์ / ดึงพิกัด; requests location permission and fills U_BFP_Latitude, U_BFP_Longitude. | |
| Map picker | Button เลือกจากแผนที่; Mobile opens MapPickerModal, Terminal opens Google Maps WebView. Confirming writes selected latitude/longitude into the form. | |
| Delivery route | Required only when customer shippingType === "1" and address type is ShipTo. Saves selected route as U_ISS_RouteCode and U_ISS_RouteName. | |
| Save | Mobile button label บันทึก; Terminal button label บันทึกที่อยู่; both call createCustomerAddressByPass(customerCode, payload). | |
| Cancel | Mobile button label กลับ; Terminal button label ยกเลิก; both close the form/modal and reset unsaved address state. |
Save Address Validation
| Check | Failure Behavior |
|---|---|
| Address-management permission | Shows คุณไม่มีสิทธิ์เพิ่มที่อยู่ and does not submit. |
Missing customerCode | Shows ไม่พบรหัสลูกค้า. |
| Missing address type | Shows กรุณาเลือกประเภทที่อยู่. |
| Missing required fields | Shows กรุณากรอกข้อมูลที่อยู่ให้ครบ: ...; required fields are address name, street, tambon, amphur, province, and zip code. |
| Legal-entity BillTo branch is partially filled | Mobile requires either head office or both branch code and branch name. |
| Delivery customer ShipTo without route | Shows กรุณาเลือกสายส่งสำหรับที่อยู่นี้. |
| API success | Closes form/modal, reloads detail, and shows เพิ่มที่อยู่เรียบร้อยแล้ว. |
| API failure | Keeps user in context and shows ไม่สามารถเพิ่มที่อยู่ได้ or the API error message. |
GPS Coordinate Request
| Step | Behavior |
|---|---|
| Open request form | Mobile button ปักหมุดตำแหน่ง; Terminal button ขออัปเดตพิกัด. Both preload existing U_BFP_Latitude and U_BFP_Longitude if present. |
| Use current location | Requests device location permission and fills the request latitude/longitude fields. |
| Map picker | Terminal toggles map picker inside CustomerLocationForm; Mobile uses its map picker flow. |
| Note | Optional note is sent as note for the approver. |
| Submit | Requires numeric addressId and non-empty latitude/longitude. |
| API | Calls requestCustomerAddressGpsUpdate({ addressId, U_BFP_Latitude, U_BFP_Longitude, note }). |
| Result | Shows ส่งคำขอสำเร็จ กรุณารอการอนุมัติ; the actual approval/rejection is documented in Customer Address Management. |
Default Shipping Address
| Rule | Behavior |
|---|---|
| Button visibility | Mobile shows ตั้งเป็นค่าเริ่มต้น only for ShipTo rows in the address action area. Terminal shows ตั้งค่าเริ่มต้น for non-default addresses and omits it for default rows. |
| Permission | Requires address-management permission. Otherwise shows คุณไม่มีสิทธิ์ตั้งค่าที่อยู่เริ่มต้น. |
| Address type | Only ShipTo can be default. BillTo returns ตั้งค่าเริ่มต้นได้เฉพาะที่อยู่จัดส่ง. |
| Already default | No API call; the current default row is treated as no-op. |
| Required IDs | Requires customerCode and addressId; otherwise shows ไม่พบข้อมูลที่อยู่ที่ต้องการแก้ไข. |
| API | Calls setCustomerDefaultAddress(customerCode, addressId). |
| Success | Reloads customer detail and shows ตั้งค่าที่อยู่จัดส่งเริ่มต้นเรียบร้อยแล้ว. |
Data And API Flow
| Concern | Behavior |
|---|---|
| List API | getCustomerSapList() calls /customers?page={page}&pageLimit={PAGESIZE}&search={search}&shippingType={shippingType}&branchId={branchId}&routeCode={routeCode}. |
| Request-mode list | Mobile request mode appends isRequest=1; normal Customer menu does not. |
| Detail API | getCustomerSapById(customerId) calls /customers/:customerId and expects profile fields, addresses, and branches. |
| Profile by code | getCustomerSapByCode(customerCode) calls /customers/:customerCode/profile; used by other flows that start from code instead of ID. |
| Point balance | getCustomerPointBalance(customerCode) calls /customers/:customerCode/point-balance; used by order/benefit flows that need the latest point balance. |
| Re-call logging | callCustomer(customerId) posts /customers/:customerId/re-call before opening the phone dialer. |
| Status request | updateCustomerStatus(customerCode, { status }) patches /customers/:customerCode/status. It is a request workflow, not a direct silent status mutation in the UI. |
| Address bypass | createCustomerAddressByPass(customerCode, payload) patches /customers/:customerCode/addresses. |
| GPS update request | requestCustomerAddressGpsUpdate(payload) posts /customer-draft/address-lat-lng. |
| Default address | setCustomerDefaultAddress(customerCode, addressId) patches /customers/:customerCode/addresses/:addressId/default. |
| Customer request approval | Mobile request mode calls approveCustomerRequest(customerCode) -> PATCH /customers/:customerCode/approve; POS Terminal handles the same service in Customer Activation. |
| Customer request rejection | Mobile request mode calls rejectCustomerRequest(customerCode) -> PATCH /customers/:customerCode/reject; POS Terminal handles the same service in Customer Activation. |
| Direct address GPS update | updateCustomerAddressGps(customerCode, addressId, payload) patches /customers/:customerCode/addresses/:addressId; this is used by related transport flows, while the Customer detail screen uses the approval request flow above. |
| Customer points | Customer detail displays tier, point, pointToNextTier, nextTierName, credit total, credit limit, and available credit-note amount when present in the detail payload. |
Request Payload Examples
The examples below omit the API host and shared Content-Type header. PATH is also omitted from the URL, as in the endpoint table above. All IDs and customer data are examples only. GET endpoints have no JSON request body; their query string and path parameters are the request input.
Customer list
GET /customers?page=1&pageLimit=32&sort=DESC&sortBy=CreatedAt&search=%E0%B8%9A%E0%B8%A3%E0%B8%B4%E0%B8%A9%E0%B8%B1%E0%B8%97%20%E0%B9%80%E0%B8%AD&shippingType=1&branchId=101&routeCode=R001
Request mode adds &isRequest=1:
GET /customers?page=1&pageLimit=32&sort=DESC&sortBy=CreatedAt&search=&shippingType=&branchId=101&routeCode=&isRequest=1
Customer detail
GET /customers/12345
Customer profile by code
GET /customers/C000123/profile
Customer point balance
GET /customers/C000123/point-balance
Re-call logging
POST /customers/12345/re-call
Content-Type: application/json
{}
Request customer status change
PATCH /customers/C000123/status
Content-Type: application/json
{
"status": "INACTIVE"
}
Use "ACTIVE" when requesting that an inactive customer be opened again.
Add a customer address
PATCH /customers/C000123/addresses
Content-Type: application/json
{
"addressType": "ShipTo",
"branchCode": "",
"branchName": "",
"checkAddressName": "ใกล้ตลาดตัวอย่าง",
"addressName": "บ้านลูกค้าตัวอย่าง",
"street": "99/9 หมู่ 4",
"block": "ต.สุรศักดิ์ อ.ศรีราชา",
"city": "จ.ชลบุรี",
"country": "TH",
"zipCode": "20110",
"visOrder": "",
"U_BFP_Amphur": "อ.ศรีราชา",
"U_BFP_Tambon": "ต.สุรศักดิ์",
"U_ISS_RouteCode": "R001",
"U_ISS_RouteName": "สายส่งศรีราชา",
"U_BFP_Latitude": "13.173200",
"U_BFP_Longitude": "100.931500"
}
For a legal-entity BillTo, send the selected branch in branchCode and branchName; head office is "00000" and "สำนักงานใหญ่". BillTo addresses clear the route fields. For a delivery customer, ShipTo requires U_ISS_RouteCode and U_ISS_RouteName.
Request an address GPS update
POST /customer-draft/address-lat-lng
Content-Type: application/json
{
"addressId": 9876,
"U_BFP_Latitude": "13.173200",
"U_BFP_Longitude": "100.931500",
"note": "ขอแก้พิกัดหน้าร้านจากตำแหน่งล่าสุด"
}
addressId is sent as a number. The latitude and longitude are sent as strings, and note may be empty.
Set the default shipping address
PATCH /customers/C000123/addresses/9876/default
Content-Type: application/json
{}
Approve a customer request
PATCH /customers/C000123/approve
Content-Type: application/json
{}
Reject a customer request
PATCH /customers/C000123/reject
Content-Type: application/json
{}
Directly update address GPS (related transport flow)
PATCH /customers/C000123/addresses/9876
Content-Type: application/json
{
"U_BFP_Latitude": "13.173200",
"U_BFP_Longitude": "100.931500"
}
This endpoint is not the normal Customer detail submit path; the Customer detail screen submits a GPS request for approval instead.
Permissions And Guards
| Topic | Mobile | POS Terminal |
|---|---|---|
| Menu permission | Normal customer menu uses CUSTOMER_MENU_DEPT_CODES: -2, 1, 2, 3, 4, 7, 8, 9, 13, 14, 16, 21, 27, 29, 30, plus admin/full non-main warehouse access. | Terminal MENU_CUSTOMER: -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 13, 14, 27, 30, 16. |
| Request mode permission | CUSTOMER_REQUEST_DEPT_CODES: 2, 4, 7, 16, 29, plus admin/full non-main warehouse access. | Separate customerActivation menu uses MENU_CAN_ACTIVE_CUSTOMER: 2, 4, 7, 16, 29. |
| Create customer documents | Mobile uses sales-order access for detail/list primary actions. Customer permission context also has action dept codes -2, 1, 2, 4, 7, 8, 9, 13, 29. | canCreateCustomerDocument() allows admin/full non-main warehouse access or dept codes -2, 1, 2, 4, 7, 8, 9, 13, 16, 21, 29. |
| Toggle status | Mobile uses customer action permission; status action is shown only for active/inactive states. | Terminal uses CUSTOMER_ACTION_DEPT_CODES: -2, 1, 2, 4, 7, 8, 9, 13, 29, plus admin. |
| Address actions | Mobile address permission allows admin/full non-main warehouse access or dept codes 13, 14, 30. | Terminal address actions allow admin or dept codes 13, 14, 30. |
| Edit from detail | Mobile blocks some list edit cases but detail edit is available through canEditCustomerFromDetail for customer action users. | Terminal shows edit action when status-action permission passes. |
| Blocked customer/order | Document creation calls getSalesOrderBlockedReason(customer, user) before starting order/booking/borrow. | |
| Existing order | Sales-order creation checks getExistingCustomerOrderForBranch(customer, branch) and prompts open existing vs create new. |
Mobile vs POS Terminal
| Topic | Mobile | POS Terminal |
|---|---|---|
| Navigation model | Dedicated stack screens and route params. | HomeScreen split-pane list/detail; actions switch active menu and load draft detail. |
| Customer modes | default, booking, borrow, request. | Normal customer menu; activation/request work is separate customerActivation. |
| List actions | Row tap plus swipe primary action. | Row tap only; actions are in detail pane. |
| Create documents | Navigates to target stack detail screen. | Builds draft detail and changes active menu to target document. |
| Address form | Full-screen form inside CustomerDetail. | Modal form inside right pane. |
| Map picker | Native/shared MapPickerModal. | Embedded Google Maps WebView picker. |
Developer Handoff Map
| Area | Code |
|---|---|
| Mobile list/detail | apps/mobile/src/screens/customerStack/customerListScreen, customerDetailScreen |
| Mobile permissions | apps/mobile/src/utils/customerPermissions.ts |
| Mobile copy/modes | apps/mobile/src/screens/customerStack/config.ts |
| Terminal detail | apps/pos-terminal/src/screens/HomeScreen/CustomerDetailForm.tsx |
| Terminal list/detail wiring | apps/pos-terminal/src/screens/HomeScreen/index.tsx, DetailPane.tsx, ListPane.tsx |
| Terminal split data | apps/pos-terminal/src/services/splitMenuData.ts -> getCustomerSplitList, getCustomerSplitDetail |
| Customer service | apps/*/src/services/customers.ts |
| Address helpers | apps/mobile/src/utils/customerAddress.ts, terminal local helpers in CustomerDetailForm.tsx |