Tham chiếu

Extension point

Các vị trí trong POS mà app được gắn vào với đơn hàng, sản phẩm, phiếu nhập, phiếu xuất, phiếu chuyển kho; kiểu hiển thị và dữ liệu ngữ cảnh của từng vị trí.

Xem Markdown

Áp dụng cho api_version: "2026-01".

Extension point (target) là vị trí POS mở sẵn để app gắn vào. Mỗi target quy định: hiển thị ở đâu, kiểu hiển thị nào, module nhận dữ liệu ngữ cảnh gì và phải trả về gì.

Cách đặt tên

Target gắn với một loại bản ghi của POS có tên dạng <resource>.<vị trí>, ví dụ order.details.action hay purchase.index.selection-action. Các resource dùng chung tên vị trí, nên app viết cho đơn hàng dùng y hệt cho phiếu nhập hay phiếu chuyển kho. Một số vị trí không có ở mọi resource: sản phẩm không có mẫu in (print-action), khách hàng chỉ có box, action và cột (xem customer.*).

Resource Màn hình POS Danh sách Form chi tiết
order Đơn hàng Danh sách đơn Form đơn
product Sản phẩm Danh sách sản phẩm Modal sản phẩm
purchase Nhập hàng Danh sách phiếu nhập Modal phiếu nhập
export Xuất hàng Danh sách phiếu xuất Modal phiếu xuất
warehouse_transfer Chuyển kho Danh sách phiếu chuyển kho Modal phiếu chuyển kho
customer Khách hàng Danh sách khách hàng Hồ sơ khách hàng

Danh sách target

Target Vị trí trong POS Kiểu
nav.item Menu chính, nhóm "Ứng dụng" Khai báo, không có code
app.page Trang riêng /shop/<shop_id>/apps/<handle>/<path> iframe hoặc Worker UI
<resource>.details.action Menu thao tác của form chi tiết headless, modal, fullscreen, navigate
<resource>.index.action Menu thao tác trên từng dòng của danh sách headless, modal, fullscreen, navigate
<resource>.index.selection-action Thanh thao tác khi chọn nhiều bản ghi headless, modal, fullscreen, navigate
<resource>.details.block Box trong form chi tiết Worker UI
<resource>.before_submit Trước khi tạo/cập nhật bản ghi trên form chi tiết hook, headless
<resource>.after_save Sau khi tạo/cập nhật bản ghi trên form chi tiết hook, headless
<resource>.index.column Cột trong danh sách hook, headless
<resource>.print-action Mẫu in trong ô "Mẫu in" (trừ product) headless, trả HTML
customer.<vị trí> Hồ sơ và danh sách khách hàng: details.block, details.action, index.action, index.selection-action, index.column Xem customer.*

<resource> là một trong order, product, purchase, export, warehouse_transfer. Sản phẩm không có print-action vì chỉ in mã vạch.

Quy ước module

Mọi module là một ES module có export default một hàm. Hàm nhận dữ liệu ngữ cảnh (giống pos.data) và có thể là async.

TypeScript
// Extension có giao diện (block, action modal/fullscreen, trang Worker UI): tự render vào document.body
export default async function (data) {
  render(<App data={data} />, document.body);
}

// Extension headless và hook: trả về kết quả theo target
export default async function (data) {
  return { message: 'Đã xong' };
}

Kiểu dữ liệu dùng chung

Dữ liệu ngữ cảnh có cùng dạng cho mọi resource: data.resource cho biết loại bản ghi, data.record là bản ghi đã lưu (data.records ở thanh thao tác hàng loạt), data.draft là dữ liệu form chi tiết đang hiển thị.

TypeScript
type Resource = 'order' | 'product' | 'purchase' | 'export' | 'warehouse_transfer';

type RefOf<R extends Resource> = {
  order: OrderRef;
  product: ProductRef;
  purchase: PurchaseRef;
  export: ExportRef;
  warehouse_transfer: WarehouseTransferRef;
}[R];

type DraftOf<R extends Resource> = {
  order: OrderDraft;
  product: ProductDraft;
  purchase: PurchaseDraft;
  export: ExportDraft;
  warehouse_transfer: WarehouseTransferDraft;
}[R];

// Form chi tiết: *.details.action, *.details.block, *.before_submit
type DetailsData<R extends Resource> = {
  resource: R;
  mode: 'create' | 'edit';
  record: RefOf<R> | null;   // null khi bản ghi chưa lưu
  draft: DraftOf<R>;         // gồm cả phần chưa lưu
};

// Một dòng của danh sách: *.index.action
type IndexData<R extends Resource> = {
  resource: R;
  record: RefOf<R>;
};

// Thanh thao tác hàng loạt: *.index.selection-action
type SelectionData<R extends Resource> = {
  resource: R;
  records: RefOf<R>[];       // tối đa 500 bản ghi
};

Ví dụ pos.data trong purchase.details.action:

JSON
{
  "resource": "purchase",
  "mode": "edit",
  "record": { "id": "7c1d…", "display_id": 318, "custom_id": null, "status": 0, "warehouse_id": "a52f…", "updated_at": "2026-10-06T02:45:00" },
  "draft": { "status": 0, "warehouse_id": "a52f…", "supplier_products_id": "e09b…", "tags": [], "note": null, "items": [{ "variation_id": "41aa…", "quantity": 20, "imported_price": 1850000 }], "total_price": 37000000 }
}

Mã bản ghi

Resource Mã dùng khi gọi Open API Query do navigate thêm vào
order record.system_id order_id, order_ids
product record.id product_id, product_ids
purchase record.id purchase_id, purchase_ids
export record.id export_id, export_ids
warehouse_transfer record.id warehouse_transfer_id, warehouse_transfer_ids
customer record.id customer_id, customer_ids

Kiểu dữ liệu theo resource

TypeScript
type OrderRef = {
  id: string | number;      // mã đơn hiển thị (mã tuỳ chỉnh nếu shop dùng, ngược lại là mã hệ thống)
  system_id: number;        // mã đơn hệ thống, như trường system_id của đơn trong Open API: /shops/{shop_id}/orders/{system_id}
  status: number;
  updated_at: string;       // ISO 8601
};

type OrderDraft = {
  status: number;
  customer_id: string | null;
  bill_full_name: string | null;
  bill_phone_number: string | null;
  warehouse_id: string | null;
  tags: (number | string)[];
  note: string | null;
  items: {
    variation_id: string;
    product_id: string;
    quantity: number;
    price: number;          // đơn giá sau giảm giá dòng
  }[];
  total_price: number;
};

type ProductRef = {
  id: string;               // UUID: /shops/{shop_id}/products/{id}
  display_id: number;
  custom_id: string | null; // mã sản phẩm
  name: string;
  is_hide: boolean;
  updated_at: string;
};

type ProductDraft = {
  name: string;
  custom_id: string | null;
  category_ids: number[];
  brand_id: number | null;
  tags: (number | string)[];
  variations: {
    id: string | null;      // null với mẫu mã chưa lưu
    custom_id: string | null;
    barcode: string | null;
    retail_price: number;
  }[];
};

type PurchaseRef = {
  id: string;               // UUID: /shops/{shop_id}/purchases/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  warehouse_id: string;
  updated_at: string;
};

type PurchaseDraft = {
  status: number;
  warehouse_id: string | null;
  supplier_products_id: string | null;   // nhà cung cấp
  tags: (number | string)[];
  note: string | null;
  items: {
    variation_id: string;
    quantity: number;
    imported_price: number;
  }[];
  total_price: number;
};

type ExportRef = {
  id: string;               // UUID: /shops/{shop_id}/export/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  type: number;             // loại phiếu xuất theo Open API (xuất thường, xuất hàng lỗi…)
  warehouse_id: string;
  updated_at: string;
};

type ExportDraft = {
  status: number;
  type: number;
  warehouse_id: string | null;
  tags: (number | string)[];
  note: string | null;
  export_items: {
    variation_id: string;
    quantity: number;
  }[];
};

type WarehouseTransferRef = {
  id: string;               // UUID: /shops/{shop_id}/transfers/{id}
  display_id: number;
  custom_id: string | null;
  status: number;
  from_warehouse_id: string;
  to_warehouse_id: string;
  updated_status_at: string;
};

type WarehouseTransferDraft = {
  status: number;
  from_warehouse_id: string | null;
  to_warehouse_id: string | null;
  note: string | null;
  items: {
    variation_id: string;
    quantity: number;
  }[];
};
  • Tên trường và mã trạng thái theo Open API. Trường người dùng không được xem (ví dụ giá nhập khi không có quyền tương ứng) bị bỏ ra; trường nằm trong access.hidden_order_fields bị bỏ khỏi OrderDraft (giá trị thành undefined).
  • Dữ liệu ngữ cảnh chỉ gồm các trường tối thiểu. Cần thêm thì gọi Open API bằng pos.api (xem Runtime API).
  • Nếu bản ghi không thoả giới hạn access của app (kho, danh mục sản phẩm, nguồn đơn…), POS không hiển thị extension cho bản ghi đó.

Thêm mục vào menu chính, mở một trang app.page.

Manifest Mô tả
label Nhãn trên menu
page id của extension app.page

Không có module. Mục chỉ hiện với người được thấy app (mặc định mọi thành viên của shop, xem Vòng đời app).

  1. App thêm mục vào menu chính của POS
  2. Rê chuột để thấy các trang của app, bấm để mở
  3. Trang của app chạy trong iframe trên domain sandbox, dữ liệu lấy qua Open API
  4. Đổi tab trong app thì URL của POS đổi theo
Minh hoạMục menu và trang riêng của appnav.itemapp.page

app.page

Trang riêng của app.

Manifest Mô tả
title Tiêu đề trang
render iframe: SPA tự do; worker: Worker UI, dùng element pos-*
entry File HTML trong gói (khi iframe)
url Trang tự host trên máy chủ của bạn (khi iframe, thay cho entry)
module Module (khi worker)

Dữ liệu ngữ cảnh:

TypeScript
type PageData = {
  path: string;                     // phần sau /apps/<handle>, ví dụ "/returns"
  query: Record<string, string>;    // query string của URL POS
};
  • Trang iframe: URL đồng bộ hai chiều; app đổi route bên trong thì URL của POS đổi theo, nên tải lại trang, nút quay lại và chia sẻ link đều đúng. Trang kết nối với POS qua SDK, xem Runtime API › Trang iframe.
  • Trang Worker UI: worker không có history. Đổi đường dẫn bằng pos.navigate({ page, path, query }); POS cập nhật URL rồi phát data-change với path, query mới, không tạo lại worker. Nút quay lại của trình duyệt cũng phát data-change.
  • Trang Worker UI dựng bằng pos-page, pos-filter-bar, pos-stat, pos-chart, pos-tabs, pos-table cùng các element khác theo Màn hình chuẩn (xem Component). Tiêu đề và nút chính trên thanh tiêu đề của POS đặt qua pos.page như trang iframe (xem Runtime API › Trang Worker UI).
TSX
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';

function Report() {
  const [tab, setTab] = useState(pos.data.query.tab || 'day');
  useEffect(() => pos.on('data-change', (data) => setTab(data.query.tab || 'day')), []);
  return (
    <pos-page heading="Doanh thu" width="full">
      <pos-tabs tabs={[{ id: 'day', label: 'Theo ngày' }, { id: 'source', label: 'Theo nguồn' }]} selected={tab} onChange={(e) => pos.navigate({ page: 'report', query: { tab: e.detail } })}>
        <pos-chart type={tab == 'day' ? 'line' : 'pie'} xKey="label" data={[{ label: '01/10', revenue: 12500000 }]} series={[{ key: 'revenue', label: 'Doanh thu', format: 'money' }]} />
      </pos-tabs>
    </pos-page>
  );
}

export default function () {
  pos.page?.setTitle('Báo cáo doanh thu');
  render(<Report />, document.body);
}

*.details.action

Thêm mục vào menu thao tác của form chi tiết. Các mục của app nằm trong nhóm "Ứng dụng" ở cuối menu, theo thứ tự khai báo.

Resource Menu
order Menu thao tác mở từ nút Lưu của form đơn
purchase, export, warehouse_transfer Menu trạng thái ở chân modal chi tiết
product Nút "Ứng dụng" ở đầu modal sản phẩm, chỉ hiện khi có app thêm action vào đây

Dữ liệu ngữ cảnh: DetailsData<R>.

Với đơn hàng, mục của app không hiện khi menu đang ở chế độ chỉ in hoặc bị ẩn thao tác.

Kiểu action

presentation Khi bấm Module trả về
headless Chạy module trong worker, mục menu hiện trạng thái đang chạy { message?: string, refresh?: boolean } hoặc không trả gì
modal Mở modal kích thước vừa, module render nội dung Không dùng; gọi pos.close() để đóng
fullscreen Mở modal toàn màn hình, đóng lại thì về đúng màn hình cũ Như modal
navigate Chuyển sang trang page với path, kèm mã bản ghi trong query (xem Mã bản ghi) Không có module

Với headless:

  • Trả về message thì POS hiện toast thành công với nội dung đó.
  • Ném lỗi (throw new Error('...')) thì POS hiện toast lỗi với error.message.
  • refresh mặc định true: POS tải lại bản ghi sau khi chạy xong.
  • Thời gian chạy tối đa 30 giây; quá thời gian thì worker bị dừng và báo lỗi.

Các video dưới đây dùng đơn hàng; resource khác hoạt động giống hệt.

  1. Mở đơn, rê chuột vào nút Lưu để mở menu thao tác
  2. Action của app nằm trong nhóm Ứng dụng của menu
  3. Code chạy trong worker, gọi Open API và ERP
  4. Xong thì POS hiện thông báo và tải lại đơn
Minh hoạAction headless trong menu thao tác đơnorder.details.actionpresentation: headless
  1. Chọn action Phiếu bảo hành trong menu thao tác
  2. POS mở modal, nội dung do app vẽ bằng element pos-*
  3. Nhập liệu như form bình thường của POS
  4. App gọi pos.close() và hiện thông báo khi xong
Minh hoạAction mở modalorder.details.actionpresentation: modal
  1. fullscreen: quy trình nhiều bước trong modal toàn màn hình
  2. Đóng lại thì về đúng đơn đang xem
  3. navigate: mở trang của app kèm order_id
Minh hoạAction toàn màn hình và chuyển trangorder.details.actionpresentation: fullscreenpresentation: navigate

should_render

Module should_render nhận cùng dữ liệu ngữ cảnh và trả về boolean.

  • POS tính trước khi bản ghi vừa tải hoặc thay đổi, lưu kết quả theo mã bản ghi và thời điểm cập nhật (updated_status_at với phiếu chuyển kho).
  • Timeout 300 ms; quá thời gian hoặc lỗi thì ẩn mục.
  • Ưu tiên dùng conditions trong manifest; chỉ dùng should_render khi điều kiện phức tạp.

*.index.action

Như *.details.action nhưng nằm trên từng dòng của danh sách.

Resource Menu
order, purchase, export, warehouse_transfer Menu trạng thái trên từng dòng
product Menu "⋯" ở cuối mỗi dòng, chỉ hiện khi có app thêm action vào đây

Dữ liệu ngữ cảnh: IndexData<R>.

  1. Rê chuột vào trạng thái của một đơn trong danh sách
  2. Action của app nằm cuối menu trạng thái
  3. Chạy xong, dòng được gắn thẻ và POS hiện thông báo
Minh hoạAction trên từng dòng danh sách đơnorder.index.action

*.index.selection-action

Mục trên thanh thao tác khi chọn nhiều bản ghi trong danh sách.

Dữ liệu ngữ cảnh: SelectionData<R>. Với navigate, query kèm theo đủ các mã, ví dụ purchase_ids=<id>,<id>,....

  1. Chọn nhiều đơn trong danh sách
  2. Thanh thao tác hàng loạt hiện action của app
  3. App nhận order_ids và mở trang xử lý
Minh hoạAction khi chọn nhiều đơnorder.index.selection-actionpresentation: navigate

*.details.block

Box nằm trong form chi tiết.

Resource Vị trí
order Ngay sau mục Ghi chú
product, purchase, export, warehouse_transfer Cuối modal chi tiết, phía trên các nút ở chân modal
  • Dữ liệu ngữ cảnh: DetailsData<R> (như *.details.action).
  • Khi người dùng sửa form, POS cập nhật pos.data và phát sự kiện data-change (xem Runtime API › Sự kiện).
  • Box có chiều rộng bằng cột chứa nó, chiều cao tự co giãn theo nội dung.
  • Box không chặn hiển thị form: trong lúc worker khởi động, POS hiện khung chờ.
  1. Box của app nằm ngay trong form đơn
  2. POS hiện khung chờ trong lúc worker khởi động
  3. Sửa đơn thì box nhận data-change và cập nhật ngay
Minh hoạBlock trong form đơnorder.details.block

*.after_save

Chạy sau khi tạo hoặc cập nhật bản ghi thành công trên form chi tiết của POS web.

TypeScript
type AfterSaveInput<R extends Resource> = {
  resource: R;
  mode: 'create' | 'edit';
  record: RefOf<R>;
};
  • Không chặn người dùng; giá trị trả về bị bỏ qua.
  • Lỗi chỉ ghi vào nhật ký lỗi của app, không hiện cho người dùng.
  1. Lưu đơn như bình thường
  2. Sau khi lưu, POS báo cho app qua order.after_save
  3. Hook chạy nền, người dùng không phải chờ
Minh hoạHook sau khi lưuorder.after_save

*.before_submit

Chạy trước khi tạo hoặc cập nhật bản ghi trên form chi tiết của POS web.

TypeScript
type BeforeSubmitInput<R extends Resource> = DetailsData<R>;

type BeforeSubmitResult =
  | { ok: true; changes?: BeforeSubmitChanges }
  | { ok: false; message: string };   // message tối đa 200 ký tự, hiện cho người dùng

type BeforeSubmitChanges = {
  note?: string;                      // ghi đè ghi chú, tối đa 2.000 ký tự
  add_tags?: (number | string)[];     // thêm thẻ theo mã thẻ của shop, tối đa 20
};
  • Nhiều app chạy song song. Chỉ cần một hook trả ok: false là bản ghi không được lưu; POS hiện tất cả message.
  • Hết timeout_ms hoặc module lỗi: xử lý theo on_error (allow cho lưu, block chặn lưu).
  • changes chỉ sửa được ghi chú và thêm thẻ. POS áp vào form rồi mới lưu, kèm thông báo tên app đã sửa.
Resource note add_tags
order Ghi chú đơn Thẻ đơn, theo nhóm thẻ của shop như khi chọn tay
purchase, export Ghi chú phiếu Thẻ phiếu
warehouse_transfer Ghi chú phiếu Không có
product Không có Thẻ sản phẩm
  • Trường resource không có thì bị bỏ qua. Mã thẻ không có trong shop hoặc đã gắn thì bỏ qua. Lấy mã thẻ bằng Open API hoặc list_shop_meta của MCP.
  • Hai app trả hai note khác nhau thì POS chặn lưu và báo trùng ghi chú. Thẻ của nhiều app được cộng dồn.
  • changes sai dạng được xử lý như module lỗi (theo on_error).
TypeScript
export default defineExtension('order.before_submit', async (data) => {
  if (data.draft.total_price < 20_000_000) return { ok: true };
  return { ok: true, changes: { add_tags: [12], note: `${data.draft.note ?? ''}\nĐơn lớn: kiểm tra trước khi giao`.trim() } };
});
  1. Bấm Lưu, POS chạy hook của app trước khi lưu
  2. Hook trả ok: false nên đơn không được lưu
  3. Bổ sung số điện thoại rồi lưu lại
  4. Hook trả ok: true, POS lưu đơn như bình thường
Minh hoạHook kiểm tra trước khi lưuorder.before_submit

Khi nào hook chạy

*.before_submit và *.after_save chỉ chạy khi người dùng bấm lưu trên form chi tiết của POS web. Hook không chạy với thay đổi từ nơi khác: menu trạng thái trên dòng của danh sách, cập nhật hàng loạt, nhập Excel, Open API, app mobile hay đồng bộ sàn. Quy tắc bắt buộc đúng mọi lúc cần làm ở phía server.

customer.*

Khách hàng có box, action và cột như các resource khác, nhưng không có before_submit, after_save (hồ sơ khách lưu từng ô ngay khi sửa, không có nút Lưu chung) và mẫu in.

Target Vị trí Dữ liệu ngữ cảnh
customer.details.block Box trong hồ sơ khách, cột phải dưới Ghi chú và Địa chỉ CustomerData
customer.details.action Nút "Ứng dụng" trên đầu hồ sơ khách, chỉ hiện khi có app thêm action vào đây CustomerData
customer.index.action Menu "⋯" cuối mỗi dòng của danh sách khách, chỉ hiện khi có app thêm action vào đây IndexData<'customer'>
customer.index.selection-action Nút "Ứng dụng" trên thanh thao tác khi chọn nhiều khách SelectionData<'customer'>
customer.index.column Cột trong danh sách khách, khoá values là id của khách ColumnData<'customer'>

Hồ sơ khách mở ở trang Khách hàng, từ form đơn và các màn hình khác đều có box và action. Danh sách khách đang lọc khách đã xoá thì không có action, cột của app.

TypeScript
type CustomerRef = {
  id: string;                       // dùng với Open API: /shops/{shop_id}/customers/{id}
  name: string;
  phone_numbers: string[];
  tags: (number | string)[];
  level_id: string | null;
  assigned_user_id: string | null;
  updated_at: string | null;
};

type CustomerData = { resource: 'customer'; record: CustomerRef };   // cùng dạng IndexData<'customer'>
  • Manifest: trường như target cùng vị trí ở resource khác (*.details.action, *.details.block, *.index.column…). Khách hàng không có conditions; cần ẩn theo dữ liệu thì dùng should_render.
  • refresh của action headless và pos.refresh() tải lại trang danh sách khách đang xem; hồ sơ đang mở từ danh sách nhận dữ liệu mới theo. Hồ sơ mở từ form đơn hay màn hình khác chưa tự tải lại.
  • navigate kèm customer_id hoặc customer_ids trong query.
  • Sửa thông tin khách ngay trong hồ sơ thì box nhận data-change với dữ liệu mới.
  • Như *.details.block: worker của box khởi động khi box lọt vào màn hình, trong lúc chờ POS hiện khung chờ.

Mẫu in của app trong ô "Mẫu in", người dùng chọn như mẫu in thường; module trả HTML, POS lọc rồi mở hộp thoại in của trình duyệt.

Resource Ô "Mẫu in"
order Menu thao tác của form đơn (in khi lưu, In F4) và hộp in của thanh thao tác hàng loạt
purchase, export, warehouse_transfer Nút In trên thanh thao tác khi chọn nhiều phiếu

Sản phẩm chỉ in mã vạch nên không có target này.

TypeScript
type PrintResource = 'order' | 'purchase' | 'export' | 'warehouse_transfer';
type PrintData<R extends PrintResource> = { resource: R; records: RefOf<R>[] };   // 1–500 bản ghi
type PrintResult = { html: string; title?: string };                             // html tối đa 2 MB
Manifest Mô tả
label Tên mẫu in, tối đa 40 ký tự
module Module trả PrintResult
  • Chạy như action headless: tối đa 30 giây, gọi được pos.api, pos.toast. Lỗi hoặc kết quả sai dạng thì POS báo không in được và ghi nhật ký lỗi.
  • Bản ghi ngoài giới hạn access của app không gửi cho module. In bằng mẫu của app không cộng số lần in của đơn.
  • Dữ liệu ngữ cảnh chỉ có mã và thông tin tóm tắt (RefOf<R>); cần dòng hàng, khách, tiền thì gọi Open API theo mã bản ghi.
  • HTML được lọc trước khi in: bỏ script, thuộc tính sự kiện (onclick…), iframe, form, link, meta. Bản in chạy trong iframe không có script. Dùng <style> hoặc style="…" để trình bày, @page để đặt khổ giấy, break-after: page để sang trang.
  • Ảnh theo cùng quy tắc với pos-image: data:image/…;base64,…, public/<tên file> trong gói, hoặc URL https:// thuộc network.allowed_origins. Ảnh khác, font và CSS tải từ ngoài bị chặn. Mã vạch, QR nên vẽ bằng SVG hoặc ảnh data:.
TypeScript
export default defineExtension('purchase.print-action', async ({ records }) => {
  const details = await Promise.all(records.map((purchase) => pos.api.get(`/shops/${pos.shop.id}/purchases/${purchase.id}`)));
  const pages = details.map(({ data }) => `<section class="slip"><h1>Phiếu nhập ${data.display_id}</h1><p>${data.items.length} dòng hàng</p></section>`);
  return {
    title: 'Phiếu nhập ERP',
    html: `<style>@page { size: A5; margin: 8mm } .slip { break-after: page }</style>${pages.join('')}`,
  };
});

*.index.column

Cột của app trong danh sách, nằm trước cột cố định bên phải (Trạng thái) hoặc cuối bảng. Có cho cả 5 resource và khách hàng. POS gọi module một lần cho các bản ghi đang hiển thị trên trang, không gọi theo từng dòng.

TypeScript
type ColumnData<R extends Resource> = { resource: R; records: RefOf<R>[] };       // bản ghi trên trang, tối đa 500
type ColumnResult = { values: Record<string, string | number | null> };         // khoá là mã bản ghi

Khoá của values là mã dùng với Open API (xem Mã bản ghi): system_id với đơn, id với resource khác.

Manifest Mô tả
title Tiêu đề cột, tối đa 30 ký tự
module Module trả ColumnResult
format text (mặc định), number, money, date, datetime
align left, center, right
width Độ rộng (px), 60–400, mặc định 140
  • Chạy như hook: timeout 5 giây, chỉ gọi được pos.api. Gom các lệnh gọi Open API bằng Promise.all hoặc dùng endpoint danh sách.
  • Kết quả được nhớ theo bản ghi và thời điểm cập nhật: danh sách tải lại (đổi trang, socket) chỉ gọi module cho bản ghi mới hoặc vừa đổi.
  • Bản ghi không có trong values, bản ghi ngoài giới hạn access và khi module lỗi: ô để trống. Chuỗi dài hơn 200 ký tự bị cắt.
  • Cột của app luôn hiện, chưa nằm trong phần cài đặt cột của danh sách.
TypeScript
export default defineExtension('order.index.column', async ({ records }) => {
  const ids = records.map((order) => order.system_id).join(',');
  const response = await fetch(`https://erp.example.com/pos-orders?ids=${ids}`);
  const erp: Record<string, string> = await response.json();
  return { values: erp };
});