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

Mobile Transport และ Vehicles

หน้านี้บันทึก flow ของหน้า transport detail และการจัดการสถานะยานพาหนะบน mobile

ขอบเขต

อิงจาก:

  • apps/mobile/src/screens/transportStack/transportDetailScreen/index.tsx
  • apps/mobile/src/screens/vehicleStack/vehicleListScreen/index.tsx
  • apps/mobile/src/services/common.ts
  • apps/mobile/src/services/googleRoutes.ts
  • apps/mobile/src/navigation/RootNavigator.tsx
  • apps/mobile/src/screens/homeScreen/index.tsx

เมนูยานพาหนะ

หน้า Home มีเมนู ยานพาหนะ ในกลุ่ม ข้อมูล

  • ใช้ icon menus.menu11
  • วางถัดจาก ผู้ใช้ทั้งหมด
  • ใช้ permission เดียวกับ ผู้ใช้ทั้งหมด คือ MENU_EMPLOYEE.has(deptCode)
  • navigate ไป VehicleList
  • ไม่มี create shortcut

หน้า Vehicle list

VehicleList ถูก register ใน RootNavigator และ render หน้า vehicleStack/vehicleListScreen

หน้าจอนี้:

  • โหลดข้อมูลรถด้วย getVehicles({ search }) จาก services/common
  • ค้นหาได้ด้วย header/search pattern แบบเดียวกับ list screen เช่น invoiceListScreen
  • refresh list หลังบันทึกสถานะรถ
  • ใช้ licensePlate เป็น identity ของรายการเมื่อมีค่า

ข้อมูลในการ์ดรถแสดง field หลักชุดเดียวกับ vehicle picker ใน transportDetailScreen:

  • ทะเบียนรถพร้อม label สถานะรถ
  • ประเภทรถ
  • น้ำหนักบรรทุกสูงสุด หน่วยกิโลกรัม

สีพื้นหลังการ์ดแยกตามสถานะ:

vehicleStatusสีการ์ด
UNCHECKEDแดงอ่อน
NOT_READYเหลืองอ่อน
READYเขียวอ่อน

เมื่อกดการ์ดรถ จะเปิด modal สำหรับแก้สถานะ

ใน modal มี:

  • radio 3 ตัวเลือก:
    • UNCHECKED: ยังไม่ตรวจสอบ
    • READY: พร้อมใช้งาน
    • NOT_READY: ยังไม่พร้อมใช้งาน
  • vehicleStatusNote เป็น textarea หลายบรรทัด
  • action ยกเลิก และ บันทึก

สี active ของ radio รองรับ dark mode เพื่อให้ข้อความที่เลือกอ่านได้ชัดเจน

เมื่อบันทึก จะเรียก:

PATCH api/vehicles/{licensePlate}/vehicle-status

payload:

{
"vehicleStatus": "READY",
"vehicleStatusNote": "ตรวจสอบแล้ว พร้อมใช้งาน"
}

API update ใช้ domain/base URL เดียวกับ getVehicles ผ่าน mstApi

การอนุมัติใบจัดใน Transport detail

transportDetailScreen สามารถอนุมัติใบจัดโดยอัปเดตสถานะใบจัดเป็น:

{
"deliveryStatus": "FINISHED"
}

สิทธิ์การอนุมัติถูกควบคุมด้วย canFinalizeTransportDeliveries

เงื่อนไขจะเป็น true เมื่อครบทุกข้อ:

  • หน้าจอไม่ใช่ create mode
  • user ปัจจุบันแก้ไข transport นี้ได้
  • transport ยังไม่เริ่มงาน (detail.startedAt ว่าง)
  • canFinalizeShippingSheet(user) อนุญาต department ของ user

canFinalizeShippingSheet(user) อนุญาต dept code:

-2, 1, 2, 4, 7, 8, 9, 13, 16, 29, 30

POS Terminal ใช้ rule pending delivery เดียวกับ Mobile สำหรับการแสดงปุ่มอนุมัติใน transport detail

ปุ่มอนุมัติใบจัดทั้งหมดด้านล่าง

bottom bar ของหน้า transport detail รองรับ action อนุมัติใบจัดทั้งหมดควบคู่กับปุ่มบันทึกเดิม

เมื่อแสดงผล:

  • ปุ่มซ้าย: อนุมัติใบจัดทั้งหมด
  • ปุ่มขวา: action เดิม เช่น บันทึก

ปุ่ม อนุมัติใบจัดทั้งหมด จะแสดงเฉพาะเมื่อ:

  • อยู่ที่ step รายการใบจัด
  • canFinalizeTransportDeliveries เป็น true
  • มีรายการใบจัดอย่างน้อย 1 ใบที่ deliveryStatus เป็น null, undefined, ค่าว่าง, whitespace, หรือ string null

เมื่อกด อนุมัติใบจัดทั้งหมด ระบบจะแสดง confirmation alert ก่อนยิง API ทุกครั้ง หลังผู้ใช้กดยืนยัน ระบบจะ loop รายการใบจัดที่ยัง pending ทั้งหมด, finalize ทีละใบ, แล้ว refresh transport detail ปัจจุบันด้วย transportId

เงื่อนไขก่อนเริ่มการขนส่ง

ปุ่ม เริ่มการขนส่ง ใน transportDetailScreen จะเริ่มงานได้เฉพาะเมื่อใบจัดทุกใบในรายการมีสถานะ deliveryStatus แล้ว

ระบบถือว่าใบจัดยังไม่พร้อมเริ่มขนส่งเมื่อ:

  • deliveryStatus เป็น null
  • deliveryStatus เป็น undefined
  • deliveryStatus เป็นค่าว่างหลัง trim()
  • deliveryStatus เป็น string null

ถ้ายังมีใบจัดที่ไม่มีสถานะ ระบบจะหยุด flow ก่อนเปิด modal เริ่มขนส่ง และมี guard ซ้ำก่อน submit เริ่มขนส่งจริง เพื่อกันกรณีข้อมูลเปลี่ยนระหว่าง modal เปิดอยู่

การจัดเรียงลูกค้าตามพิกัดด้วย Google

sort planner ในหน้า transport detail รองรับ action จัดเรียงเส้นทางด้วย Google

footer ของ modal จัดเรียงแบ่งเป็น 2 แถว:

  • แถวแรก: เคลียร์รายชื่อจัดเรียง, toggle ชื่อ (แสดงชื่อเดิม / แสดงชื่อย่อ), ปุ่มปิด, และปุ่มบันทึก
  • แถวที่สอง: ปุ่มเต็มบรรทัด จัดเรียงลูกค้าตามพิกัดด้วย Google

เมื่อกด จัดเรียงลูกค้าตามพิกัดด้วย Google ระบบจะแสดง confirmation alert ก่อน หลังผู้ใช้กดยืนยัน:

  • หน้าจออ่าน GPS ปัจจุบันของเครื่องด้วย Geolocation.getCurrentPosition
  • ใช้ตำแหน่งปัจจุบันของเครื่องเป็น route origin
  • ลูกค้าที่มี U_BFP_Latitude และ U_BFP_Longitude ที่ถูกต้องจะถูกใช้เป็นจุดในเส้นทาง
  • ลูกค้าที่ไกลจากตำแหน่งปัจจุบันที่สุดจะถูกใช้เป็น Google Routes destination
  • ลูกค้าที่เหลือและมี GPS จะถูกส่งเป็น intermediates
  • services/googleRoutes.optimizeGoogleRoute(...) เรียก Google Routes computeRoutes พร้อม optimizeWaypointOrder: true
  • หน้าจออ่าน routes.optimizedIntermediateWaypointIndex จาก response แล้วนำมาเรียงรายการใน sort planner ใหม่
  • ลูกค้าที่ไม่มี GPS ที่ถูกต้องจะยังอยู่ใน payload และถูกต่อท้ายหลังลูกค้าที่ Google จัดเรียงแล้ว โดยคงลำดับเดิม

หลังเรียงเสร็จ ระบบจะแสดง alert เรียงเสร็จสิ้นแล้ว

action นี้อัปเดตเฉพาะ state ของ sort planner เท่านั้น ยังไม่บันทึก backend ทันที ผู้ใช้ต้องกดปุ่มบันทึกของ modal เอง ซึ่งจะ persist ลำดับปัจจุบันผ่าน flow เดิม sortTransportCustomers({ transportId, customerCodes })

หมายเหตุ operation:

  • Google API key อ่านจาก constants/config.ts ชื่อ GOOGLE_ROUTE_API_KEY
  • เพราะ origin ใช้ GPS ปัจจุบันของเครื่อง เส้นทางจะเริ่มจากตำแหน่งเครื่อง ณ ตอนผู้ใช้กดยืนยัน ถ้าต้องการเริ่มจากคลัง/สาขา เครื่องควรอยู่ที่คลัง/สาขานั้น หรือปรับ flow ให้ใช้พิกัดคลังที่เก็บไว้แทน
  • implementation ปัจจุบันจำกัดการจัดเส้นทางด้วย Google ไว้ที่ลูกค้าที่มี GPS ถูกต้องไม่เกิน 26 รายการต่อครั้ง

ปุ่มพิมพ์ใน Header

header menu ของหน้า transport detail ไม่แสดง ยืนยันใบจัดสายส่งทั้งหมด แล้ว การอนุมัติใบจัดทั้งหมดถูกย้ายไปอยู่ที่ปุ่มด้านล่างแทน

header menu จะแสดงปุ่มพิมพ์เฉพาะเมื่อ:

  • หน้าจอไม่ใช่ create mode
  • มี transportId
  • transport มีใบจัดอย่างน้อย 1 ใบ
  • ทุกใบจัดมี deliveryStatus ที่ไม่ใช่ null และไม่ใช่ค่าว่าง

เมื่อครบเงื่อนไข จะมีเมนู:

  • พิมพ์ใบแจ้งหนี้ทั้งหมด
  • พิมพ์ใบจัดสินค้า

ทั้งสอง action โหลดข้อมูล invoice ผ่าน:

GET api/transports/{transportId}/invoices

implementation อยู่ใน services/transport ชื่อ getTransportCustomerInvoices(transportId)

พิมพ์ใบแจ้งหนี้ทั้งหมด

พิมพ์ใบแจ้งหนี้ทั้งหมด ใช้ข้อมูลจาก getTransportCustomerInvoices(transportId) โดยตรง

  • response ถูก group ตามลูกค้า
  • ลูกค้า 1 คนมีได้หลาย invoice
  • หน้าจอ flatten invoice ของทุกลูกค้า ตามลำดับ sort ของลูกค้า
  • เรียก printInvoice(invoice, { printIndex }) โดยตรง จึงไม่เรียก getInvoiceDetail ซ้ำ
  • ใช้ invoice print template เดียวกับ flow อื่นที่ผ่าน printInvoice

เพื่อลดความเสี่ยง native print crash เมื่อพิมพ์หลายใบต่อเนื่อง loop จะรอให้ React Native interactions จบก่อน และหน่วง 800ms หลังพิมพ์แต่ละใบ ก่อนเริ่มใบถัดไป

พิมพ์ใบจัดสินค้า

พิมพ์ใบจัดสินค้า ใช้ข้อมูลจาก getTransportCustomerInvoices(transportId) เช่นกัน

  • flatten invoice จากลูกค้าทุกคน
  • map items ของแต่ละ invoice ให้เป็น shape products แบบเดียวกับ shipping screen
  • รวมสินค้าทั้งหมดด้วย buildGroupedProducts
  • พิมพ์ผ่าน printShippingProductSummary(...) โดยใช้ template ใบจัดสินค้าเดียวกับ shippingDetailScreen

การรับชำระลูกค้าแบบอื่นๆ

ใน step รายการส่งสินค้าแยกตามลูกค้า เมื่อเลือก action ชำระเงินและเลือกประเภท อื่นๆ (OTHER) ระบบใช้ flow สำหรับกรณีชำระไม่ปกติ เช่น รับสินค้าบางรายการ, ไม่รับสินค้า, หรือแบ่งชำระ

เงื่อนไข validation หลัก:

  • ต้องกรอก รายละเอียดเพิ่มเติม ทุกครั้ง
  • ต้องอัปโหลด รูปส่งของ
  • ต้องมีลายเซ็นลูกค้า เพราะ OTHER ไม่ใช่ TRANSFER หรือ BILL_PAYMENT
  • ถ้าเลือก ไม่รับสินค้า ระบบไม่เช็กยอดเงิน และตอน submit จะส่ง cash = 0 และ transfer = 0
  • ถ้าไม่ได้เลือก ไม่รับสินค้า ระบบเช็ก cash + transfer เทียบกับ grandAmount
  • cash + transfer ต้องไม่เกิน grandAmount ทุกกรณี รวมถึง รับสินค้าบางรายการ และ แบ่งชำระ
  • ถ้าไม่ได้เลือก รับสินค้าบางรายการ ระบบยังบังคับให้ cash + transfer ไม่น้อยกว่า grandAmount
  • ถ้าเลือก รับสินค้าบางรายการ ระบบยอมให้ cash + transfer น้อยกว่า grandAmount ได้ แต่ยังห้ามเกิน grandAmount

ตอน submit OTHER จะ append system note หลังหมายเหตุผู้ใช้ เช่นยอดเงินสด, ยอดโอน, และ flag รับสินค้าบางรายการ, ไม่รับสินค้า, หรือ แบ่งชำระ เพื่อให้คลัง/การเงินตรวจต่อได้

หมายเหตุการดูแล

  • ต้อง keep เอกสาร Home menu ใน workflows/home-menu-and-permissions ให้ตรงกับ route และ permission ของ VehicleList
  • ให้คง API สถานะรถไว้บน mstApi เว้นแต่ backend route จะย้ายออกจาก MST domain
  • ถ้าเปลี่ยน permission ฝั่ง shipping ให้ตรวจ transportDetailScreen ด้วย เพราะหน้านี้ reuse canFinalizeShippingSheet(user)
  • ต้อง keep print actions ของ transport ให้ตรงกับ print helper และ template ที่ใช้ร่วมกับ shippingDetailScreen