## 1. Settings foundation

- [x] 1.1 Create migration `create_settings_table` (`id`, `key` unique string, `key_group` string, `content` text, timestamps)
- [x] 1.2 Seed the `near_expiry_days` row (`key_group=product`, `content=30`) via the migration or a dedicated seeder run once
- [x] 1.3 Create `App\Models\Setting` with a `scopeKey`/`static get($key, $default)` helper that reads `content` and falls back to `$default` when the row is missing
- [x] 1.4 Add `SettingController@edit`/`@update` for a single-form "Cài đặt chung" page, validating the near-expiry value as a non-negative integer
- [x] 1.5 Add route `GET/POST /settings/general` inside the existing `Route::prefix('settings')` group in `routes/web.php` (POST, not PUT — matches the plain-POST pattern already used by Brand/Unit/Gift settings forms)
- [x] 1.6 ~~Create `resources/views/pages/settings-general.blade.php`~~ — not needed: existing settings pages (Brand/Unit/Gift) render inline via `Admin::content` + `Encore\Admin\Widgets\Form`, no dedicated Blade view file. `SettingController@edit` follows that same pattern.
- [x] 1.7 Add the "Cài đặt chung" entry to the settings menu (via a migration inserting into `admin_menu`, consistent with the additive-migration pattern, instead of re-running the truncating `MenuTableSeeder`)

## 2. Product expiry & promotion columns

- [x] 2.1 Create migration `add_expiry_date_and_promotion_note_to_products_table` adding nullable `expiry_date` (date) and nullable `promotion_note` (text)
- [x] 2.2 Add `expiry_date` and `promotion_note` to `Product::$fillable`

## 3. Product form UI

- [x] 3.1 In `products-add.blade.php`, add the "Ghi chú khuyến mãi" textarea (`promotion_note`) directly below the Số lượng/Đơn vị row
- [x] 3.2 In `products-add.blade.php`, add the "Hạn sử dụng" date input (`expiry_date`) directly above the `attr_weight` (Khối lượng) select, outside the `@foreach($productAttrs...)` loop — implemented as a `bootstrap-datetimepicker` (already vendored under `public/vendor/laravel-admin/`, wired up with `moment.js`) so the visible field displays Vietnamese `dd/mm/yyyy`, while a paired hidden `expiry_date` input carries the real `Y-m-d` value submitted to the server; a plain `type="date"` was rejected because its displayed format follows the browser/OS locale, not the page
- [x] 3.3 Mirror both additions in `products-edit.blade.php`, pre-filling the visible picker from `$item->expiry_date->format('d/m/Y')` and the hidden field from `$item->expiry_date->format('Y-m-d')`; promotion note pre-fills from `$item->promotion_note`
- [x] 3.4 Add `expiry_date` (`nullable|date_format:Y-m-d` — server-side stays on the standard Laravel date format since the hidden field always submits `Y-m-d`) and `promotion_note` (`nullable|string`) to the validation rules in `ProductController@store` and `@update`

## 4. Product list: expiry column, highlighting, filter

- [x] 4.1 In `ProductController@index`, when `?expiring=1` is present, filter products to `whereNotNull('expiry_date')->whereDate('expiry_date', '>=', today())->whereDate('expiry_date', '<=', today()->addDays(Setting::get('near_expiry_days', 30)))` and order by `expiry_date` ascending; otherwise keep the current `orderBy('id','desc')` behavior (implemented via `orderBy`, not `reorder` — the latter doesn't exist in Laravel 5.6's query builder)
- [x] 4.2 Pass the near-expiry threshold (or a per-product "is near expiry" flag) to the `pages.products` view so the table can highlight rows without recomputing the threshold in Blade
- [x] 4.3 Add an "Hạn sử dụng" column to `products.blade.php` after "Ngày tạo", showing the formatted `expiry_date` or blank
- [x] 4.4 ~~Apply a warning highlight to the expiry cell when the product is within the near-expiry threshold and not expired~~ — superseded by a two-tier highlight: `text-danger bold` when `expiry_date` is already in the past, `text-warning bold` when within the near-expiry threshold, computed via `$isExpired`/`$isNearExpiry` in `products.blade.php`
- [x] 4.5 ~~Add the "Sản phẩm gần hết hạn" toggle button/link~~ — superseded: replaced with a `<select name="expiring">` filter dropdown (`''` = all, `'near'` = near-expiry, `'expired'` = expired) that auto-submits on change (pattern reused from `orders.blade.php`'s status filter), because the original toggle button required two clicks to clear and couldn't express "already expired" as a third state. `ProductController@index` branches on `$expiring === 'near' | 'expired' | else` instead of the old boolean `$expiring`.

## 5. POS: promotion note & expiry warning

Unplanned addition surfaced during the same change: the POS "Bán hàng" screen had no visibility into `promotion_note` or expiry status at the point of sale.

- [x] 5.1 Add `getIsExpiredAttribute()` to `App\Models\Product`, appended to JSON via `$appends = ['is_expired']`, so `/pos/scan` responses and the jsrender templates can read it without recomputing the threshold in JS
- [x] 5.2 In `PosController@index`'s inline JS (`addProductRow`), show `promotion_note` (prefixed with a small `fa-gift` icon) under the product name in the search-suggestion dropdown, only when non-empty
- [x] 5.3 In `PosController@index`'s inline JS, show a red `fa-exclamation-triangle` icon (no extra text) next to the product name in the search-suggestion dropdown when `is_expired` is true
- [x] 5.4 Mirror both (promotion note with gift icon, expired warning icon) in the `#scannerItem` jsrender cart-row template in `resources/views/vendor/admin/index.blade.php`, using `[[if promotion_note]]`/`[[if is_expired]]` conditional tags
- [ ] 5.5 Manually verify in a running environment: scanning/searching an expired product shows the warning icon in both the dropdown and the cart line, and a product with a promotion note shows it with the gift icon in both places — not yet verified (no local PHP/Docker environment available in the session that implemented this)

## 6. Verification

Verified against the local Docker environment (migrations run, real Eloquent/Blade rendering via `php artisan tinker`, not just static review), at the time when 4.4/4.5 still had their original (pre-superseded) behavior:

- [x] 6.1 Manually verify: creating/editing a product with and without expiry date + promotion note saves correctly — created products with/without `expiry_date`/`promotion_note`; `products-add`/`products-edit` views render the new fields and edit correctly pre-fills saved values
- [x] 6.2 Manually verify: product list highlights only products within the configured threshold and excludes expired ones — rendered `pages.products` with a near-expiry, an expired, a far-future, and a no-date product: only the near-expiry one got the highlight class. Superseded by the 4.4 rework (expired products are now highlighted too, in red) — not re-verified in Docker; only reviewed statically.
- [x] 6.3 Manually verify: `?expiring=1` returns only non-expired, within-threshold products sorted by `expiry_date` ascending, and clearing it restores the default list — ran the exact filter query used in `ProductController@index`; it returned only the near-expiry product. Superseded by the 4.5 rework (`expiring` is now `'near'`/`'expired'`/absent instead of a boolean) — not re-verified in Docker; only reviewed statically.
- [x] 6.4 Manually verify: updating `near_expiry_days` on `/settings/general` changes which products are highlighted/filtered — called `SettingController@update` with a valid value (persisted) and a negative value (rejected by validation, prior value retained), then restored the default (30)
