Now.js Framework Documentation

Now.js Framework Documentation

LineItemsManager - จัดการรายการสินค้าในเอกสาร

TH 11 Feb 2026 07:53

LineItemsManager - จัดการรายการสินค้าในเอกสาร

เอกสารฉบับนี้อธิบาย LineItemsManager ซึ่งเป็นคอมโพเนนต์สำหรับจัดการรายการสินค้าในเอกสารต่างๆ เช่น ใบสั่งซื้อ ใบเสนอราคา ใบรับสินค้า โดยรองรับการเพิ่ม แก้ไข ลบรายการ และคำนวณราคาอัตโนมัติ

📋 สารบัญ

  1. ภาพรวม
  2. การติดตั้งและนำเข้า
  3. การใช้งานพื้นฐาน
  4. คุณสมบัติ HTML
  5. การกำหนดคอลัมน์
  6. การผูกข้อมูล API
  7. การรวมรายการซ้ำ
  8. การโหลดข้อมูลจากแหล่งอื่น
  9. ระบบคำนวณ
  10. อีเวนต์
  11. JavaScript API
  12. ตัวอย่างการใช้งาน
  13. แนวทางปฏิบัติที่ดี

ภาพรวม

LineItemsManager เป็นคอมโพเนนต์สำหรับจัดการรายการสินค้าในเอกสาร โดยให้คุณสามารถเพิ่มสินค้าจากระบบ autocomplete แล้วจะสร้างแถวในตารางพร้อมฟอร์มอินพุตสำหรับแก้ไขข้อมูลอัตโนมัติ

ฟีเจอร์หลัก

  • Event-Driven: ฟังเหตุการณ์ 'change' จากช่องค้นหาสินค้า
  • Detail API: ดึงข้อมูลสินค้าแบบละเอียดจาก API เมื่อเลือกสินค้า
  • Flexible API Parameters: ส่งพารามิเตอร์หลายตัวไปยัง API (qty, warehouse, batch ฯลฯ)
  • Flexible Columns: กำหนดคอลัมน์ผ่าน <th data-field>
  • Editable/Readonly Fields: รองรับฟิลด์แก้ไขได้และอ่านอย่างเดียว
  • Display-Only Fields: แสดงข้อมูลเฉยๆ ไม่มีอินพุต
  • Custom Buttons: ปุ่มกำหนดเองในแต่ละเซลล์ (เช่น คำนวณ VAT)
  • Auto-Calculation: คำนวณอัตโนมัติ (เช่น จำนวน × ราคา = ยอดรวม)
  • Merge Duplicates: รวมรายการซ้ำอัตโนมัติ (ตัวเลือก)
  • External Callbacks: เรียกฟังก์ชันภายนอกผ่าน data-on-calculate
  • Auto-Load: โหลดรายการจาก source อัตโนมัติ (เช่น PO, Quotation)
  • Data Binding: รองรับ data-attr="data:items" เหมือน TableManager

เมื่อไหร่ควรใช้ LineItemsManager

ใช้ LineItemsManager เมื่อ:

  • ต้องการจัดการรายการสินค้าในเอกสาร (PO, Invoice, Receipt)
  • ต้องการให้ผู้ใช้เพิ่มสินค้าผ่าน autocomplete
  • ต้องการแก้ไขข้อมูลในตาราง (จำนวน, ราคา, หมายเหตุ)
  • ต้องการคำนวณยอดรวมอัตโนมัติ
  • ต้องการรวมรายการซ้ำ หรือโหลดข้อมูลจากแหล่งอื่น

ไม่ควรใช้ LineItemsManager เมื่อ:

  • ต้องการแสดงตารางข้อมูลแบบอ่านอย่างเดียว (ใช้ TableManager แทน)
  • ไม่ต้องการให้แก้ไขข้อมูลในตาราง
  • ต้องการควบคุมการเรนเดอร์แถวอย่างละเอียด

การติดตั้งและนำเข้า

LineItemsManager โหลดมาพร้อมกับ Now.js Framework และพร้อมใช้งานทันทีผ่าน window object:

// ไม่ต้อง import - พร้อมใช้งานทันที
console.log(window.LineItemsManager); // LineItemsManager object

สิ่งที่ต้องพึ่งพา

การใช้งาน LineItemsManager ต้องอาศัยโมดูลต่อไปนี้:

  • HttpClient หรือ fetch – สำหรับเรียก API
  • ElementManager – สำหรับสร้างอินพุต (ทางเลือก แต่แนะนำ)

การใช้งานพื้นฐาน

1. โครงสร้าง HTML แบบเต็ม

<!-- ช่องค้นหาสินค้า -->
<input type="text" name="product_text" data-role="product"
       data-autocomplete="true"
       data-source="api/products/search">

<!-- ฟิลด์พารามิเตอร์เพิ่มเติม -->
<input type="number" name="qty" value="1" data-role="qty">
<input type="number" name="warehouse" value="1" data-role="warehouse">

<!-- ตารางรายการสินค้า -->
<table data-line-items="items"
       data-detail-api="api/products/get"
       data-listen-select="[data-role='product']"
       data-api-params="[data-role]"
       data-allow-delete="true"
       data-merge="true"
       data-on-calculate="calculateItems">
  <thead>
    <tr>
      <th data-field="sku">รหัสสินค้า</th>
      <th data-field="name" data-type="text">ชื่อสินค้า</th>
      <th data-field="quantity" data-type="number"
          data-role="quantity" data-min="1">จำนวน</th>
      <th data-field="unit_price" data-type="currency"
          data-role="price">ราคา</th>
      <th data-field="subtotal" data-type="currency"
          data-readonly="true">รวม</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

2. การใช้งานผ่าน JavaScript

// สร้าง instance สำหรับตาราง
const instance = LineItemsManager.create('#my-table', {
  detailApi: 'api/products/get',
  listenSelect: '[data-role="product"]',
  allowDelete: true,
  mergeOnDuplicate: true
});

// เพิ่มรายการ
instance.addItem({
  sku: 'P001',
  name: 'สินค้า A',
  quantity: 1,
  unit_price: 100
});

3. การเริ่มทำงานอัตโนมัติ

LineItemsManager จะสร้าง instance อัตโนมัติสำหรับทุก <table data-line-items>:

<!-- จะถูกสร้างอัตโนมัติเมื่อหน้าโหลด -->
<table data-line-items="items"></table>

คุณสมบัติ HTML

คุณสมบัติพื้นฐาน

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-line-items string 'items' ชื่อฟิลด์สำหรับ form submission (จำเป็น)
data-detail-api string - URL API สำหรับดึงรายละเอียดสินค้า
data-listen-select string '[data-role="product-search"]' Selector สำหรับช่องค้นหาสินค้า
data-api-params string '[data-role]' Selector pattern สำหรับ collect parameters
data-allow-delete boolean true แสดงปุ่มลบหรือไม่
data-delete-confirm boolean false แสดงยืนยันก่อนลบหรือไม่
data-reindex-on-remove boolean false ถ้าเป็น true จะรี-อินเด็กซ์ชื่อ input/ids หลังลบแถว ให้ indices ต่อเนื่อง

คุณสมบัติการรวมรายการซ้ำ

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-merge boolean true รวมรายการซ้ำอัตโนมัติ
data-merge-key string 'sku' ฟิลด์สำหรับตรวจสอบรายการซ้ำ

คุณสมบัติการโหลดจากแหล่งอื่น

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-load-from string - Selector ของ select ที่ใช้โหลดข้อมูล (เช่น #po_id)
data-load-api string - URL API สำหรับโหลดรายการ
data-load-param string 'id' ชื่อพารามิเตอร์ที่ส่งไปยัง API
data-load-clear boolean true ล้างรายการเดิมก่อนโหลดหรือไม่

หมายเหตุ: ค่าเริ่มต้นของ data-listen-select เป็น '[data-role="product-search"]' เพื่อความแน่นอนแนะนำให้ระบุ data-listen-select ใน <table> เสมอ (ตัวอย่างด้านบนใช้ data-listen-select="[data-role='product']") หรือระบุ input ให้มี data-role="product-search" เพื่อให้ตรงกับค่าเริ่มต้น

คุณสมบัติการคำนวณ

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-on-calculate string - ชื่อฟังก์ชันสำหรับคำนวณ (global function)

การกำหนดคอลัมน์

คอลัมน์ถูกกำหนดผ่าน <th> ใน <thead> โดยใช้ data-field และ attributes อื่นๆ

คุณสมบัติคอลัมน์

Attribute คำอธิบาย ตัวอย่าง
data-field ชื่อฟิลด์ข้อมูล (จำเป็น) data-field="quantity"
data-type ประเภทอินพุต data-type="number"
data-readonly อ่านอย่างเดียว data-readonly="true"
data-display-only แสดงเฉยๆ ไม่มีอินพุต data-display-only="true"
data-hidden ซ่อนคอลัมน์ data-hidden="true"
data-role ชื่อ role สำหรับใช้คำนวณ data-role="quantity"

ประเภทอินพุต (data-type)

Type คำอธิบาย Attributes เพิ่มเติม
number ตัวเลข data-min, data-max, data-step
currency สกุลเงิน (ทศนิยม 2 ตำแหน่ง) data-min, data-max, data-step
text ข้อความ data-maxlength, data-size
select ดรอปดาวน์ -
checkbox ช่องทำเครื่องหมาย -

โหมดการแสดงผล

1. Editable (แก้ไขได้)

<th data-field="quantity" data-type="number">จำนวน</th>

→ สร้างอินพุตที่แก้ไขได้

2. Readonly Input (อ่านอย่างเดียว แต่ส่งฟอร์ม)

<th data-field="subtotal" data-type="currency" data-readonly="true">รวม</th>

→ สร้างอินพุตแบบ readonly สำหรับส่งฟอร์ม

3. Display Only (แสดงเฉยๆ)

<th data-field="unit_name" data-display-only="true">หน่วย</th>

→ แสดงข้อความ ไม่สร้างอินพุต ไม่ส่งฟอร์ม

4. Hidden (ซ่อน)

<th data-field="product_id" data-hidden="true">ID</th>

→ สร้าง hidden input

ปุ่มในเซลล์

สามารถเพิ่มปุ่มกำหนดเองในเซลล์ได้:

<th data-field="unit_price"
    data-type="currency"
    data-button-click="addVat"
    data-button-class="icon-plus"
    data-button-title="เพิ่ม VAT">ราคา</th>

Attributes:

  • data-button-click - ชื่อฟังก์ชันที่จะเรียก
  • data-button-class - CSS class ของปุ่ม
  • data-button-text - ข้อความในปุ่ม
  • data-button-title - tooltip ของปุ่ม

ตัวอย่างฟังก์ชัน:

function addVat(currentValue, button, rowData, instance) {
  return currentValue * 1.07; // เพิ่ม VAT 7%
}

การผูกข้อมูล API

การกำหนดพารามิเตอร์ API

LineItemsManager จะ collect parameters จากอินพุตที่ตรงกับ data-api-params:

<!-- ตัวอย่าง: data-api-params="[data-role]" -->
<input type="text" data-role="product" value="P001">
<input type="number" data-role="qty" value="5">
<input type="number" data-role="warehouse" value="1">

เมื่อเลือกสินค้า จะส่ง parameters:

product=P001&qty=5&warehouse=1

หมายเหตุสำคัญ:

  • ใช้ data-role หรือ data-api-param เท่านั้น ไม่ใช้ attribute name
  • Parameter name มาจาก data-role หรือ data-api-param

รูปแบบ Response ที่คาดหวัง

API ควรส่งกลับในรูปแบบ:

{
  "success": true,
  "data": {
    "sku": "P001",
    "name": "สินค้า A",
    "quantity": 1,
    "unit_price": 100.00,
    "unit_name": "ชิ้น",
    "warehouse_name": "คลังหลัก"
  }
}

หรือรองรับ nested data:

{
  "success": true,
  "data": {
    "data": {
      "sku": "P001",
      "name": "สินค้า A",
      ...
    }
  }
}

การรวมรายการซ้ำ

เมื่อเปิดใช้งาน data-merge="true" (ค่าเริ่มต้น) LineItemsManager จะรวมรายการซ้ำอัตโนมัติ:

<table data-line-items="items"
       data-merge="true"
       data-merge-key="sku">

วิธีการทำงาน:

  1. ตรวจสอบว่ามี SKU ซ้ำหรือไม่
  2. ถ้าซ้ำ → บวกจำนวนเข้าไปในแถวเดิม
  3. ถ้าไม่ซ้ำ → สร้างแถวใหม่

ฟิลด์จำนวนที่รองรับ:

  • quantity
  • qty
  • received_qty
  • issued_qty

ปิดการรวมรายการซ้ำ

<table data-line-items="items" data-merge="false">

การโหลดข้อมูลจากแหล่งอื่น

สามารถโหลดรายการจาก source อื่น (เช่น PO, Quotation) อัตโนมัติ:

ตัวอย่าง: โหลดจาก PO

<!-- Select PO -->
<select id="po_id" name="po_id">
  <option value="">-- เลือก PO --</option>
  <option value="1">PO-2024-001</option>
  <option value="2">PO-2024-002</option>
</select>

<!-- ตาราง -->
<table data-line-items="items"
       data-load-from="#po_id"
       data-load-api="api/po/items"
       data-load-param="po_id"
       data-load-clear="true">
  <thead>...</thead>
  <tbody></tbody>
</table>

วิธีการทำงาน:

  1. เมื่อเลือก PO → เรียก api/po/items?po_id=1
  2. ล้างรายการเดิม (ถ้า data-load-clear="true")
  3. เพิ่มรายการจาก API

รูปแบบ Response

{
  "success": true,
  "data": {
    "items": [
      {"sku": "P001", "name": "สินค้า A", "quantity": 5, "unit_price": 100},
      {"sku": "P002", "name": "สินค้า B", "quantity": 10, "unit_price": 50}
    ]
  }
}

หรือ

{
  "success": true,
  "data": [
    {"sku": "P001", ...},
    {"sku": "P002", ...}
  ]
}

ระบบคำนวณ

LineItemsManager ใช้ external callback สำหรับการคำนวณ เพื่อให้ยืดหยุ่นและไม่ติด logic เฉพาะ

การตั้งค่า Callback

<table data-line-items="items" data-on-calculate="calculateItems">

ตัวอย่างฟังก์ชันคำนวณ

function calculateItems({items, instance}) {
  let totalQty = 0;
  let totalAmount = 0;

  // คำนวณแต่ละแถว
  const updatedItems = items.map(item => {
    const qty = parseFloat(item.quantity) || 0;
    const price = parseFloat(item.unit_price) || 0;
    const subtotal = qty * price;

    totalQty += qty;
    totalAmount += subtotal;

    return {
      subtotal: subtotal.toFixed(2)
    };
  });

  // อัพเดทยอดรวม
  return {
    items: updatedItems,
    '#total_qty': totalQty,
    '#total_amount': totalAmount.toFixed(2)
  };
}

โครงสร้าง Callback

Input:

{
  items: Array,      // รายการทั้งหมดจาก DOM
  instance: Object   // LineItemsManager instance
}

Output:

{
  items: Array,      // แต่ละ element = object ของฟิลด์ที่ต้องการอัพเดท
  '#selector': value // อัพเดทค่าใน elements นอกตาราง
}

ตัวอย่างขั้นสูง: คำนวณส่วนลดและ VAT

function calculateItems({items, instance}) {
  let subtotal = 0;

  const updatedItems = items.map(item => {
    const qty = parseFloat(item.quantity) || 0;
    const price = parseFloat(item.unit_price) || 0;
    const itemSubtotal = qty * price;

    subtotal += itemSubtotal;

    return {subtotal: itemSubtotal.toFixed(2)};
  });

  // อ่านค่าส่วนลดจาก input
  const discountPercent = parseFloat(document.getElementById('discount')?.value) || 0;
  const discount = subtotal * (discountPercent / 100);

  const afterDiscount = subtotal - discount;
  const vat = afterDiscount * 0.07;
  const total = afterDiscount + vat;

  return {
    items: updatedItems,
    '#subtotal': subtotal.toFixed(2),
    '#discount_amount': discount.toFixed(2),
    '#after_discount': afterDiscount.toFixed(2),
    '#vat': vat.toFixed(2),
    '#total': total.toFixed(2)
  };
}

การเรียก Recalculate แบบ Manual

// จาก instance
instance.calculate();

// จาก element
const instance = LineItemsManager.getInstance('#my-table');
instance.calculate();

// จากที่ไหนก็ได้ (recalculate ทั้งหมด)
LineItemsManager.recalculate();

การเรียกจาก Template Events

<input type="number" id="discount"
       data-on="input:LineItemsManager.recalculate">

อีเวนต์

LineItemsManager ส่งอีเวนต์เมื่อมีการเปลี่ยนแปลง

ประเภทของอีเวนต์

อีเวนต์ เมื่อไหร่ รายละเอียด
lineitems:add เพิ่มแถวใหม่ {row, rowIndex, instance}
lineitems:update อัพเดทแถว {row, rowIndex, instance}
lineitems:remove ลบแถว {rowIndex, instance}
lineitems:merge รวมรายการซ้ำ {row, rowIndex, instance}
lineitems:calculate คำนวณเสร็จ {rows, instance}
lineitems:clear ล้างรายการทั้งหมด {instance}
lineitems:action กดปุ่มในเซลล์ {action, field, rowIndex, rowData, button, instance}
lineitems:sourceLoaded โหลดจาก source สำเร็จ {source, items, count, instance}
lineitems:sourceError โหลดจาก source ผิดพลาด {source, error, instance}

การฟังอีเวนต์

const table = document.querySelector('[data-line-items]');

table.addEventListener('lineitems:add', (e) => {
  console.log('เพิ่มรายการ:', e.detail.row);
  console.log('ที่แถว:', e.detail.rowIndex);
});

table.addEventListener('lineitems:calculate', (e) => {
  console.log('คำนวณเสร็จ จำนวนแถว:', e.detail.rows);
});

table.addEventListener('lineitems:sourceLoaded', (e) => {
  console.log('โหลดจาก source:', e.detail.source);
  console.log('จำนวนรายการ:', e.detail.count);
});

JavaScript API

สร้าง Instance

const instance = LineItemsManager.create(table, options);

// ตัวอย่าง
const instance = LineItemsManager.create('#my-table', {
  detailApi: 'api/products/get',
  mergeOnDuplicate: true,
  allowDelete: true
});

อ่าน Instance

// จาก element
const instance = LineItemsManager.getInstance(table);

// จาก selector
const instance = LineItemsManager.getInstance('#my-table');

จัดการรายการ

// เพิ่มรายการ
instance.addItem({
  sku: 'P001',
  name: 'สินค้า A',
  quantity: 1,
  unit_price: 100
});

// อัพเดทแถว (ระบุ rowIndex)
instance.updateRow(0, {
  quantity: 5,
  unit_price: 90
});

// ลบแถว
instance.removeRow(0);

// ล้างทั้งหมด
instance.clear();

จัดการข้อมูล

// ดึงข้อมูลทั้งหมด
const data = instance.getData();
// [{sku: 'P001', name: 'สินค้า A', ...}, ...]

// ตั้งค่าข้อมูล (ล้างเดิมและเพิ่มใหม่)
instance.setData([
  {sku: 'P001', name: 'สินค้า A', quantity: 1, unit_price: 100},
  {sku: 'P002', name: 'สินค้า B', quantity: 2, unit_price: 50}
]);

โหลดจาก Source

// โหลดรายการจาก source (เช่น PO ID = 123)
await instance.loadFromSource(123);

คำนวณ

// เรียกคำนวณ
instance.calculate();

ทำลาย Instance

instance.destroy();

ตัวอย่างการใช้งาน

ตัวอย่าง 1: ใบสั่งซื้อ (Purchase Order)

<form id="po-form">
  <!-- ค้นหาสินค้า -->
  <label>สินค้า</label>
  <input type="text" name="product_text" data-role="product"
         data-autocomplete="true"
         data-source="api/products/search">

  <label>จำนวน</label>
  <input type="number" name="qty" value="1" data-role="qty" min="1">

  <!-- ตารางรายการ -->
  <table data-line-items="items"
         data-detail-api="api/products/get"
         data-listen-select="[data-role='product']"
         data-api-params="[data-role]"
         data-on-calculate="calculatePO">
    <thead>
      <tr>
        <th data-field="sku" data-display-only="true">รหัส</th>
        <th data-field="name" data-display-only="true">สินค้า</th>
        <th data-field="quantity" data-type="number" data-min="1">จำนวน</th>
        <th data-field="unit_price" data-type="currency">ราคา</th>
        <th data-field="subtotal" data-type="currency" data-readonly="true">รวม</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <!-- ยอดรวม -->
  <div>
    <label>ยอดรวม</label>
    <input type="text" id="total_amount" readonly>
  </div>

  <button type="submit">บันทึก</button>
</form>

<script>
function calculatePO({items}) {
  let total = 0;

  const updatedItems = items.map(item => {
    const qty = parseFloat(item.quantity) || 0;
    const price = parseFloat(item.unit_price) || 0;
    const subtotal = qty * price;
    total += subtotal;

    return {subtotal: subtotal.toFixed(2)};
  });

  return {
    items: updatedItems,
    '#total_amount': total.toFixed(2)
  };
}
</script>

ตัวอย่าง 2: ใบรับสินค้า (Goods Receipt) - โหลดจาก PO

<form id="gr-form">
  <!-- เลือก PO -->
  <label>ใบสั่งซื้อ</label>
  <select id="po_id" name="po_id">
    <option value="">-- เลือก PO --</option>
  </select>

  <!-- ตารางรายการ (โหลดจาก PO) -->
  <table data-line-items="items"
         data-load-from="#po_id"
         data-load-api="api/po/items"
         data-load-param="po_id"
         data-on-calculate="calculateGR">
    <thead>
      <tr>
        <th data-field="sku" data-display-only="true">รหัส</th>
        <th data-field="name" data-display-only="true">สินค้า</th>
        <th data-field="ordered_qty" data-display-only="true">สั่งซื้อ</th>
        <th data-field="received_qty" data-type="number" data-min="0">รับแล้ว</th>
        <th data-field="warehouse" data-type="select">คลัง</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <button type="submit">บันทึก</button>
</form>

ตัวอย่าง 3: ใบแจ้งหนี้ (Invoice) - มี VAT และส่วนลด

<form id="invoice-form">
  <!-- ค้นหาสินค้า -->
  <input type="text" data-role="product"
         data-autocomplete="true"
         data-source="api/products/search">

  <!-- ตารางรายการ -->
  <table data-line-items="items"
         data-detail-api="api/products/get"
         data-listen-select="[data-role='product']"
         data-on-calculate="calculateInvoice">
    <thead>
      <tr>
        <th data-field="name">สินค้า</th>
        <th data-field="quantity" data-type="number">จำนวน</th>
        <th data-field="unit_price" data-type="currency"
            data-button-click="addVat"
            data-button-class="icon-plus"
            data-button-title="+ VAT 7%">ราคา</th>
        <th data-field="subtotal" data-type="currency" data-readonly="true">รวม</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <!-- สรุป -->
  <div>
    <label>ยอดรวม</label>
    <input type="text" id="subtotal" readonly>
  </div>
  <div>
    <label>ส่วนลด (%)</label>
    <input type="number" id="discount_percent" value="0"
           data-on="input:LineItemsManager.recalculate">
  </div>
  <div>
    <label>ส่วนลด (บาท)</label>
    <input type="text" id="discount_amount" readonly>
  </div>
  <div>
    <label>หลังหักส่วนลด</label>
    <input type="text" id="after_discount" readonly>
  </div>
  <div>
    <label>VAT 7%</label>
    <input type="text" id="vat" readonly>
  </div>
  <div>
    <label>ยอดสุทธิ</label>
    <input type="text" id="total" readonly>
  </div>
</form>

<script>
// ปุ่มเพิ่ม VAT 7%
function addVat(currentValue) {
  const price = parseFloat(currentValue) || 0;
  return (price * 1.07).toFixed(2);
}

// คำนวณ Invoice
function calculateInvoice({items}) {
  let subtotal = 0;

  const updatedItems = items.map(item => {
    const qty = parseFloat(item.quantity) || 0;
    const price = parseFloat(item.unit_price) || 0;
    const itemSubtotal = qty * price;
    subtotal += itemSubtotal;

    return {subtotal: itemSubtotal.toFixed(2)};
  });

  const discountPercent = parseFloat(document.getElementById('discount_percent')?.value) || 0;
  const discount = subtotal * (discountPercent / 100);
  const afterDiscount = subtotal - discount;
  const vat = afterDiscount * 0.07;
  const total = afterDiscount + vat;

  return {
    items: updatedItems,
    '#subtotal': subtotal.toFixed(2),
    '#discount_amount': discount.toFixed(2),
    '#after_discount': afterDiscount.toFixed(2),
    '#vat': vat.toFixed(2),
    '#total': total.toFixed(2)
  };
}
</script>

แนวทางปฏิบัติที่ดี

1. ใช้ data-role แทน name

ไม่ควร:

<input name="product" data-api-param="product">

ควร:

<input name="product_text" data-role="product">

2. กำหนด data-field ชัดเจน

ไม่ควร:

<th>สินค้า</th>

ควร:

<th data-field="name">สินค้า</th>

3. ใช้ data-display-only สำหรับฟิลด์ที่ไม่ต้องแก้ไข

ไม่ควร:

<th data-field="sku" data-readonly="true">รหัส</th>  <!-- สร้าง readonly input -->

ควร:

<th data-field="sku" data-display-only="true">รหัส</th>  <!-- แสดงข้อความ -->

4. ใช้ data-readonly สำหรับฟิลด์ที่ต้องส่งฟอร์ม

ถูกต้อง:

<th data-field="subtotal" data-type="currency" data-readonly="true">รวม</th>
<!-- สร้าง readonly input สำหรับส่งฟอร์ม -->

5. ตรวจสอบ API Response Format

Response ที่ดี:

{
  "success": true,
  "data": {
    "sku": "P001",
    "name": "สินค้า A",
    "quantity": 1,
    "unit_price": 100.00
  }
}

6. จัดการ Error ใน Callback

ฟังก์ชันที่ดี:

function calculateItems({items, instance}) {
  try {
    // คำนวณ
    return {items: updatedItems};
  } catch (err) {
    console.error('Calculate error:', err);
    return {items: []};
  }
}

7. ใช้ Event Listeners สำหรับ Logic เพิ่มเติม

table.addEventListener('lineitems:add', (e) => {
  // Logic เพิ่มเติมหลังเพิ่มรายการ
  console.log('เพิ่มรายการ:', e.detail.row);
});

8. ทำความสะอาด Instance เมื่อไม่ใช้งาน

// เมื่อไม่ต้องการใช้งานแล้ว
instance.destroy();

สรุป

LineItemsManager เป็นเครื่องมือที่ทรงพลังสำหรับจัดการรายการสินค้าในเอกสาร โดยให้ความยืดหยุ่นสูง รองรับ:

  • ✅ Event-driven architecture
  • ✅ Flexible API parameters
  • ✅ External calculation callbacks
  • ✅ Auto-load from sources
  • ✅ Merge duplicates
  • ✅ Customizable columns
  • ✅ Rich input types

ทำให้สามารถสร้างฟอร์มเอกสารต่างๆ เช่น PO, Invoice, Receipt ได้อย่างรวดเร็วและมีประสิทธิภาพ