POS หน้าร้าน
POS หน้าร้านเป็น flow ขายหน้าร้านเฉพาะ Terminal สำหรับลูกค้า walk-in จัดการตะกร้า รับชำระเงิน พิมพ์ เปิดลิ้นชัก และ customer display/second screen
เปลี่ยนคลังปัจจุบัน
กดปุ่มชื่อสาขา/คลังที่มุมบนซ้าย แล้วยืนยัน ต้องการเลือกคลังใหม่ใช่หรือไม่ เพื่อเปิด เลือกคลัง ของสาขาที่บันทึกไว้โดยตรง สาขาจะคงเดิม และรายการคลังยังตรวจสิทธิ์ผู้ใช้รวมถึงสิทธิ์คลังหลักตามเดิม การ refresh หรือโหลดใหม่ยังอยู่ในหน้าเลือกเฉพาะคลัง เมื่อเลือกจะบันทึกคลังภายใต้สาขาเดิมแล้วกลับ POS หน้าร้าน กดปิดเพื่อกลับโดยไม่บันทึก ส่วนการตั้งค่าจาก Home และหน้าเข้าสู่ระบบยังเลือกได้ทั้งสาขาและคลัง
วิธีชำระเงินใบสั่งขาย
OrderPaymentStep แสดง เงินสด → โอน → แบ่งชำระ → QRcode เป็นแถวเดียวเลื่อนแนวนอนได้ ปุ่มมีความกว้างอ่านง่าย โดยเปิดแสดงตัวเลือกโอนแล้ว และไม่รีเซ็ตกลับเงินสดเมื่อเลือกโอน
- แบ่งชำระ ใช้
paymentChannel: OTHERมีช่องเงินสด/เงินโอนและช่องแนบสลิปเดิมของการโอน ต้องกรอกทั้งสองยอด ระบุศูนย์และทศนิยมไม่เกิน 2 ตำแหน่งได้ และผลรวมต้องเท่ากับยอดสุทธิที่ต้องชำระtotalDueก่อนเรียกPOST /api/payment - ช่องจำนวนเงินสด/เงินโอนแสดงคอมมาคั่นหลักพันขณะกรอก เช่น
1,000และ1,434,900โดยคงข้อความทศนิยมระหว่างพิมพ์ เช่น1,000.และ1,000.50คอมมาใช้เฉพาะการแสดงผล ส่วนการคำนวณ ยอด QR และ payload ชำระเงินใช้ค่าตัวเลขเดิม - ยอดแบ่งชำระ ใช้ยอดสุทธิหลังหักสิทธิประโยชน์และส่วนลด รวมค่าขนส่ง/ค่าบวกเพิ่มและหักใบแจ้งหนี้ยกเลิกตาม helper ร่วมกับ Mobile กรอกช่องใดช่องหนึ่งแล้วระบบเติมยอดที่เหลืออีกช่อง โดยไม่รับค่าที่เกินยอดสุทธิและคงสองยอดเดิมไว้ ดูสูตรและ JSON ใน ใบสั่งขาย: ขั้นตอนชำระเงิน
- เงินสด โอน และ QR ปกติไม่ส่งฟิลด์แบ่งชำระเพิ่ม วิธี QR ปกติยังขึ้นกับ environment และปุ่มตรวจสอบ QR ใช้อ่านสถานะใบสั่งขาย ส่วน QR เงินโอน/แบ่งชำระด้านล่างเป็น flow แยกที่อนุญาตใน UAT
- หน้าพนักงานเป็นเจ้าของฟอร์ม จอลูกค้า native แสดงสรุปยอด ลูกค้า และเลขใบสั่งขายแบบอ่านอย่างเดียว พร้อมชื่อวิธี เงินโอน หรือ แบ่งชำระ เงินสดและเงินโอน ตามที่เลือก การพิมพ์ยอดแบ่งชำระและแนบสลิปเก็บไว้ที่หน้าพนักงาน
QR SCB สำหรับเงินโอนและแบ่งชำระ
ปุ่ม สร้าง QR Code ใช้ได้ในวิธี โอน เมื่อยอดสุทธิที่ต้องชำระเต็มจำนวนมากกว่า 0 และในวิธี แบ่งชำระ เมื่อกรอกยอดโอนมากกว่า 0 พร้อมผ่าน validation ของฟอร์มชำระเงินเดิม ใบสั่งขายต้องยังเป็น WAITING_PAYMENT และไม่มีใบแจ้งหนี้ซ้ำ ก่อนสร้าง QR ระบบโหลดใบสั่งขายและใบแจ้งหนี้ใหม่ หากเลขเอกสาร สถานะ หรือยอดชำระเปลี่ยน ต้อง refresh และตรวจข้อมูลก่อนสร้างอีกครั้ง
- วิธี โอน ใช้ยอดสุทธิ
totalDueเต็มจำนวนหลังหักสิทธิประโยชน์และส่วนลด รวมค่าขนส่ง/ค่าบวกเพิ่มตามกติกาเดิม ส่วน แบ่งชำระ ใช้เฉพาะยอดเงินโอนที่กรอก ทั้งสองวิธีใช้ service และ reference แบบเดียวกัน ยอด QR ต้องมากกว่า 0 และมีทศนิยมไม่เกิน 2 ตำแหน่ง ref1ใช้เลขใบสั่งขายจาก API โดยตัดช่องว่างหัวท้ายและเครื่องหมายขีด แล้วต่อด้วยเวลาขณะสร้างรูปแบบHHmmssหากเลขใบสั่งขายยาว 12–13 ตัวอักษร จะได้ reference ยาว 18–19 ตัวอักษร รหัสสาขาที่มีตัวอักษรใช้ได้ โดยลบ validation เดิมที่บังคับเป็นตัวเลขทั้งหมดแล้วref2เป็นOTHERเสมอ ระบบป้องกันการสร้าง reference เดิมซ้ำภายในวินาทีเดียวกันขณะที่แอปทำงาน ต้องรอวินาทีถัดไปก่อนสร้างอีกครั้ง- flow QR เงินโอน/แบ่งชำระสร้าง QR ใน UAT ได้ ส่วนวิธีชำระ QRcode ปกติยังใช้เงื่อนไข environment เดิม flow เงินโอนมี reference ของตัวเองและไม่ใช้ cache QR ปกติของใบสั่งขาย
- หน้าจอที่ 2 ซึ่งเป็น Android Customer Display แสดง QR ยอดโอน และ reference ทั้งสองวิธี โดย โอน แสดงยอดสุทธิเต็มจำนวนพร้อมหัวข้อ โอนเงินด้วย QR Code ส่วน แบ่งชำระ แสดงยอดเงินสดและยอดเงินโอนแยกกันด้วย หน้าจอหลักเป็นผู้แก้ไขยอด ส่วนหน้าจอที่ 2 แสดงสรุป
ตัวอย่างใบสั่งขาย 101-261002-001 ยอดโอน 75.25 บาท สร้างเวลา 12:34:56 จะส่ง payload สร้าง QR SCB ดังนี้:
{
"amount": 75.25,
"ref1": "101261002001123456",
"ref2": "OTHER"
}
Service ขอ SCB access token ผ่าน /getTokenScb/{branchCode} แล้วลงลายเซ็นและส่ง payload ไป /getQrPayment/{branchCode} ทั้งสอง route เป็นของ payment gateway แยกจาก API รับชำระของ POS
ตรวจสลิปที่สแกนสำหรับ QR เงินโอน
สแกนสลิปที่หน้าจอหลัก ระบบส่งค่าที่อ่านได้ไป POST /api/payment/verify-slip ด้วย { "qrCode": "<ค่าจากเครื่องสแกน>" } สำหรับ flow QR ค่าสินค้าทั้งวิธี โอน และ แบ่งชำระ ต้องตรวจ response ผ่านทุกเงื่อนไขดังนี้:
| ฟิลด์ใน response | เงื่อนไข |
|---|---|
valid | ต้องเป็น true |
alreadyUsed | ต้องเป็น false |
status.code | ต้องเป็น 1000 |
slip.amount | ตรงกับยอดเงินโอนของ QR ล่าสุด โดยเปรียบเทียบเป็นสตางค์ |
slip.ref1 | ตรงกับ ref1 ของ QR ล่าสุดทุกตัวอักษร |
slip.ref2 | ตรงกับ OTHER ทุกตัวอักษร |
slip.transRef | มีเลขอ้างอิงธุรกรรมที่ไม่ว่าง |
ใช้ slip.amount ตรวจยอด โดย info.amount เป็นข้อมูลประกอบ เมื่อผ่านทั้งหมดจะแสดง global alert สลิปเงินโอนถูกต้อง และอนุญาตให้ยืนยันการชำระเงินภายใต้ guard การชำระเดิม การแนบรูปสลิปอย่างเดียวไม่ทำให้ยืนยันได้เมื่อกดสร้าง QR ใน flow เงินโอนหรือแบ่งชำระแล้ว
เมื่อแก้ยอด เปลี่ยนวิธีชำระ หรือสร้าง QR ใหม่ ผลตรวจสลิปเดิมจะถูกยกเลิก หาก response กลับมาหลังฟอร์มหรือ QR เปลี่ยน ระบบจะไม่ใช้ผลนั้น ต้องตรวจสลิปกับ QR ปัจจุบันใหม่ การยืนยันชำระยังส่งตามวิธีที่เลือก: โอน ใช้ paymentChannel: TRANSFER โดยไม่เพิ่มฟิลด์แบ่งชำระ ส่วน แบ่งชำระ ใช้ paymentChannel: OTHER พร้อมยอดเงินสด/เงินโอน โดย QR ref2 เป็น OTHER ทั้งสองวิธี ก่อนกดสร้าง QR ยังใช้การแนบรูปสลิปแบบเดิมได้ แต่เมื่อเริ่มสร้าง QR แล้ว รวมถึงกรณีสร้างไม่สำเร็จ ต้องมีผลตรวจสลิปที่ตรงกับ QR ล่าสุดก่อนยืนยัน
การลบค่าทดสอบวันที่ 2 ตุลาคม 2026
แอปกลับมาเปิดหน้า Home และใช้ใบสั่งขายที่พนักงานเลือกแล้ว ลบการเปิด order 2392 อัตโนมัติ การค้างรอคะแนนไม่จำกัดเวลาของ order นี้ และ payload QR ชั่วคราว { "amount": 1, "ref1": "SSK260928001190427", "ref2": "OTHER" } การสร้าง QR และตรวจสลิปใช้ยอดและ reference ที่สร้างจากฟอร์มปัจจุบัน
เก็บ request/response SCB ที่ชำระจริงไว้เป็น regression fixture ที่ apps/pos-terminal/src/services/__tests__/fixtures/scb-split-payment-2026-10-02.json โดยไม่มีการใช้ข้อมูลนี้ในแอปที่ทำงานจริง ตรวจการลบค่าทดสอบผ่าน 75 tests, Terminal TypeScript, Kotlin compile และ rebuild/install Android แล้ว (8126987d)
ทางเข้าในแอป
| App | Target |
|---|---|
| Mobile | not available |
| POS Terminal | StorefrontPosStack |
ภาพรวมการทำงาน
Service / API ที่เกี่ยวข้อง
StorefrontPosStack: POS navigation, cart, checkout, printing, and second-screen stateorders.ts/invoices.ts: order, invoice, payment, and retry behavior when POS creates accounting documentsproducts.ts: product list, stock, price, and step-price contextborrow.ts: รายการ/รายละเอียดใบเบิก การดำเนินการตามสถานะ การแปลง และการเปิดใบสั่งขายที่แปลงแล้ว
สิทธิ์และเงื่อนไข
| เรื่อง | รายละเอียด |
|---|---|
| Terminal menu key | storefrontPos |
| deptCode ที่เห็นเมนู | -2, 1, 2, 3, 4, 7, 29, 21, 8, 9, 12, 20, 22, 23, 27, 28 |
| เงื่อนไขสาขา | เฉพาะ Terminal; งาน customer display/native Android อยู่ใต้ apps/pos-terminal/android |
Workflow หลัก
- เมนูนี้มีเฉพาะ POS Terminal ไม่มีใน Mobile
- งาน second screen/customer display ต้องทำตาม AGENTS: verify, commit/push แล้วรัน
yarn terminal:android:8082 - ยอดตะกร้าต้องตรงกับ payment และเอกสารที่พิมพ์
- อย่าสับสนกับใบจัดสินค้าหน้าร้านซึ่งเป็น fulfillment ของ invoice
คิวเพิ่มสินค้าแบบต่อเนื่อง
Product workspace ใช้คิว FIFO เดียวสำหรับการสร้าง item ของใบสั่งขายและใบเบิก รวมถึงสินค้ารายการแรกในช่วงที่ยังไม่มี identity ของเอกสาร
- รายการที่เลือกจะแสดงในตะกร้าทันทีพร้อมสถานะระดับรายการ กำลังเพิ่ม และยังเลือกสินค้าเพิ่มต่อได้โดยไม่มี loading แบบเต็มหน้าจอ
- งานแรกในคิวจะสร้าง header และเก็บ
orderId/borrowIdที่ API คืนมา จากนั้นงานถัดไปในคิวจะใช้ identity เดียวกันตามลำดับ; เลขเอกสารใช้แสดงผลเท่านั้น และห้ามใช้แทน identity สำหรับสร้าง item - เมื่อสำเร็จสถานะจะเป็น เพิ่มแล้ว หากการอ่าน detail จากเซิร์ฟเวอร์ตามหลัง response เพิ่ม item ระบบจะเก็บรายการ local ไว้และ merge กับ detail เพื่อไม่ให้รายการที่เพิ่มเร็วหายจากหน้าจอ
- identity ของ item ใน order detail รองรับฟิลด์
items[].idที่ API ส่งมา (รวมถึงชื่อฟิลด์ id แบบเดิม) เมื่อ FIFO จบ detail ที่บันทึกบนเซิร์ฟเวอร์เป็นข้อมูลหลัก และจะคง local เฉพาะรายการที่กำลังเพิ่ม กำลังลบ หรือผิดพลาด จึงไม่แสดงรายการ preview ที่สำเร็จซ้ำหลัง refresh - ระหว่างคิวทำงานหรือกำลังตรวจยอด order รอบสุดท้าย ปุ่มให้ลูกค้าตรวจสอบ/ยืนยันจะถูกปิดไว้ หลัง item สุดท้ายของใบสั่งขายเสร็จ ระบบคำนวณ amount และ weight จาก item ที่บันทึกจริง และอัปเดต header เฉพาะเมื่อข้อมูลไม่ตรง ส่วนใบเบิกไม่ตรวจยอดรวมแบบใบสั่งขาย
- การลบรายการที่ยังรอในคิวจะยกเลิกงานนั้นก่อนเริ่มส่ง ส่วนรายการที่กำลังส่งจะรอรับ identity ของ item แล้วจึงเรียก API DELETE ตามปกติ
Plus Sales: สินค้าแนะนำจาก Cross-Sell
Plus Sales ใน StorefrontPosStack ใช้ crossSell จากคำขอ POST /api/orders/upsell เดิม ผลตอบกลับชุดเดียวกันยังใช้ stepNudge แสดงคำแนะนำเพิ่มจำนวนเพื่อรับราคาขั้นถัดไปที่รายการในตะกร้า โดยยกเลิกการแสดงสินค้าตัวอย่าง 5 รายการแล้ว
เงื่อนไขการแสดง
- ใช้จังหวะเรียก upsell เดิมเมื่อเปิดใบสั่งขายที่บันทึกแล้วและมีสินค้า หลังเพิ่มสินค้าสำเร็จ หรือหลังลบรายการที่บันทึกแล้ว ต้องมีตัวตนใบสั่งขาย (
orderNo,orderIdหรือorderStorageId) และรายการสินค้าไม่ว่าง งานนี้ไม่ได้เพิ่มคำขอแนะนำสินค้าอีกชุด - นำ
crossSell[].productCodeไปจับคู่itemCodeของสินค้า Firebase/Firestore ประจำสาขา ใช้ชื่อ ราคา สต็อก และ object สินค้าจาก Firebase ไม่ใช้ราคาหรือรายละเอียดสินค้าจาก upsell การค้นหาและตัวกรองหมวดหมู่ Product Hub ของรายการทางซ้ายไม่จำกัดรายการแนะนำ - เปรียบเทียบรหัสโดยตัดช่องว่างหัวท้ายและไม่สนตัวพิมพ์เล็ก/ใหญ่ รักษาลำดับจาก API ตัดรหัสซ้ำ/ไม่ถูกต้อง และข้ามรหัสที่ไม่พบใน Firebase เช่นเดียวกับรายการสินค้าหลัก แสดงเฉพาะราคามากกว่าศูนย์และสต็อกพร้อมขาย (
onHand - booking) มากกว่าศูนย์ - ตัดรหัสที่ตรงกับ
productCodeหรือitemCodeของรายการในใบสั่งขาย รวมถึงสินค้าในตะกร้าปัจจุบันที่เพิ่งเพิ่มและกำลังรอบันทึก ตัดออกจาก Plus Sales เท่านั้น ไม่ลบสินค้าออกจาก order/ตะกร้า - หากกรองแล้วไม่เหลือสินค้า ให้ซ่อนทั้งแถบบนทั้งสองจอ รวมถึงช่วงยังไม่ได้ผลตอบกลับ ระหว่างโหลดคำแนะนำใหม่ เมื่อคำขอล้มเหลว ตะกร้าว่าง และโหมดใบเบิก ไม่มีรายการสำรองหรือลิมิตตัวอย่าง 5 การ์ด
ผลจากคำขอเก่าจะไม่ทับผลใหม่ เมื่อเปลี่ยนใบสั่งขายหรือโหมดเอกสารจะล้างคำแนะนำเดิม โดยตัวป้องกันคำขอซ้อนครอบคลุมทั้งการอ่านรายละเอียดใบสั่งขายและการเรียก upsell
การใช้งานบนจอหลักและจอรอง
| หน้าจอ | พฤติกรรม |
|---|---|
| จอหลัก: React Native ของพนักงาน | การ์ดขนาดกะทัดรัดมีชื่อและราคา ไม่มีรูปภาพหรือปุ่มลูกศร ปัดแนวนอนเพื่อดูรายการ กดการ์ดแต่ละใบเพื่อเปิด modal เพิ่มสินค้าเดียวกับการกดรายการสินค้าทางซ้าย การกดการ์ดอย่างเดียวยังไม่บันทึกสินค้า |
| จอรอง: Android Customer Display แบบ native | แสดงสินค้าและราคาชุดเดียวกันหลังกรอง เลื่อนแนวนอนได้อย่างเดียว การ์ดไม่สามารถเพิ่ม/แก้สินค้า หรือเปิด modal |
พนักงานเป็นเจ้าของการแก้ไขสินค้า Plus Sales เรียก handler เลือกสินค้าเดิม จึงใช้สิทธิ์ การจัดการรายการซ้ำ ข้อจำกัดสินค้าจองราคา สต็อก น้ำหนัก Step Price payload และข้อความผิดพลาดจาก API เหมือน flow เพิ่มสินค้าปกติ กดยกเลิก modal แล้วใบสั่งขายไม่เปลี่ยน ส่วนการยืนยันเพิ่มใช้คิวบันทึกเดิม
รูปแบบ API ที่ client ใช้
คำขอเดิมส่ง branchId และ items[] ที่มี productCode, qty, uomCode, whGrpCode ตัวอย่างผลตอบกลับด้านล่างใช้เพื่ออธิบายรูปแบบเท่านั้น รหัส RECOMMENDED-001 ต้องมีใน Firebase จึงจะแสดงการ์ด ฟิลด์เพิ่มเติมในคำแนะนำไม่ถูกนำมาใช้แทนรายละเอียดหรือคำนวณราคา
{
"crossSell": [{ "productCode": "RECOMMENDED-001" }],
"stepNudge": [
{
"productCode": "i2-dy-10-gkh",
"current": 9,
"nextFrom": 10,
"nextPrice": 660,
"needMore": 1
}
]
}
กรณี crossSell: [] จะซ่อน Plus Sales แต่ stepNudge ที่เข้าเงื่อนไขยังแสดงที่รายการในตะกร้าได้ การ์ดสินค้าแนะนำกับคำแนะนำราคาขั้นถัดไปเป็นคนละส่วน
จุดตรวจ Regression
- ยังไม่มีผล upsell,
crossSellว่าง/รูปแบบไม่ถูกต้อง, ไม่พบรหัสใน Firebase หรือถูกกรองออกหมด: ไม่เหลือหัวข้อหรือพื้นที่แถบว่างบนทั้งสองจอ - รหัสต่างตัวพิมพ์/มีช่องว่าง รหัสซ้ำ และคำแนะนำมากกว่า 5 รายการ: จับคู่ได้ ตัดซ้ำ และแสดงตามลำดับ API โดยไม่ติดลิมิตตัวอย่าง
- สินค้าที่อยู่ใน order ตามรหัสใดรหัสหนึ่ง รวมถึงสินค้าที่เพิ่มแล้วกำลังบันทึก: หายจาก Plus Sales แต่สินค้าเดิมใน order ยังอยู่
- ปัดซ้าย–ขวาจากชื่อ ราคา และพื้นที่ว่างในการ์ดได้โดยไม่เปิด modal กดการ์ดจอหลักเปิดฟอร์มเดิมและยกเลิกได้ จอรองยังอ่านอย่างเดียว
- รักษาพื้นที่สัมผัสของการ์ดจอหลักด้วย
pointerEvents="box-only"และScrollViewของ Gesture Handler การปิด touch ทั้งการ์ดเคยทำให้เลื่อนติด ส่วนการกดใช้Pressableของ React Native เหมือนรายการสินค้าหลัก - ตรวจว่าคำขอ refresh ที่ล้มเหลวและผลเก่าที่ตอบช้าไม่ทำให้รายการเก่ากลับมา และ
stepNudgeยังทำงานแยกกันได้
หน้าจอลูกค้า: ค้นหาเบอร์และความยินยอมสมัครสมาชิก
ที่หน้าระบุลูกค้าบนหน้าจอที่ 2 ปุ่มค้นหาด้วยเบอร์โทรจะแสดงไอคอนค้นหาตามปกติจนกว่าจะกรอกครบ 10 หลัก เมื่อครบแล้วปุ่มจะเปลี่ยนเป็นข้อความ ยืนยัน พร้อมพื้นหลังสีเขียวอ่อน และหากลบตัวเลขออกจะกลับเป็นไอคอนและสีเดิมทันที
การสมัครสมาชิกต้องให้ลูกค้ายอมรับประกาศ PDPA แบบ mock ก่อนจึงเปิดฟอร์มสมัครสมาชิก
- หากพนักงานกด สมัครสมาชิก จากหน้าจอหลัก หน้าจอหลักจะแสดงสถานะรอลูกค้ายอมรับเงื่อนไขและข้อตกลง ขณะที่หน้าจอที่ 2 แสดง Modal PDPA; กด ยอมรับ แล้วจึงเปิดฟอร์มสมัครสมาชิกเดิมที่พนักงานเป็นผู้แก้ไขข้อมูล ส่วน ไม่ยอมรับ จะกลับไปหน้าระบุลูกค้าทั้งสองหน้าจอ
- หากลูกค้ากด สมัครสมาชิก จากหน้าจอที่ 2 จะเห็น Modal PDPA เดียวกันก่อน; กด ยอมรับ แล้วจึงไปฟอร์มสมัครสมาชิกเดิมที่ลูกค้าเป็นผู้กรอก ส่วน ไม่ยอมรับ จะยกเลิก flow
- ข้อความ PDPA ปัจจุบันเป็นข้อความ mock และระบุจุดรองรับการเพิ่มลายเซ็นลูกค้าในอนาคต
- ระหว่างพนักงานเป็นเจ้าของฟอร์ม หน้าจอที่ 2 ใช้สำหรับรับความยินยอมเท่านั้น และต้องไม่แก้ไขหรือ sync ฟอร์มพร้อมกันกับหน้าจอหลัก
หน้าจอลูกค้า: ตรวจสอบรายการ โฆษณา และประเมินบริการ
ระหว่างเพิ่มสินค้า แผงข้อมูลลูกค้าจะไม่แสดง section สิทธิประโยชน์ แต่ใช้พื้นที่ส่วนล่างแสดงโฆษณาแบบเต็มขอบของแผง โดยจอ 1920x1080 ใช้ขนาดภาพเตรียมประมาณ 538x812 px ตามพื้นที่คงเหลือจริงหลังข้อมูลลูกค้า ชื่อลูกค้าแสดงได้เพียงบรรทัดเดียวและตัดท้ายด้วย ...
- เมื่อหน้าจอหลักส่ง order ที่คิวและการตรวจยอดเสร็จแล้วให้ลูกค้าตรวจสอบ โฆษณาจะถูกแทนด้วยปุ่มยืนยันรายการ และเมื่อกดแล้วโฆษณาจะกลับมาทันที
- เมื่อสถานะใบสั่งขายปัจจุบันเป็น
CONFIRMEDหน้าจอที่ 2 แสดง emoticon 5 ระดับเรียงจากซ้ายไปขวา: ดีมาก (5), ดี (4), พอใช้ (3), แย่ (2) และ แย่มาก (1) ตัวเลขนี้ใช้อธิบายค่าที่ส่ง API โดยไม่แสดงบนปุ่มจริง ไม่มีดาว ปุ่มกลับหน้าแรก หรือ modal ยืนยัน - เมื่อเลือกคะแนนจะเรียก
POST /api/orders/{id}/ratingโดยใช้ id ของใบสั่งขายปัจจุบันและ session ที่ยืนยันตัวตนของหน้าจอหลักremarkเป็นข้อความว่างเสมอ ตัวอย่างเมื่อเลือก 5 คะแนน:
{
"rating": 5,
"remark": ""
}
- ปุ่มคะแนนถูกปิดระหว่างบันทึกและป้องกันคำขอซ้ำ แสดงคำขอบคุณเมื่อ API สำเร็จเท่านั้น หาก API ไม่สำเร็จจะแสดงข้อความ error และให้ลองใหม่ ระบบไม่รับ event ของ order อื่น และไม่ส่งผลกลับหน้าจอที่ออกไปแล้ว
- หน้าประเมินปกติรอ 60 วินาทีก่อนกลับหน้าระบุลูกค้า เมื่อเลือกคะแนนจะหยุดนับเวลาระหว่างส่งคำขอ หากสำเร็จแสดงคำขอบคุณ 5 วินาทีแล้วกลับ ไม่มีการ fix order หรือค้างหน้าประเมินเพื่อทดสอบอีกแล้ว
- เพิ่มการส่งคะแนนใน
11408089และลบค่าทดสอบชั่วคราวใน8126987d
รายการใบเบิกและใบสั่งขายที่แปลงแล้ว
หน้าระบุลูกค้ามีปุ่ม แปลงคิวดูตัวอย่าง / ดูรายชื่อลูกค้าปลาวันนี้ ซึ่งเปิดรายการใบเบิกของ Terminal ด้วย API getBorrows({ page, search }) แบบแบ่งหน้าเดียวกับ Mobile แต่ละรายการแสดงเลขที่เอกสารและประเภทใบเบิกภาษาไทย สถานะพร้อมเวลาออกเอกสาร ลูกค้า ผู้เบิก ผู้รับงาน และผู้คืน/ตัดสต็อก
- กดรายการจะโหลด
getBorrowDetail(borrowId)แล้วเปิด product workspace เดิมในborrowmode พร้อมรายการสินค้าและปุ่มดำเนินการตามสถานะที่บันทึกไว้ - กดกลับจากใบเบิกที่เปิดด้วยเส้นทางนี้ จะกลับรายการใบเบิกและโหลดข้อมูลใหม่ ไม่กลับหน้าระบุลูกค้า
- การแปลงใช้ guard เดียวกับ Mobile: สาขาหลัก ประเภท
SPECรับงานแล้ว และผู้ใช้มีสิทธิ์ เมื่อกดจะเปิด Modal ร่วมกัน แสดงชื่อสินค้า รหัส จำนวน checkbox ที่เห็นการเลือกชัดเจน และ เลือกทั้งหมด เมื่อกด ยืนยัน จะส่งเฉพาะborrowItemIdsที่เลือกไป/api/borrow/{id}/convert - สถานะ
PARTIAL_CONVERTEDแสดง แปลงและรอคืน ในหน้ารายละเอียด สินค้าที่lotsว่าง/ไม่มี lots ใช้พื้นหลังแดงอ่อนเพื่อบอกว่าแปลงแล้ว ส่วนรายการที่ยังมี lots ใช้พื้นหลังปกติ - เมื่อยืนยันคืนในสถานะ
PARTIAL_CONVERTEDจะส่ง ID ของทุกรายการที่ยังมีlotsไป/api/borrow/{id}/return-itemsแม้hasFailedSubBorrowเป็น true โดย lots มีอยู่ในรายการแล้ว จึงไม่มีหน้าจอเลือก lots ส่วนสถานะอื่นที่ดำเนินการได้ยังใช้/approveหรือ/retryตามเดิม - เมื่อ
convertBorrowสำเร็จ ระบบ refresh ใบเบิกเพื่อรับconvertedOrderId; ปุ่ม ไปที่ใบสั่งขาย จะโหลดgetOrderDetail(convertedOrderId)แล้วเปิด workspace เดียวกันในordermode - การเปิดใบสั่งขายที่แปลงแล้วใช้ข้อมูล order ที่บันทึกไว้เป็นหลักสำหรับสินค้า การรับ/จัดส่ง ที่อยู่ เส้นทาง พิกัด และ document flags จากนั้นโหลด customer profile ด้วย
customerCodeเพื่อให้ tier, point, สิทธิประโยชน์ และที่อยู่ fallback ครบ หากโหลด customer profile ไม่ได้ ยังเปิด order จาก snapshot ที่บันทึกไว้ได้
ดูเงื่อนไขสิทธิ์ ตัวอย่าง payload การตรวจรายการที่เหลือ และตำแหน่งโค้ดร่วมทั้งหมดใน ใบเบิกยืม
ลิ้นชักเก็บเงิน
หลังบันทึกชำระเงินสำเร็จ Terminal จะเปิดลิ้นชัก C9D ผ่าน CPOS Helper ที่ติดตั้งอยู่ รวมถึงกรณีเซิร์ฟเวอร์หรือ QR ยืนยันใบสั่งขายที่กำลังรอชำระ การ refresh ใบเดิมที่ชำระแล้วหรือเปิดดูใบที่ยืนยันอยู่แล้วจะไม่เปิดซ้ำ หากเชื่อมต่อ helper ไม่ได้ จะแจ้งปัญหาลิ้นชักแยกจากผลชำระเงิน ดูวิธีเชื่อมต่อที่ยืนยันแล้วและการตั้งค่าที่ Telpo C9D Cash Drawer
ตัวกรองสินค้าจาก Product Hub
การ์ดสินค้า สต็อก ช่องจำนวน ยอดจำนวนรวมในหน้าต่างเพิ่ม/แก้ไข และราคาสินค้าแนะนำของ Storefront ใช้หน่วยขายจาก inventory แทนคำว่า “ลัง” ตายตัว โดยชื่อหน่วยจำนวนใช้ salUomName ก่อน แล้วจึงใช้ชื่อหรือรหัสหน่วยที่มี หากไม่มีหน่วยจะไม่ต่อท้ายข้อความ สินค้าชั่งน้ำหนักแสดงราคาและราคาเดิมต่อ กิโลกรัม แต่จำนวนยังใช้หน่วยขายของสินค้า ข้อความ Step Price ใช้ helper getStepDisplayUomCode ร่วมกับ Mobile รวมทั้งเงื่อนไขกิโลกรัมและลำดับเลือกรหัสหน่วย การเปลี่ยนนี้เป็นข้อความแสดงผล ไม่เปลี่ยนจำนวน สูตรคำนวณ หรือ payload ของ API
หลังแคชเชียร์เลือกลูกค้าและเปิด สร้างใบสั่งขาย Product Picker ของ Terminal จะโหลด category tree จาก Product Hub ด้วย company=BFPDB ก่อน แล้วเรียกด้วย company=NFFDB โดยทั้งสองคำขอส่ง levels=itemType,itemGroup,sizeRange จากนั้นนำทั้งสอง response มารวมและใช้ตัวเตรียม filter เดิมรวมรายการที่มี key เดียวกันภายใต้ parent เดียวกันในทุกระดับ บวก count เข้าด้วยกัน และเก็บรายการที่มีเฉพาะบริษัทใดบริษัทหนึ่ง ต้องโหลดสำเร็จทั้งสองบริษัทก่อนแสดง filter ที่รวมแล้ว โดยตัด tree entry ที่ไม่มี key หรือ label ก่อนแสดงผล
- Section filter แรกมี ทั้งหมด ตามด้วย item type จาก Product Hub เมื่อเลือก item type จะจำกัด Section ที่สองให้เหลือ item group ของ type นั้น
- Section ที่สองแสดง item group; กด group เดิมซ้ำเพื่อยกเลิกการเลือก และการเลื่อน scroll ในทั้งสอง Section จะไม่เลือกปุ่ม
- เรียก
GET /pdhub/productsด้วยcompany=BFPDB,page=1,pageSize=200โดยจะส่งitemTypeและitemGroupเฉพาะเมื่อเลือกค่าไว้ และส่งได้พร้อมกันทั้งสอง key - response ของ Product Hub เป็นรายการสิทธิ์ของสินค้า ไม่ใช่แหล่งข้อมูลสำหรับ card โดยตรง Product Picker จะเทียบ
itemCodeกับ inventory ของสาขาใน Firestore แล้วแสดงเฉพาะรายการที่พบทั้งสองแหล่งตามลำดับของ Product Hub แต่ใช้รายละเอียดสินค้า ราคา และสต็อกจาก Firestore - หากเปลี่ยน filter ต่อเนื่อง จะใช้เฉพาะ response ล่าสุดของ Product Hub ในการอัปเดตรายการที่เห็น
รายการโปรโมชันและวันสิ้นสุด
หน้าเลือกสินค้า Terminal StorefrontPosStack โหลดโปรโมชันปัจจุบันของคลังที่เลือกอัตโนมัติเมื่อเข้าหน้าและเมื่อเปลี่ยนคลัง การ์ดในรายการสินค้าปกติที่ตรงกับโปรจะแสดงไอคอนของขวัญหน้าชื่อสินค้าและข้อมูลวันสิ้นสุดโปร กด โปร เพื่อกรองเฉพาะสินค้าที่ตรงกับโปร
- เรียก
GET /pdhub/promotion-master/active/current?branchCode=NF-ACRโดยใช้PRODUCT_HUB_SERVERตามBASE_API_ENV: UAT ใช้https://apiproduct-uat.boonsiri.co.thและ PRD ใช้https://apiproduct.boonsiri.co.thโดยส่งbranch.whsGrpCodeซึ่งเป็นรหัสคลัง แม้BranchCodeจะต่างกัน หากไม่มีรหัสคลังจะไม่ส่งคำขอ - อ่าน response
{ data: [...], error: null, status: true, time: ... }แล้วเทียบdata[].itemCodeกับ inventory ของคลังใน Firestore โดยตัดช่องว่างหัวท้ายและไม่สนตัวพิมพ์เล็ก/ใหญ่ ตัดรหัสว่าง ซ้ำ หรือไม่พบใน Firestore หากรหัสซ้ำใช้รายการแรกที่พบ - ใช้ชื่อ ราคา สต็อก และข้อมูลสินค้าจาก Firestore แสดงเฉพาะ
price > 0และสต็อกพร้อมขายonHand - booking > 0ค่าpromotionPriceและbasePriceใน response ไม่เขียนทับราคา inventory การค้นหาและเรียงราคายังใช้กับสินค้าที่จับคู่แล้ว - แสดงไอคอนของขวัญหน้าชื่อสินค้าทุกรายการที่ตรงกับโปร แม้ไม่มีวันสิ้นสุด และต่อท้ายราคา/หน่วยด้วย
endDateที่ถูกต้อง เช่น ฿210/ลัง (ถึงวันที่ 30/09/2026) โดยแปลงส่วนวันที่ใน API เป็นDD/MM/YYYY(ปี ค.ศ.) ค่า2026-09-30T23:59:59.000Zแสดง30/09/2026โดยไม่แปลง timezone จนเลื่อนไปวันที่ 1 ตุลาคม หากไม่มีวันที่หรือวันที่ไม่ถูกต้องจะไม่แสดงข้อความวันที่ ทั้งรายการปกติและแท็บโปรแสดงไอคอนและข้อมูลวันสิ้นสุดเหมือนกัน
เมื่อเปิด โปร จะล้างตัวเลือกประเภท/กลุ่ม/ขนาดสินค้า แต่คงคำค้นปัจจุบัน และกรองด้วยข้อมูลโปรที่โหลดไว้แล้วโดยไม่เรียกซ้ำหลังโหลดสำเร็จ กด สินค้า หรือ ล้างตัวกรอง เพื่อกลับรายการปกติและล้างคำค้น/ตัวกรอง โดยข้อมูลโปรบนการ์ดยังแสดงอยู่ การเลือกหมวดสินค้าจะออกจากแท็บโปรเช่นกัน หากโปรว่าง แท็บโปรจะไม่มีสินค้า แต่รายการปกติยังแสดงตามเดิม มีสถานะโหลดและ error จาก API หากโหลดไม่สำเร็จให้กด โปร เพื่อโหลดใหม่ การโหลดโปรแยกจากคำขอรายการปกติ ผลที่กลับมาช้าจึงเพิ่มข้อมูลบนการ์ดปกติได้โดยไม่สลับแท็บ และผลของคลังเก่าจะไม่ทับโปรของคลังที่เลือกใหม่ เมื่อ Firestore อัปเดตราคาและสต็อก ข้อมูลโปรบนการ์ดยังคงอยู่
กดสินค้าที่พบเพื่อเปิดขั้นตอนเพิ่มสินค้าเดิมโดยใช้ object จาก Firestore สิทธิ์การขาย Step Price payload และการตรวจสต็อกยังผ่าน flow เดิม
ไฟล์และการตรวจสอบ
| ส่วน | ไฟล์ |
|---|---|
คำขอ branchCode และการอ่านผล/error | apps/pos-terminal/src/services/productHub.ts |
| จับคู่สินค้า สลับแท็บ แปลงวันที่ และข้อความบนการ์ด | apps/pos-terminal/src/screens/StorefrontPosStack/ProductSelectionStep/index.tsx |
| การตัดบรรทัดและรูปแบบข้อความวันที่ | apps/pos-terminal/src/screens/StorefrontPosStack/ProductSelectionStep/styles.ts |
| เทสต์สัญญา API | apps/pos-terminal/src/services/__tests__/productHubPromotions.test.ts |
| เทสต์รายการ วันสิ้นสุด และผลตอบกลับเก่า | apps/pos-terminal/src/screens/StorefrontPosStack/__tests__/promotions.test.tsx |
รันจาก repository root:
yarn terminal:typecheck
TZ=Asia/Bangkok yarn workspace @bsr/pos-terminal jest --runInBand --watchman=false src/screens/StorefrontPosStack/__tests__/promotions.test.tsx src/services/__tests__/productHubPromotions.test.ts
โค้ดล่าสุดผ่าน Terminal typecheck และเทสต์ทั้ง 7 กรณีข้างต้น ยังต้องตรวจหน้าตาบนอุปกรณ์จริง
น้ำหนักรายการสินค้าในใบสั่งขาย
รายการในใบสั่งขายที่บันทึกแล้วใช้ ตัวแก้น้ำหนักรวมร่วม: ปุ่ม แก้ไขน้ำหนัก ตรวจสิทธิ์แก้รายการและ fillWeight เปิด Modal น้ำหนัก และต้องยืนยันผ่าน Global Alert ก่อนส่ง PATCH /api/orders/{orderId}/items/{id}/weight โดยมีเพียง { "weight": 50.5 }
- ใช้กับรายการใบสั่งขายที่บันทึกแล้ว ไม่ใช่ flow ใบเบิกดูปลาหรือสินค้าที่ยังเพิ่มครั้งแรก
saveOrderItemWeightไม่ให้ส่งขณะคิวบันทึกสินค้าหรือการตรวจยอดรอบสุดท้ายยังทำงาน โดยแจ้งกรุณารอให้บันทึกรายการสินค้าเสร็จก่อน- ก่อน PATCH ยกเลิกคำยืนยันลูกค้าเดิม (
customerConfirmationRequestedและcustomerConfirmed) ฟอร์มน้ำหนักเป็นของจอพนักงาน โดย Customer Display ไม่แก้ค่าระหว่างกรอก - หลัง PATCH โหลด order ใหม่และนำรายการที่บันทึกจริงกลับมาแสดงในตะกร้า โดยคงรายการ local ที่ยังบันทึกไม่เสร็จหรือผิดพลาดไว้ ราคาและยอดรวมใช้ผลจากเซิร์ฟเวอร์ flow นี้ไม่เรียกสูตรคำนวณ item/header ของการแก้รายการทั่วไป
- เมื่อบันทึกและ reload สำเร็จ ปิดตัวแก้ไข ลูกค้าต้องตรวจและยืนยันใหม่ผ่านขั้นตอนยืนยันเดิมก่อนดำเนินการต่อ
Mobile vs POS Terminal
| เรื่อง | Mobile | POS Terminal |
|---|---|---|
| โครงหน้าจอ | not available | StorefrontPosStack |
| รายการ | stack/list screen เฉพาะเมนู | split-pane list card ใน HomeScreen |
| รายละเอียด | detail screen เต็มหน้า | detail panel ฝั่งขวา |
Developer Handoff Map
| Area | Code |
|---|---|
| POS stack | apps/pos-terminal/src/screens/StorefrontPosStack |
| Native Android display | apps/pos-terminal/android |
| Shared order/payment services | apps/pos-terminal/src/services/orders.ts |
| Plus Sales cards and tap/swipe behavior | apps/pos-terminal/src/screens/StorefrontPosStack/ProductSelectionStep/PlusSalesPanel.tsx |
| Cross-sell matching and exclusion | packages/utils/src/orderUpsell.ts |
| Cross-sell regression tests | apps/pos-terminal/src/screens/StorefrontPosStack/__tests__/crossSellProducts.test.ts |
| Sales-order payment UI | apps/pos-terminal/src/screens/StorefrontPosStack/OrderPaymentStep/index.tsx |
| Split-payment amount and validation | packages/utils/src/orderSplitPayment.ts, packages/utils/src/orderSalesFlow.ts |
| Storefront borrow entry/list | apps/pos-terminal/src/screens/StorefrontPosStack/index.tsx |
| Product workspace / borrow and converted-order loading | apps/pos-terminal/src/screens/StorefrontPosStack/ProductSelectionStep/index.tsx |
| Storefront fulfillment | HomeScreen/StorefrontDetailForm.tsx |