## 1. Data layer

- [x] 1.1 Migration: tạo bảng `customer_debts` (`customer_id` FK, `order_id` FK nullable, `type` enum `pos_debt|manual_debt|repayment`, `amount` decimal, `note` string nullable, `created_by` nullable, timestamps)
- [x] 1.2 Migration: thêm cột `debt_total` (decimal, default 0) vào bảng `customers`
- [x] 1.3 Model mới `App\Models\CustomerDebt` (quan hệ `customer()`, `order()`)
- [x] 1.4 `App\Models\Customer`: thêm quan hệ `debts()` (hasMany `CustomerDebt`), thêm `debt_total` vào `$fillable`/`$casts` nếu cần

## 2. Ledger accounting core (dùng chung cho POS, ghi nợ tay, thu nợ)

- [x] 2.1 Viết helper tập trung (ví dụ `CustomerDebt::record($customer, $type, $amount, $orderId = null, $note = null)`) — chạy trong `DB::transaction`, `lockForUpdate()` trên dòng `customers`, tạo `customer_debts` entry, cộng/trừ `debt_total` theo `$type` (`pos_debt`/`manual_debt` cộng, `repayment` trừ), `save()` lại customer
- [x] 2.2 Viết helper điều chỉnh entry đã tồn tại (ví dụ `CustomerDebt::adjustForOrder($order, $newAmount)`) — tìm `pos_debt` entry theo `order_id`, cập nhật `amount`, điều chỉnh `debt_total` theo phần chênh lệch, trong cùng transaction; nếu chưa có entry và `$newAmount > 0` thì tạo mới qua 2.1
- [x] 2.3 Trong helper thu nợ (`type = repayment`), validate `amount <= debt_total` hiện tại (đọc sau khi đã `lockForUpdate`) — reject nếu vượt, không tạo entry, không đổi `debt_total`

## 3. POS: ghi nợ tại màn Bán hàng

- [x] 3.1 `resources/views/pages/pos.blade.php`: thêm input "Tiền nợ" cạnh "Tiền giảm" trong `#totalPOS` (theo mockup đã cung cấp), bind `data-model="debt"`, name phù hợp để submit cùng form
- [x] 3.2 `PosController` JS (trong `index()`): disable input "Tiền nợ" mặc định; enable khi `fillCustomerInfo()` nhận được khách hàng có `id`, disable lại nếu khách hàng bị bỏ chọn
- [x] 3.3 `OrderController@store`: validate `debt_amount` (nullable|numeric|min:0), reject nếu `debt_amount > 0` mà không có `customer` hợp lệ; nếu `status === 'done'` và `debt_amount > 0` thì gọi helper 2.1 với `type = pos_debt`, `order_id = $order->id`
- [x] 3.4 `OrderController@update`: validate tương tự 3.3; nếu đơn đang `done` (hoặc chuyển sang `done`) thì gọi helper 2.2 (`adjustForOrder`) để cập nhật/điều chỉnh entry thay vì tạo mới; đơn `draft` không tạo/điều chỉnh entry nào
- [x] 3.5 Kiểm tra thủ công: tạo đơn draft với "Tiền nợ" > 0 → không phát sinh nợ; tạo đơn done với "Tiền nợ" > 0 → nợ được cộng; sửa đơn done tăng/giảm/về 0 "Tiền nợ" trong khung giờ editable → nợ được điều chỉnh đúng, không bị nhân đôi

## 4. Ghi nợ độc lập (thủ công)

- [x] 4.1 Route mới (`routes/web.php`) cho action ghi nợ tay (ví dụ `POST /customers/{customer}/debts`)
- [x] 4.2 Controller action tương ứng (`CustomerController` hoặc controller mới cho công nợ): validate `amount` (required|numeric|min:0.01), `note` (nullable|string), gọi helper 2.1 với `type = manual_debt`, `order_id = null`
- [x] 4.3 UI nhập ghi nợ tay (modal hoặc form) — đặt ở trang chi tiết khách hàng và/hoặc màn Danh sách công nợ

## 5. Thu nợ

- [x] 5.1 Route mới cho action thu nợ (ví dụ `POST /customers/{customer}/repayments`), dùng chung cho cả 2 vị trí gọi tới (màn danh sách công nợ + trang chi tiết khách hàng)
- [x] 5.2 Controller action: validate `amount` (required|numeric|min:0.01), gọi helper 2.3 (`type = repayment`); trả lỗi rõ ràng khi vượt `debt_total`
- [x] 5.3 UI nút "Thu nợ" + modal nhập số tiền/ghi chú trên trang chi tiết khách hàng (`customer-statis.blade.php`)
- [x] 5.4 UI nút "Thu nợ" + modal tương tự trên từng dòng của màn Danh sách công nợ

## 6. Màn hình Danh sách công nợ

- [x] 6.1 Route mới (ví dụ `GET /debts`) và controller (mới hoặc thêm vào `CustomerController`) trả về danh sách khách hàng kèm `debt_total`
- [x] 6.2 View mới liệt kê: Tên khách hàng, Tổng số tiền nợ (đọc trực tiếp từ `customers.debt_total`, không `SUM()`), nút "Thu nợ" theo dòng
- [x] 6.3 Chi tiết một khách hàng trên màn này (hoặc liên kết sang trang chi tiết khách hàng có sẵn): danh sách đơn hàng liên quan, query `customer_debts WHERE customer_id = ? AND order_id IS NOT NULL` join `orders`
- [x] 6.4 Thêm liên kết điều hướng tới màn Danh sách công nợ vào menu/sidebar hiện có

## 7. Tích hợp vào trang chi tiết khách hàng hiện có

- [x] 7.1 `CustomerController@statis`: truyền `debt_total` hiện tại của khách hàng ra view
- [x] 7.2 `customer-statis.blade.php`: hiển thị số nợ hiện tại + nút "Thu nợ" (xem 5.3)

## 8. Kiểm thử thủ công tổng thể

- [x] 8.1 Luồng đầy đủ: tạo khách hàng mới → bán hàng ghi nợ tại POS → kiểm tra `debt_total` và ledger đúng → ghi nợ tay thêm → thu nợ một phần từ màn danh sách → thu nợ hết từ trang chi tiết khách hàng → `debt_total` về 0
- [x] 8.2 Thử thu nợ vượt quá `debt_total` hiện tại → bị từ chối, số dư không đổi
- [x] 8.3 Thử ghi nợ tại POS khi chưa chọn khách hàng → input bị disable / submit bị chặn

## 9. Sửa lỗi phát hiện sau khi kiểm thử thủ công (round 1)

- [x] 9.1 Fix: `updatePOS()` ở `PosController.php` build `posData` thiếu key `debt` → `bindings('update')` (thay thế toàn bộ model, không merge) xoá mất giá trị "Tiền nợ" vừa nhập mỗi khi có `model-change` (kể cả blur chính ô đó). Đã đọc lại `debt` hiện tại từ model trước khi gọi update, giống cách `discount` đang làm.
- [x] 9.2 Migration + cột mới `orders.debt_amount` (decimal, default 0) — lưu số nợ đã nhập trực tiếp trên đơn (draft lẫn done), tách biệt khỏi ledger `customer_debts`/`customers.debt_total`, để giải quyết việc lưu nháp "mất" tiền nợ khi mở lại
- [x] 9.3 `OrderController@store`/`@update`: validate `debt_amount <= order->total`, reject với lỗi rõ ràng nếu vượt
- [x] 9.4 `OrderController@update`: khi đơn đã `status=done` trước request này (`$wasDone`), bỏ qua hoàn toàn `debt_amount` gửi lên — không sửa `order->debt_amount`, không tạo/điều chỉnh ledger entry, không đụng `debt_total`. Xoá `CustomerDebt::adjustForOrder()` (không còn dùng)
- [x] 9.5 `OrderController@update` (`create_now_mode`): khi hoàn tất một đơn nháp qua nút "Tạo đơn" (không gửi `debt_amount`), dùng `order->debt_amount` đã lưu từ trước làm giá trị ghi nợ, thay vì mặc định về 0
- [x] 9.6 `pos.blade.php`: thêm dòng chú thích nhỏ dưới label "Tiền nợ" — "Chỉ tính vào công nợ của khách khi đơn hàng được hoàn tất"
- [x] 9.7 `PosController.php` (`fillOrderDraft`): điền `debt` vào `#totalPOS` từ `result.debt_amount` khi mở đơn để sửa; nếu `result.status === 'done'` thì disable `#debtInput` (khoá không cho sửa)
- [x] 9.8 `orders-detail.blade.php`: thêm dòng "Tiền nợ" hiển thị `$item->debt_amount` khi > 0
- [x] 9.9 Cập nhật `design.md` và `specs/pos-debt-entry/spec.md` cho khớp với quyết định mới (khoá sửa nợ sau khi done, cột `orders.debt_amount`, ràng buộc `<= total`)
- [x] 9.10 `PosController.php`: validate `debt_amount` (bắt buộc có khách hàng, không vượt tổng tiền hàng) ngay trong handler click của nút submit (trước khi form thực sự gửi đi), tránh round-trip server làm mất dữ liệu giỏ hàng đang nhập dở. Bỏ qua validate này khi `#debtInput` đang bị khoá (đơn `done` cũ)
- [x] 9.11 `CustomerDebt::voidForOrder()`: khi xoá đơn có `pos_debt` entry còn `amount > 0`, giảm `debt_total` theo `min(amount entry, debt_total hiện tại)` (không trừ thẳng, tránh âm nếu đã có repayment ở giữa) và đặt entry đó về `amount = 0` (giữ lại dòng cho audit)
- [x] 9.12 `OrderController@destroy`: gọi `CustomerDebt::voidForOrder($order)` trước khi `Order::destroy()`
- [x] 9.13 `orders-detail.blade.php`: hộp thoại xác nhận xoá hiển thị rõ số tiền nợ sẽ bị huỷ khi đơn có `debt_amount > 0`, thay vì thông báo xoá chung chung
- [x] 9.14 Cập nhật `design.md` (decision #6, đóng Open Question về xoá đơn có nợ) và `specs/pos-debt-entry/spec.md` (requirement mới về xoá đơn)

## 10. Sửa lỗi cộng nợ/điểm 2 lần khi đơn nhảy qua lại draft ⇄ done

- [x] 10.1 Migration + cột `orders.points_awarded_at` (timestamp, nullable) — đánh dấu đơn đã từng được cộng điểm thưởng, tách khỏi trạng thái `status` hiện tại của đơn
- [x] 10.2 `Order.php`: thêm quan hệ `debts()` (hasMany `CustomerDebt`) và accessor `debt_locked` — kiểm tra thẳng ledger (`đã có pos_debt entry chưa`) thay vì suy ra từ `status === 'done'`, để không bị "mở khoá" khi đơn bị chuyển ngược về draft
- [x] 10.3 `OrderController@store`/`@update`: thay điều kiện khoá nợ từ `$wasDone` (dựa vào trạng thái trước request) sang `$order->debt_locked` (dựa vào ledger) — sống sót qua nhiều lần `draft ⇄ done`
- [x] 10.4 `OrderController@update`: cộng điểm thưởng chỉ khi `!$order->points_awarded_at`, set cột này ngay sau khi cộng — không cộng lại lần 2 dù đơn có chuyển trạng thái qua lại bao nhiêu lần
- [x] 10.5 `PosController.php` (`fillOrderDraft`): đổi điều kiện disable `#debtInput` từ `result.status === 'done'` sang `result.debt_locked`, để đơn draft-đã-từng-done vẫn hiển thị khoá đúng
- [x] 10.6 `Customer::reversePointsForOrder()`: khi xoá đơn đã từng cộng điểm, trừ lại `customer.points` theo `max(0, points - earned_point)` (chặn ở 0, không trừ thẳng vì điểm có thể đã dùng đổi quà)
- [x] 10.7 `OrderController@destroy`: gọi `Customer::reversePointsForOrder($order)` song song với `CustomerDebt::voidForOrder($order)`
- [x] 10.8 `orders-detail.blade.php`: sửa điều kiện cảnh báo nợ khi xoá từ `debt_amount > 0` sang `debt_locked` (tránh cảnh báo sai cho đơn draft chưa từng done); thêm cảnh báo số điểm sẽ bị thu hồi vào cùng hộp thoại xác nhận xoá
- [x] 10.9 Cập nhật `design.md` (mở rộng decision #3 và #6) và `specs/pos-debt-entry/spec.md` (requirement khoá nợ viết lại theo ledger, thêm scenario draft-sau-done, requirement mới về hoàn điểm khi xoá đơn)

## 11. Rà lỗi toàn nhánh (so với develop) + làm rõ trạng thái nợ trên trang chi tiết đơn hàng

- [x] 11.1 `PosController.php`: 2 chỗ validate "Tiền nợ" lúc bấm submit dùng `swal(message, '', 'error')` dạng rút gọn — dính cùng lỗi entity-encoding đã sửa ở `orders-detail.blade.php`; đổi sang dạng object có `html: true`
- [x] 11.2 `OrderController@destroy`: gộp `reversePointsForOrder()` + `voidForOrder()` + `Order::delete()` vào một transaction duy nhất — trước đó nếu bước xoá đơn thất bại sau khi 2 bước hoàn điểm/nợ đã commit riêng, dữ liệu sẽ sai lệch (điểm/nợ đã hoàn nhưng đơn vẫn còn)
- [x] 11.3 `orders-detail.blade.php`: thêm nhãn phân biệt "Đã cộng vào công nợ" (`debt_locked`) và "Chưa cộng — đơn còn nháp" bên cạnh số "Tiền nợ", vì số tiền hiển thị không tự nói lên được đã ảnh hưởng tới `debt_total` của khách hay chưa
- [x] 11.4 Cập nhật `specs/pos-debt-entry/spec.md` (requirement hiển thị "Tiền nợ" ở trang chi tiết đơn hàng, thêm scenario cho cả 2 trạng thái đã/chưa khoá)

## 12. Thiết kế lại màn Danh sách công nợ theo phản hồi thực tế

- [x] 12.1 `customer-statis.blade.php`: bỏ hẳn bảng "ĐƠN HÀNG LIÊN QUAN ĐẾN NỢ" — giữ nguyên cột "Tổng nợ hiện tại" và 2 nút Thu nợ/Ghi nợ tay
- [x] 12.2 `CustomerController@statis`: bỏ query `$debtOrders` không còn dùng tới sau khi bỏ bảng trên
- [x] 12.3 `debts.blade.php`: bỏ cột "Thao tác" và nút "Ghi nợ tay" theo từng dòng — bảng chỉ còn Khách hàng/SĐT/Tổng nợ; mỗi dòng click vào để đi tới trang chi tiết công nợ
- [x] 12.4 `debts.blade.php`: thêm 2 nút "Thu nợ"/"Ghi nợ tay" dùng chung ở đầu trang, mở modal có picker chọn khách hàng (select2 + `/customers/scan`, cùng cơ chế với `#customerKey` ở POS) trước khi nhập số tiền/ghi chú
- [x] 12.5 ~~`DebtController@show` + route `GET /debts/{customer}` + view `debt-detail.blade.php`~~ — **revert ở mục 13**, xem lý do bên dưới
- [x] 12.6 Cập nhật `design.md` (decision #7 mới) và `specs/debt-management-screen/spec.md` (viết lại toàn bộ requirement cho khớp thiết kế màn hình mới)
- [x] 12.7 `debts.blade.php`: sửa lại header — layout `flex` ban đầu đặt breadcrumb làm phần tử flex thứ 3 cạnh nút, phá vỡ quy ước AdminLTE (breadcrumb trôi nổi bên phải trong `.content-header` không-flex); chuyển 2 nút Thu nợ/Ghi nợ tay xuống `.box-header` của bảng thay vì nhét chung với breadcrumb
- [x] 12.8 ~~Thêm link "Xem chi tiết công nợ →" trên `customer-statis.blade.php` trỏ sang `debts.show`~~ — **revert ở mục 13**, xem lý do bên dưới
- [x] 12.9 Cập nhật `specs/debt-management-screen/spec.md` (scenario link điều hướng từ trang khách hàng sang trang chi tiết công nợ)

## 13. Gộp "trang chi tiết công nợ riêng" thành tab "Công nợ" trong chi tiết khách hàng

Sau khi làm xong mục 12, phát hiện đang bị trùng lặp: trang chi tiết công nợ riêng (`/debts/{customer}`) và ý tưởng link từ trang khách hàng sang đó là 2 UI khác nhau hiển thị cùng 1 dữ liệu. Quyết định cuối: gộp thành 1 tab "Công nợ" ngay trong trang khách hàng — không có trang/route riêng nào nữa, tránh trùng UI/query ở 2 nơi.

- [x] 13.1 Xoá `DebtController@show`, route `GET /debts/{customer}` (`debts.show`), và view `debt-detail.blade.php`
- [x] 13.2 `customer-statis.blade.php`: thêm tab "Công nợ" mới (cạnh Thông tin cơ bản/Thống kê nhanh/Đơn hàng đã mua) — nội dung: tổng nợ hiện tại, 2 nút Thu nợ/Ghi nợ tay (scope theo khách hàng, không cần picker), bảng "Danh sách đơn hàng liên quan" (chuyển từ mục 12 sang, dùng lại `$debtOrders`)
- [x] 13.3 Bỏ 2 nút Thu nợ/Ghi nợ tay khỏi vùng header chung của các tab (không còn hiện xuyên suốt mọi tab) và bỏ link "Xem chi tiết công nợ →" (không cần nữa vì tab nằm ngay trên cùng trang)
- [x] 13.4 `CustomerController@statis`: thêm lại query `$debtOrders` (đã xoá ở mục 12.2, giờ cần lại cho tab mới)
- [x] 13.5 `customer-statis.blade.php`: thêm JS — nếu `location.hash === '#debtTab'` lúc load trang thì tự kích hoạt tab "Công nợ" bằng Bootstrap tab API, để link từ nơi khác trỏ thẳng vào đúng tab
- [x] 13.6 `debts.blade.php`: đổi link mỗi dòng từ `route('debts.show', ...)` sang `route('customers.statis', ...) . '#debtTab'`
- [x] 13.7 Cập nhật `design.md` (viết lại decision #7) và `specs/debt-management-screen/spec.md` (viết lại toàn bộ requirement liên quan tới hiển thị chi tiết nợ, dùng khái niệm "tab Công nợ" thay vì "trang chi tiết công nợ riêng")
- [x] 13.8 Fix: 3 trang trong hồ sơ khách hàng (`customer-edit.blade.php` "Thông tin cơ bản", `customer-statis.blade.php` "Thống kê nhanh", `customer-orders.blade.php` "Đơn hàng đã mua") là 3 route riêng biệt, mỗi file tự lặp lại thanh tab của mình (không dùng chung 1 partial) — tab "Công nợ" ở mục 13.2 chỉ được thêm vào đúng 1 trong 3 file, nên đứng ở "Thông tin cơ bản" hay "Đơn hàng đã mua" sẽ không thấy tab đó. Thêm `<li><a href="{{ route('customers.statis',[$item->id]) }}#debtTab">Công nợ</a></li>` vào thanh tab của `customer-edit.blade.php` và `customer-orders.blade.php` (link điều hướng sang trang Thống kê nhanh kèm hash, tận dụng JS tự kích hoạt tab đã có ở mục 13.5) — giữ nguyên bản trong `customer-statis.blade.php` (tab thật, `data-toggle="tab"`)

## 14. Sửa UI màn Danh sách công nợ + tab Công nợ theo phản hồi

- [x] 14.1 `customer-statis.blade.php`: nút "Đổi quà" trước đây nằm ở vùng header dùng chung cho mọi tab (hiện cả khi đang xem tab "Công nợ", không liên quan gì tới nợ) — chuyển vào bên trong `#customerTab` (cạnh link "Quà đã nhận"), chỉ hiện khi đang ở tab "Thống kê nhanh"
- [x] 14.2 `debts.blade.php`: `.box-header` trước đó chỉ có đúng 1 div con (nhóm nút) nằm trong `flex space-between` — không có gì để "space between" nên bị lệch, lại thiếu `is-vcentered` nên nút không canh giữa theo chiều dọc với phần còn lại của header. Thêm `is-vcentered`, thêm ô tìm kiếm ở bên trái để nút bên phải có đối trọng
- [x] 14.3 `debts.blade.php`: thêm ô tìm kiếm theo tên/SĐT (`#debtSearchInput`), lọc trực tiếp bằng JS trên `data-search` của từng dòng (không cần gọi lại server), kèm thông báo "Không tìm thấy khách hàng phù hợp" khi không khớp dòng nào
- [x] 14.4 Cập nhật `specs/debt-management-screen/spec.md` (requirement mới về tìm kiếm trên màn Danh sách công nợ)
- [x] 14.5 Điều tra lỗi `Uncaught SyntaxError` khi pjax điều hướng tới trang "Công nợ" qua menu sidebar — đã kiểm tra kỹ cú pháp JS của `debts.blade.php` và `customer-statis.blade.php` (kể cả mô phỏng nội dung Blade `{{ }}` sau khi render) bằng `node --check`, cả hai đều hợp lệ. Chưa xác định được nguyên nhân gốc qua rà soát tĩnh — cần thêm thông tin tái hiện lỗi cụ thể từ người dùng (xem có phải chỉ xảy ra qua pjax hay cả khi tải thẳng URL) để điều tra tiếp
