Now.js Framework Documentation
LineItemsManager - จัดการรายการสินค้าในเอกสาร
LineItemsManager - จัดการรายการสินค้าในเอกสาร
เอกสารฉบับนี้อธิบาย LineItemsManager ซึ่งเป็นคอมโพเนนต์สำหรับจัดการรายการสินค้าในเอกสารต่างๆ เช่น ใบสั่งซื้อ ใบเสนอราคา ใบรับสินค้า โดยรองรับการเพิ่ม แก้ไข ลบรายการ และคำนวณราคาอัตโนมัติ
📋 สารบัญ
- ภาพรวม
- การติดตั้งและนำเข้า
- การใช้งานพื้นฐาน
- คุณสมบัติ HTML
- การกำหนดคอลัมน์
- การผูกข้อมูล API
- การรวมรายการซ้ำ
- การโหลดข้อมูลจากแหล่งอื่น
- ระบบคำนวณ
- อีเวนต์
- JavaScript API
- ตัวอย่างการใช้งาน
- แนวทางปฏิบัติที่ดี
ภาพรวม
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เท่านั้น ไม่ใช้ attributename - 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">วิธีการทำงาน:
- ตรวจสอบว่ามี SKU ซ้ำหรือไม่
- ถ้าซ้ำ → บวกจำนวนเข้าไปในแถวเดิม
- ถ้าไม่ซ้ำ → สร้างแถวใหม่
ฟิลด์จำนวนที่รองรับ:
quantityqtyreceived_qtyissued_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>วิธีการทำงาน:
- เมื่อเลือก PO → เรียก
api/po/items?po_id=1 - ล้างรายการเดิม (ถ้า
data-load-clear="true") - เพิ่มรายการจาก 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 ได้อย่างรวดเร็วและมีประสิทธิภาพ