ขั้นตอนงานการขาย
หน้านี้สรุปขั้นตอนขายหลักที่ระบบ POS ต้องรองรับทั้งบน Mobile และ Terminal
ภาพรวมขั้นตอนงาน
มุมมองผู้ใช้งานและลูกค้า
หมายเหตุการพัฒนา
- รายละเอียดของ scanner, printer และ payment integration อาจต่างกันตาม hardware profile
- ฝั่ง Terminal ควรมอง customer display เป็น runtime surface หลัก ไม่ใช่แค่ capability check
- เมื่อ payment integration นิ่งแล้ว ควรแยกหน้าเป็นขั้นตอนเงินสด บัตร และการชำระเงินแบบผสม
การพิมพ์ใบเบิกจากรายละเอียดใบสั่งขายบน Mobile
orderDetailScreen supports printing storage request documents from the order detail flow.
- The storage-print modal for choosing
พิมพ์ไม่มี Lotsorพิมพ์มี Lotsincludes a required remark textarea. - The textarea uses the same stacked textarea treatment as the invoice remark modal.
- Pressing either print button updates the order first with the current screen state and the storage remark.
- After the update succeeds, the screen reloads the order detail from the API, updates local state, and then prints the selected document variant.
- For draft orders, the print flow moves the order to
NEWbefore reloading and printing.
ส่วนลดเครดิตโน้ตในใบสั่งขายบน Mobile
orderDetailScreen lets users apply a credit-note discount from the order benefit menu.
- Opening the credit-note picker requires an existing order, benefit-management permission, a customer code, no existing credit-note discount, and no active coupon discount on the order.
- The picker loads eligible credit notes with
GET /api/credit-notesusingbranchId,isDiscount=1, and the ordercustomerCode. - Selecting a credit note validates that the note is not already used and that BFP/NFF credit-note discounts do not exceed the matching company item total in the order.
- Applying the selected note calls
POST /api/orders/{orderId}/credit-notes/{creditNoteId}. - If the apply response does not contain
requireOtp: true, the screen closes the picker and refreshes the order detail immediately. - If the apply response contains
requireOtp: true, the screen closes the picker and opens an OTP modal. - The OTP modal accepts a 10-digit phone number, then calls
POST /api/orders/{orderId}/credit-notes/{creditNoteId}/send-otpwith{ phone }. - After
send-otpreturns a token, the modal shows the OTP code field and confirms withPOST /api/orders/{orderId}/credit-notes/{creditNoteId}/confirm-otpusing{ token, otpCode }. - When OTP confirmation succeeds, the screen closes the OTP modal and refreshes the order detail.
ค่าขนส่งและระยะทางใน Mobile Order
orderDetailScreen แยก orders.amount เป็นยอดขายสินค้า และใช้ orders.shippingFee เป็นค่าขนส่งของใบสั่งขาย
- ใบสั่งขายใหม่ default
shippingFeeเป็น0 - เมื่อ user ยืนยันการเปลี่ยนประเภทการขายระหว่างสายส่งกับหน้าร้าน ระบบจะ reset
shippingFeeเป็น0 - ถ้า user กดยกเลิกหรือ dismiss กล่องยืนยัน ระบบจะคงประเภทการขายเดิมและไม่เคลียร์ค่าขนส่งเดิม
- ใบสั่งขายสายส่งจะแสดง row เฉพาะสายส่งสำหรับระยะทาง, ค่าขนส่งที่แก้ไขได้, และยอดที่ต้องชำระ
- ค่าในช่องค่าขนส่งเปิด modal text input เมื่อกด และ modal จะไม่ auto focus ตอนเปิด
- ถ้าใบสั่งขายมี
orderIdแล้ว เมื่อแก้ค่าขนส่งจาก modal ระบบจะบันทึก order ทันที - พิกัดต้นทางของสาขามาจาก helper กลางใน
@bsr/utilsเพื่อให้ Mobile และ POS Terminal ใช้ source เดียวกันได้ - พิกัดปลายทางมาจาก
orderDetail.U_BFP_LatitudeและorderDetail.U_BFP_Longitude; ถ้าขาดค่าใดค่าหนึ่ง, ค่าไม่ใช่ตัวเลข, หรือเป็น0จะไม่คำนวณระยะทางและไม่เรียก Google Routes - ระยะทางตามถนนจริงดึงจาก Google Routes API ด้วย
GOOGLE_ROUTE_API_KEYและ fieldroutes.distanceMeters - ระยะทางถูกปัดเป็นกิโลเมตรด้วยกฎ 500 เมตร หรือเทียบเท่า
Math.round(distanceMeters / 1000) - ค่าขนส่งอัตโนมัติคิดจาก
max(0, roundedKm - 150) * 15เช่น ระยะทาง 152 กม. จะได้ค่าขนส่ง 30 บาท - ถ้าการคำนวณระยะทางอัตโนมัติทำให้
shippingFeeของ order ที่มีอยู่เปลี่ยน ระบบจะบันทึก order ทันที - ใบสั่งขายที่เปิดจาก booking price จะเติม shipping address, route code, route name, และพิกัดลูกค้าจากที่อยู่
ShipTodefault ของ SAP customer ก่อนคำนวณระยะทางสำหรับสายส่ง - เมื่อเลือกที่อยู่จัดส่งใหม่ ระบบจะ update address, route, และพิกัดลูกค้า แล้วคำนวณระยะทางใหม่ถ้าใบสั่งขายเป็นสายส่ง
- ยอดบนการ์ดใน
orderListScreenและยอดที่ส่งตอนชำระเงินหลายรายการใช้invoiceAmount + shippingFeeและ fallback เป็นamount + shippingFeeเมื่อไม่มีinvoiceAmount
การค้นหา Lot/Batch ตามคลังบน Mobile
storefrontDetailScreen and shippingDetailScreen load Lot/Batch options through:
GET products/lot?itemCode={itemCode}&whsCode={whsCode}&company={company}
Warehouse-code source rules:
storefrontDetailScreenuses the item'swhsCodefor both the auto Lot/Batch pick and the manual picker, then falls back to the selected branch warehouse code.shippingDetailScreenuses the product group'swhsCodefor both the auto Lot/Batch pick and the manual picker.
Do not use whGrpCode as the primary Lot/Batch lookup warehouse in storefront, because Lot/Batch availability is warehouse-specific.
In shippingDetailScreen, pressing a product to choose Lot/Batch checks transport confirmation before permission checks. If the delivery has already been assigned to transportation but deliveryStatus is null or an empty string, the screen stops the Lot/Batch flow and shows ขนส่งยังไม่ยืนยันใบจัดนี้.
แจ้งเตือนรับรายการหน้าร้านบน Mobile
storefrontDetailScreen แสดง popup รับงานหลังจากโหลด detail, branch, และ user สำเร็จแล้วเท่านั้น เพื่อไม่ให้ popup แสดงชั่วคราวจาก empty detail ก่อน API ตอบกลับ
กฎปัจจุบัน:
- ระหว่างโหลด detail จะไม่แสดง popup รับงาน
- ถ้า detail โหลด error จะปิด popup รับงานไว้ก่อน
- ถ้า detail จริงมี
acceptedByแล้ว จะไม่แสดง popup รับงาน - user code
999997เป็น storefront action override: user นี้แก้การตรวจ Lot/Batch/location ของสินค้าและส่ง B1 ได้ แม้ไม่ใช่ผู้รับงาน
ชำระเงินใบสั่งขายบน Mobile
orderPaymentScreen อนุญาตให้เลือกช่องทาง โอน โดยไม่บังคับแนบสลิป
เมื่อกด ยืนยันการชำระ:
- ระบบยัง log pay-order URL และ payload ลง console เพื่อช่วยดีบัก
- ระบบยังยิง API
POST /api/orders/paymentผ่านpayOrder(payload)ตาม flow ปกติ - ถ้า API สำเร็จและไม่มี
slipPathจะแสดง Sweet Alert success:ชำระเงินสำเร็จ กรุณาส่ง Incoming ที่หน้าใบแจ้งหนี้ - ถ้า API สำเร็จและมี
slipPathจะไม่แสดง success alert
ขั้นตอนใบจองราคาแบบ split-pane บน POS Terminal
POS terminal does not mirror the mobile navigation stack literally. The mobile sequence Home > ListScreen > DetailScreen is represented as a split-pane Home workspace.
Booking price layout:
- panel 1: sales menu entry
ใบจองราคา - panel 2: booking-price list
- panel 3: booking-price detail or draft form
Creation flow:
Unsaved-change behavior:
- leaving panel 3 while the draft/detail is dirty opens a shared alert
- the alert has three actions:
- cancel
- discard without saving
- save and exit
- save-and-exit creates the booking price when no id exists and updates it when an id exists
List behavior:
- panel 2 supports search, filters, and swipe actions
- loading uses the shared loading overlay
- booking-price status is represented by card background color
- panel 2 is refreshed after booking-price create/delete
Detail behavior:
- panel 3 title is
รายละเอียดใบจองราคา - remark and product item edits mark the detail as dirty
- product rows can be pressed to edit or swiped to delete
- product row display follows mobile conventions: item name, amount, quantity, step-price indicator, weight indicator, and formula text for weight-based pricing
- panel 3 does not wrap the booking detail in a generic outer ScrollView; booking metadata, remark, and product-list title are the product FlashList header so the product list owns scrolling
- existing booking prices can be edited only on the same created day, using the server business date from
GET /api/healthzwhen available - if the existing booking price is outside the editable created day, panel 3 disables add item, edit item, delete item, remark/header editing, and save actions
Product picker behavior:
- the picker is a full-screen modal host
- the product list is on the left side inside the modal
- the add/edit item form is an overlay inside the same modal, not a second nested modal
- this single-modal approach is required for iOS compatibility
- quick quantity options are
1,5,10,50, and100 - step-price data is loaded from the real product step-price API
- add-item overlay follows mobile by using the product name as the title and omitting the price input
- edit-item overlay follows mobile by using the
แก้ไขรายการสินค้าtitle and keeping the price input - quick quantity changes load step price when the selected quantity requires step pricing
- step-price rows show mobile range labels with unit text, such as
ไม่เกิน X {UOM}andเกิน X ไม่เกิน Y {UOM} - editing an existing item first refreshes inventory from Firestore by
productCode; available stock comes from refreshedonHand - booking - editing a step-price item reloads step prices from the MST API;
currentStepPriceis saved as JSON fallback data, not treated as the primary current price source
Persistence behavior:
- creating a booking price calls
POST /api/booking-price - updating a booking price calls
PATCH /api/booking-price/{bookingPriceId} - adding a booking-price item calls
POST /api/booking-price/{bookingPriceId}/items - updating a booking-price item calls
PATCH /api/booking-price/{bookingPriceId}/items/{itemId} - deleting a booking-price item calls
DELETE /api/booking-price/{bookingPriceId}/items/{itemId} - deleting a booking price calls
DELETE /api/booking-price/{bookingPriceId} branchIdmust be numeric in the booking-price payload- add-item persistence must match mobile order: ensure/create booking header, create item, update booking totals, reload detail, and refresh panel 2 when the booking was newly created
- edit-item persistence must patch the item immediately, update booking totals, and reload detail
- item payload flags must be booleans:
weightBaseFlagandstepPriceFlag - item payload company fields must follow mobile fallbacks:
companyCode ?? companyandcompanyName ?? ComName ?? company ?? comName