ข้ามไปยังเนื้อหาหลัก

ขั้นตอนงานการขาย

หน้านี้สรุปขั้นตอนขายหลักที่ระบบ 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 พิมพ์ไม่มี Lots or พิมพ์มี Lots includes 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 NEW before 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-notes using branchId, isDiscount=1, and the order customerCode.
  • 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-otp with { phone }.
  • After send-otp returns a token, the modal shows the OTP code field and confirms with POST /api/orders/{orderId}/credit-notes/{creditNoteId}/confirm-otp using { 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 และ field routes.distanceMeters
  • ระยะทางถูกปัดเป็นกิโลเมตรด้วยกฎ 500 เมตร หรือเทียบเท่า Math.round(distanceMeters / 1000)
  • ค่าขนส่งอัตโนมัติคิดจาก max(0, roundedKm - 150) * 15 เช่น ระยะทาง 152 กม. จะได้ค่าขนส่ง 30 บาท
  • ถ้าการคำนวณระยะทางอัตโนมัติทำให้ shippingFee ของ order ที่มีอยู่เปลี่ยน ระบบจะบันทึก order ทันที
  • ใบสั่งขายที่เปิดจาก booking price จะเติม shipping address, route code, route name, และพิกัดลูกค้าจากที่อยู่ ShipTo default ของ 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:

  • storefrontDetailScreen uses the item's whsCode for both the auto Lot/Batch pick and the manual picker, then falls back to the selected branch warehouse code.
  • shippingDetailScreen uses the product group's whsCode for 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/healthz when 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, and 100
  • 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 refreshed onHand - booking
  • editing a step-price item reloads step prices from the MST API; currentStepPrice is 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}
  • branchId must 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: weightBaseFlag and stepPriceFlag
  • item payload company fields must follow mobile fallbacks: companyCode ?? company and companyName ?? ComName ?? company ?? comName