# Flowcharts

## auth

# Flowchart — `auth`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-16

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## Request → authentication flow (live: Encore\Admin) 🟢

```mermaid
flowchart TD
    A[HTTP request] --> B{Route matched}
    B -->|"GET /"| C[Route::redirect to /pos - 301]
    B -->|"admin auth routes"| D["Admin::registerAuthRoutes<br/>(login / logout / password)"]
    B -->|"app routes"| E["middleware ['web','admin']"]
    E --> F{admin guard: authenticated?}
    F -->|no| G[redirect to admin login]
    F -->|yes| H{RBAC: permission for path?}
    H -->|denied| I[403 / forbidden]
    H -->|granted| J[Controller action]
    G --> D
```

## Dead framework-auth path (confirmed intentional) 🟢

```mermaid
flowchart TD
    A["Auth\\LoginController / RegisterController<br/>ForgotPassword / ResetPassword"] --> B["config/auth.php: guard 'web'<br/>provider 'users' -> App\\User::class"]
    B --> C{"App\\User exists?"}
    C -->|"NO (missing file)"| D["🟢 dead by design — never reachable:<br/>no Auth::routes() in web.php;<br/>admin-only app, no self-registration"]
```

**Notes:**
- Effective landing: `/` → 301 `/pos`; `HomeController::index` also `redirect()->to('pos')` (its Encore dashboard block is dead code). 🟢
- `RedirectIfAuthenticated` sends already-authenticated users to `/home`. 🟢
- The concrete permission check (node `H`) is implemented inside the vendored `Encore\Admin` package (`admin` middleware). Detail deferred to the Detective. 🟡


## brands

# Flowchart — `brands`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `BrandController` — reference-data editor (Encore\Admin scaffolding) 🟢

```mermaid
flowchart TD
    A["GET settings/brand (index)"] --> B["Admin content: two columns"]
    B --> C["left: grid()"]
    B --> D["right: 'new brand' widget form<br/>name (required), description<br/>POST settings/brand"]
    C --> E["columns: id, name, description"]
    E --> F["disable create/export/selector/filter/pagination"]
    F --> G["row actions:"]
    G --> H["disableDelete() on ALL rows"]
    G --> I{"row key == 1?"}
    I -->|yes| J["disableEdit()<br/>🟢 confirmed default brand"]
    I -->|no| K["edit allowed"]
```

**Notes:**
- `store`/`update`/`destroy` are provided by the Encore\Admin `ModelForm` trait and driven by `form()` (fields `name` required, `description`); only `index`/`edit`/`grid`/`form` are overridden. 🟢
- `Brand::products()` = `hasMany(Product)` via `products.brand_id` (a required FK). Brands are never UI-deletable, and id 1 is edit-locked — confirmed mandatory default brand. 🟢
- No custom algorithms or lifecycle hooks.


## categories

# Flowchart — `categories`

> Produced by the Reversa **Archaeologist** (phase: interpretation) · doc_level: `complete`
> Generated on 2026-09-18

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `CategoryController` — reference-data editor (Encore\Admin scaffolding) 🟢

```mermaid
flowchart TD
    A["GET settings/categories (index)"] --> B["Admin content: two columns"]
    B --> C["left: grid()"]
    B --> D["right: 'new category' widget form<br/>name (required), description<br/>POST settings/categories"]
    C --> E["columns: id, name, description"]
    E --> F["disable create/export/selector/filter/pagination"]
    F --> G["row actions:"]
    G --> H["disableDelete() on ALL rows"]
    G --> I["(id==1 edit-lock is COMMENTED OUT<br/>🟡 all rows remain editable — pending,<br/>grouped with parent_id gap below)"]
```

## Category consumption by `products` (two-level hierarchy) 🟢

```mermaid
flowchart TD
    P["ProductController create()/edit()"] --> Q["Category::whereNotNull('parent_id')<br/>->pluck('name','id')"]
    Q --> R{"category has parent_id?"}
    R -->|yes: child/leaf| S["appears in product category dropdown"]
    R -->|no: top-level group| T["hidden from dropdown"]
    U["store()/update() validation"] --> V["category_id required|exists:categories,id"]
```

**Notes:**
- `store`/`update`/`destroy` are provided by the Encore\Admin `ModelForm` trait and driven by `form()` (fields `name` required, `description`); only `index`/`edit`/`grid`/`form` are overridden. 🟢
- The `categories` table carries a nullable `parent_id`, giving categories a self-referential two-level shape. Products may only be assigned to **child** categories — `ProductController` filters the dropdown with `whereNotNull('parent_id')` (`app/Http/Controllers/ProductController.php:73,157`). Top-level categories (`parent_id` null) act as group headers. 🟢
- 🔴 **GAP / inconsistency (status: pending):** neither the index widget form nor `form()` exposes `parent_id`, so a category created or edited through the UI keeps `parent_id = null` and therefore never appears in the product dropdown. Parent/child wiring must be done directly in the database (or a seed). Team has not yet confirmed whether this is intentional (categories are pre-provisioned) or an unfinished feature — deferred to Detective/PM for a decision.
- `Category::products()` = `hasMany(Product)` via `products.category_id` (a required FK). Category ids `1` (milk / "Sữa") and `8` (medicine / "Thuốc") are special-cased in the customer/order statistics aggregation (`Order::summaryLogging`, `CustomerController::statis`). 🟢
- No custom algorithms or lifecycle hooks in this module.


## customers

# Flowchart — `customers`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `CustomerController::store` — create (web + POS quick-add) 🟢

```mermaid
flowchart TD
    A["POST /customers"] --> B["validate<br/>(fullname, phone numeric unique-live,<br/>gender in male/female/other, birthday d/m/yyyy regex)"]
    B --> C{"parse birthday d/m/Y → birthday2 (Y-m-d)"}
    C -->|ok| D["birthday2 set"]
    C -->|fail| E["catch: birthday = null"]
    D --> F["Customer::create"]
    E --> F
    F --> G{"request expectsJson?"}
    G -->|yes POS| H["JSON customer (or 400)"]
    G -->|no| I["toastr + redirect customers.index"]
```

## `CustomerController::redeemRewardPoints` — gift redemption ✅ (fixed: now atomic)

```mermaid
flowchart TD
    A["POST /customers/id/redeem-points<br/>(gift_id, note)"] --> B["Gift::active()->find(gift_id)"]
    B --> C["customer.checkGiftAvailable(gift)"]
    C --> D{"available?}"}
    D -->|no| E["back with error<br/>(unavailable / over limit / not enough points)"]
    D -->|yes| T["DB::transaction: lockForUpdate(customer, gift)<br/>+ re-check checkGiftAvailable under lock"]
    T --> F["gifts()->attach(gift, {points, note})"]
    F --> G["gift.used += 1"]
    G --> H["customer.points -= gift.points"]
    H --> I["toastr success, redirect back"]
```

✅ Fixed (2026-09-17): steps F–H now run inside `DB::transaction()` with `lockForUpdate()` on both rows, plus a re-check under the lock — a failure or concurrent redemption can no longer desynchronise pivot / `used` / `points`.

## Debt entry points → `CustomerDebt::record` (locked) 🟢

```mermaid
flowchart TD
    A1["POST /customers/id/debts<br/>(manual_debt)"] --> R
    A2["POST /customers/id/repayments<br/>(repayment)"] --> R
    A3["orders store/update<br/>(pos_debt)"] --> R
    R["CustomerDebt::record(customer, type, amount, ...)"]
    R --> T["DB::transaction + lockForUpdate(customer)"]
    T --> U{"type == repayment?"}
    U -->|yes| V{"amount > debt_total?"}
    V -->|yes| W["throw (repayment exceeds balance)"]
    V -->|no| X["debt_total -= amount"]
    U -->|no| Y["debt_total += amount"]
    X --> Z["insert ledger row (balance_after) + save customer"]
    Y --> Z
```

## `CustomerController::statis` — precomputed statistics 🟡

```mermaid
flowchart TD
    A["GET /customers/id/statistic"] --> B["read CustomerOrderSummary row<br/>(built nightly by Order::summaryLogging)"]
    B --> C["try: unpack categories_statistic JSON"]
    C --> D["split milk (cat 1) / medicine (cat 8) / other (aggregated)"]
    C -->|exception| E["degrade to empty collections"]
    D --> F["render customer-statis (amount/owe/debt/points totals)"]
    E --> F
```

**Notes:**
- `scan` (`phone`/`fullname LIKE`, take 10) always returns a 200 collection; its `204` branch is dead (a Collection is always truthy). 🟢 Confirmed harmless by the team — left as-is, not fixed.
- `orders` lists a customer's orders with an optional `category` filter (`whereHas('products')`) and `amountTotal = Σ total`. 🟢
- `debt` paginates the `CustomerDebt` ledger and shows the live `debt_total`. 🟢


## dashboard

# Flowchart — `dashboard`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-16

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `DashboardController::index` 🟢

```mermaid
flowchart TD
    A["GET /dashboard"] --> B["KPIs:<br/>products=count, customers=count,<br/>orders=count(status='done' AND today)"]
    B --> C{"request()->range ?: 'month'"}
    C -->|day| D["buckets: Sáng 06-12 / Trưa 12-18 / Chiều 18-24<br/>🟢 sequential via mutating $startDay (verified correct)"]
    C -->|week| E["one WHEN per day<br/>startOfWeek..endOfWeek"]
    C -->|month| F["5-day windows across month<br/>label 'dd/mm-dd/mm'; yearRange"]
    D --> G["build raw SQL CASE ... END AS range_date"]
    E --> G
    F --> G
    G --> H["Order::select(DB::raw(query))<br/>->groupBy('range_date')<br/>->pluck('quantity','range_date')"]
    H --> I["map rangeData → labelChart / valueChart"]
    I --> J["top products:<br/>order_product ⋈ products (not deleted)<br/>SUM(qty) desc, limit 10"]
    J --> K["view('pages.dashboard', total, chartRange,<br/>range, yearRange, labelChart, valueChart, products)"]
```

**Notes:**
- 🟢 Day-range `CASE`: all three `WHEN` branches reuse `$startDay`, mutated in place by `Carbon::addHours(6)` (Carbon `1.25.*`) at each use. Because PHP evaluates `.` concatenation left-to-right, this yields three correct, non-overlapping 6h windows (Sáng 06-12, Trưa 12-18, Chiều 18-24) — not a bug, though fragile/non-obvious. Verified by manual trace.
- 🟢 Orders in 00:00–06:00 fall outside all buckets and are excluded from the "day" chart — confirmed intentional: the store is closed overnight and orders in that window are negligible.
- 🟢 `App\Models\OrderProduct` and `Cassandra\Custom` imports are unused/erroneous but confirmed harmless (dead imports, never referenced).


## debts

# Flowchart — `debts`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `DebtController::index` — accounts-receivable overview 🟢

```mermaid
flowchart TD
    A["GET /debts (debts.index)"] --> B["header = 'Danh sách công nợ'"]
    B --> C["base query:<br/>Customer where debt_total &gt; 0<br/>order by debt_total desc"]
    C --> D{"q present?"}
    D -->|yes| E["where(closure):<br/>phone LIKE %q% OR fullname LIKE %q%<br/>🟢 grouped — stays AND-scoped<br/>to debt_total &gt; 0"]
    D -->|no| F["(no extra filter)"]
    E --> G["customers = query-&gt;paginate(30)<br/>✅ fixed — was ->get() (no pagination)"]
    F --> G
    G --> H["view('pages.debts', customers)"]
```

**Notes:**
- The page renders two modals whose forms POST to the **customers** module: `POST /customers/{customer}/repayments` (Thu nợ) and `POST /customers/{customer}/debts` (Ghi nợ tay). Those handlers call `CustomerDebt::record(...)` — see `flowcharts/customers.md`. This controller performs **no writes**. 🟢
- Read-only projection over `customers.debt_total` and the `customer_debts` ledger; introduces no entities of its own.


## gifts

# Flowchart — `gifts`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `GiftController::form` — validation & save lifecycle 🟢

```mermaid
flowchart TD
    A["submit new/edit gift<br/>(settings/gifts via ModelForm)"] --> B["validate:<br/>name required<br/>image max:1024 mimes<br/>points/limit nullable numeric min:0"]
    B --> C{"editing existing?"}
    C -->|yes| D["quantity rule =<br/>nullable|numeric|min:{used}<br/>(can't drop stock below redeemed)"]
    C -->|no| E["quantity rule = nullable|numeric"]
    D --> F["saving hook"]
    E --> F
    F --> G["points/limit/quantity ?: 0<br/>used = id ? used : 0"]
    G --> H["persist row"]
    H --> I{"image uploaded?"}
    I -->|yes| J["Image::make(storage/app/public/{image})<br/>-&gt;fit(300,300)-&gt;save()"]
    I -->|no| K["done"]
    J --> K
```

## `GiftController::scan` — redeemable-gift lookup 🟢

```mermaid
flowchart TD
    A["GET settings/gifts/scan?q=&active="] --> B["Gift where name LIKE %q%"]
    B --> C{"has('active')?"}
    C -->|yes| D["where active = request(active)"]
    C -->|no| E["(no active filter)"]
    D --> F["take(10)-&gt;get()"]
    E --> F
    F --> G["json {data: [...]}"]
```

**Notes:**
- Redemption itself is enforced elsewhere: `Customer::checkGiftAvailable` (gift exists, `quantity_available !== 0`, per-customer count `< limit` unless `limit=0`, `points <= customer.points`) and the transactional `CustomerController::redeemRewardPoints` — see `flowcharts/customers.md`. 🟢
- `quantity_available = quantity - used` is computed, not stored; the out-of-stock test uses strict `=== 0`. 🟡


## orders

# Flowchart — `orders`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `OrderController::store` — create order + totalling engine 🟢

```mermaid
flowchart TD
    A["POST /orders (from POS cart)"] --> B["validate items[], customer.phone,<br/>discount_amount, debt_amount, status"]
    B --> C["customer = Customer::code(phone)->first()"]
    C --> D["for each item: find Product by code"]
    D --> E["resolve unit_id → ProductUnit (null if missing)<br/>price = getPriceByCustomerType(type, unit_id)<br/>subtotal += round(price,1)*qty<br/>earned_point += round(reward_point,1)*qty<br/>count++ (lines)"]
    E --> F["total = subtotal - discount_amount; paid = total"]
    F --> G{"debt_amount > 0?"}
    G -->|no customer| H["🚫 redirect: customer required"]
    G -->|debt > total| I["🚫 redirect: debt exceeds total"]
    G -->|ok / 0| J["save order + attach order_product pivots"]
    J --> K{"status == 'done' and customer?"}
    K -->|yes| L["customer.points += earned_point<br/>points_awarded_at = now()"]
    K -->|no| M["skip points"]
    L --> N{"done and debt > 0?"}
    M --> N
    N -->|yes| O["CustomerDebt::record(pos_debt)"]
    N -->|no| P["skip"]
    O --> Q{"status?"}
    P --> Q
    Q -->|draft| R["toastr 'Saved draft' → redirect POS"]
    Q -->|done| S["render pos-print"]
```

## `OrderController::update` — edit / finalise draft 🟢

```mermaid
flowchart TD
    A["PUT /orders/id"] --> B["order = findOrFail; debtLocked = order.debt_locked"]
    B --> C{"create_now_mode?"}
    C -->|yes| D["rebuild items[] from existing pivots<br/>(finalise stored draft without cart)"]
    C -->|no| E["use submitted items"]
    D --> F
    E --> F{"order.is_editable?"}
    F -->|no (done > 24h)| G["🚫 back: cannot edit"]
    F -->|yes| H["recompute totals (same engine as store)<br/>detach + re-attach pivots"]
    H --> I{"debtLocked?"}
    I -->|yes| J["freeze debt_amount (never re-post)"]
    I -->|no| K["apply debt rules (customer req, ≤ total)"]
    J --> L{"done and !points_awarded_at?"}
    K --> L
    L -->|yes| M["award points once, set points_awarded_at"]
    L -->|no| N["skip"]
    M --> O["save; if done & !locked & debt>0 → record pos_debt"]
    N --> O
    O --> P{"draft? → redirect POS · done? → pos-print"}
```

## `OrderController::destroy` — delete with reversals 🟢

```mermaid
flowchart TD
    A["DELETE /orders/id"] --> B["DB::transaction"]
    B --> C["Customer::reversePointsForOrder<br/>(points -= earned_point, clamped ≥ 0, if awarded)"]
    C --> D["CustomerDebt::voidForOrder<br/>(insert debt_void entries, reduce debt_total by<br/>min(entry, balance), flag shortfall in note)"]
    D --> E["order->delete() (hard — no SoftDeletes)"]
    E --> F["order_product cascades; customer_debts.order_id → null"]
    F --> G["JSON status"]
```

**Notes:**
- `count` counts distinct **lines**, not summed quantity. 🟢
- Products with an unknown `code` are silently skipped during totalling. 🟢
- `is_editable` = draft, or `done` within `Order::$limit_hours_editable` (24h) of `updated_at`. 🟢
- `debt_locked` = a `pos_debt` ledger row already exists → debt frozen across `done→draft→done` re-finalisation (posts exactly once). 🟢
- Nightly `Order::summaryLogging()` (scheduler + artisan) rebuilds `customer_order_summary` (`orders_count`, `amount_total`, `points_total`, `categories_statistic` — no `owe_total`, which was removed as unused).


## pos

# Flowchart — `pos`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-16

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `PosController::index` — render terminal 🟢

```mermaid
flowchart TD
    A["GET /pos"] --> B["build admin URLs<br/>(pos/scan, customers/scan,<br/>orders/scan?status=draft, orders, customers.statis)"]
    B --> C{"request()->id set?"}
    C -->|yes| D["load Order draft<br/>+ customer + products.units.unit<br/>→ replace each product.units with buildUnitsPayload"]
    C -->|no| E["no draft"]
    D --> F["Admin::script(pos client JS ~630 lines)"]
    E --> F
    F --> G["view('pages.pos')"]
```

## `PosController::scan` — product lookup 🟢

```mermaid
flowchart TD
    A["GET /pos/scan?q="] --> B{"Product::code(q)->first() ?"}
    B -->|match| C["{ is_barcode:true,<br/>data: product + units payload }"]
    B -->|no match| D["Product name LIKE '%q%' take 10"]
    D --> E["{ is_barcode:false,<br/>data: [products + units payload] }"]
```

## Client cart — add / re-unit / submit (confirmed correct by the team against the live UI) 🟢

```mermaid
flowchart TD
    A["scan / autocomplete select"] --> B["addItem"]
    B --> C{"lineKey = code + '__' + (unit_id||'base')<br/>already in cart?"}
    C -->|yes| D["updateItem: qty += 1, re-price"]
    C -->|no| E["push line, qty=1, fetch /products/get-price"]
    D --> F["updatePOS: subtotal/discount/debt"]
    E --> F
    F --> G["change unit on a line"]
    G --> H{"target lineKey collides?"}
    H -->|yes| I["merge: qty added directly, remove old line"]
    H -->|no| J["relabel line, re-price (qty preserved)"]
    I --> K["submit"]
    J --> K
    K --> L{"debt > 0 and input not disabled?"}
    L -->|yes, no customer| M["🚫 block: must select customer"]
    L -->|yes, debt > total| N["🚫 block: debt cannot exceed total"]
    L -->|ok / debt=0| O["POST/PUT /orders (OrderController)"]
```

**Pricing rule (`Product::getPriceByCustomerType`):** `wholesale_prices[type]` if present else `sale_price`; divided by `conversion_qty` when a non-base unit is selected. 🟢

**Notes:**
- Debt input is locked (server ignores it) when the order is `done` or already has a `pos_debt` ledger entry (`debt_locked`). 🟢
- Persistence, totals, earned points and debt posting happen in `OrderController` (orders module). 🟡


## products

# Flowchart — `products`

> Produced by the Reversa **Archaeologist** (phase: excavation) · doc_level: `complete`
> Generated on 2026-09-17

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `ProductController::index` — listing, expiry filter, search 🟢

```mermaid
flowchart TD
    A["GET /products"] --> B["nearExpiryDays = Setting::get('near_expiry_days', 30)"]
    B --> C{"expiring param?"}
    C -->|near| D["expiry_date between today..today+N, asc"]
    C -->|expired| E["expiry_date < today, desc"]
    C -->|none| F["order by id desc"]
    D --> G{"q present?"}
    E --> G
    F --> G
    G -->|yes| H["where(fn: code LIKE %q% OR name LIKE %q%)<br/>✅ fixed — grouped in a closure, stays<br/>AND-scoped to the expiry filter"]
    G -->|no| I["(no search)"]
    H --> J["paginate(30) → pages.products"]
    I --> J
```

## `ProductController::store` / `update` + `syncProductUnits` 🟢

```mermaid
flowchart TD
    A["POST /products (or PUT /products/id)"] --> B["validate<br/>(code unique among live, price, unit,<br/>category exists, brand exists, picture mimes)"]
    B --> C{"code empty?"}
    C -->|yes| D["code = generateCode(category_id)<br/>= 'P' + catId + pad(max(id)+1, 6)"]
    C -->|no| E["keep code"]
    D --> F{"sale_price empty?"}
    E --> F
    F -->|yes| G["sale_price = price"]
    F -->|no| H["keep sale_price"]
    G --> I{"picture uploaded?"}
    H --> I
    I -->|yes| J["store on public disk<br/>(update: unlink old — 🔴 confirmed wrong path 'public2',<br/>old file never deleted; pending team/lead confirmation)"]
    I -->|no| K["skip"]
    J --> L["Product::create / fill+save"]
    K --> L
    L --> M["syncProductUnits(product, product_units[])"]
    M --> N["delete existing units → filter unit_id&&qty>0<br/>→ first is_default wins → bulk insert"]
    N --> O["toastr success, redirect back"]
```

## `ProductController::getPriceByCustomerType` — POS price endpoint 🟢

```mermaid
flowchart TD
    A["GET /products/get-price?product_id&(id|phone)&unit_id"] --> B{"product_id set?"}
    B -->|no| C["throw → 400 JSON"]
    B -->|yes| D["Product::with(units)->findOrFail"]
    D --> E["resolve customer by id or phone (optional)"]
    E --> F["type = customer?->type ?: ''"]
    F --> G["Product::getPriceByCustomerType(type, unit_id)"]
    G --> H["wholesale_prices[type] ?? sale_price<br/>÷ conversion_qty if unit chosen (qty>0)"]
    H --> I["JSON price"]
```

**Notes:**
- `destroy` soft-deletes and the model's `deleted` event renames the row to `(DELETED) …`. 🟢
- `create`/`edit` offer wholesale-price inputs only for wholesale customer types (`Customer::$types` minus `khach_le`). 🟢


## units

# Flowchart — `units`

> Produced by the Reversa **Archaeologist** (phase: interpretation) · doc_level: `complete`
> Generated on 2026-09-18

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

## `UnitController` — reference-data editor (Encore\Admin scaffolding) 🟢

```mermaid
flowchart TD
    A["GET settings/units (index)"] --> B["Admin content: two columns"]
    B --> C["left: grid()"]
    B --> D["right: 'new unit' widget form<br/>name (required), description<br/>POST settings/units"]
    C --> E["columns: id, name, description"]
    E --> F["disable create/export/selector/filter/pagination"]
    F --> G["row actions:"]
    G --> H["disableDelete() on ALL rows"]
    G --> I{"row key == 1?"}
    I -->|yes| J["disableEdit()<br/>🟢 confirmed default unit"]
    I -->|no| K["edit allowed"]
```

## Unit consumption across the app 🟢

```mermaid
flowchart TD
    U["units table"] --> V["Unit::all()->pluck('name','name')<br/>-> products.unit base-unit dropdown (string)"]
    U --> W["Unit::all(['id','name'])<br/>-> conversion-units table (product_units.unit_id)"]
    U --> X["Unit::find(pivot.unit_id)->name<br/>-> pos-print / orders-detail / customer-orders<br/>blade unit labels"]
    W --> Y["product_units.unit_id FK -> units.id"]
    X --> Z["order_product.unit_id FK -> units.id (onDelete set null)"]
```

**Notes:**
- `store`/`update`/`destroy` are provided by the Encore\Admin `ModelForm` trait and driven by `form()` (fields `name` required, `description`); only `index`/`edit`/`grid`/`form` are overridden. 🟢
- Row actions: `disableDelete()` on every row; `disableEdit()` when the row key is `1` (`app/Http/Controllers/UnitController.php:66-71`) — unit id 1 is the confirmed default/fallback unit (this id-1 lock is active here, whereas in `categories` the equivalent block is commented out — see the `categories` module gap). 🟢
- The `Unit` model (`app/Models/Unit.php`) is completely empty (no relations, casts, or fillable); the inverse `ProductUnit::unit()` = `belongsTo(Unit)` lives on `ProductUnit`. 🟢
- 🟡 The `units` table has a nullable `category_id` column that no code reads or writes — an apparently dead/unused column (units are not scoped by category anywhere). Confirm before relying on it.
- Units are consumed in three places: the product base-unit dropdown (`products.unit`, a **string** keyed by name), the conversion-units table (`product_units.unit_id`), and order/print/customer-order-history unit labels (`Unit::find(pivot.unit_id)->name` in `pos-print.blade.php:57`, `orders-detail.blade.php:72`, `customer-orders.blade.php:66`). 🟢
