status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/specs/progress.md
@@
-- Active slice: Phase 6 - admin customer management surfaces
+- Active slice: Phase 6 - admin discounts/content/settings surfaces
@@
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, and token-gated order lookup under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. Storefront search/suggest, analytics, app/webhook APIs, and broader admin REST surfaces are still missing. |
-| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog and operations shell now includes dashboard KPI/date-range reporting, orders-over-time bars, top-product summaries, funnel counters, product index/form, collection index/form, inventory list, order index/detail, order search/status filters, bank-transfer payment confirmation, refund creation, fulfillment creation, and shipment status transitions with auth protection and store scoping. Customer/content/settings admin pages are still missing. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, and token-gated order lookup under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. Storefront search/suggest, analytics, app/webhook APIs, and broader admin REST surfaces are still missing. |
+| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog and operations shell now includes dashboard KPI/date-range reporting, orders-over-time bars, top-product summaries, funnel counters, product index/form, collection index/form, inventory list, order index/detail, order search/status filters, bank-transfer payment confirmation, refund creation, fulfillment creation, shipment status transitions, customer index/detail, customer order history, customer address create/edit/delete/default actions, and order-to-customer links with auth protection and store scoping. Discount/content/settings admin pages are still missing. |
@@
-| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
+| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin customer address management, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
@@
-| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, search, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin product/collection/inventory/order pages, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
-| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, and the Phase 6 admin dashboard are implemented. Phase 6 customer/content/settings admin surfaces are next. |
+| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, search, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin product/collection/inventory/order/customer pages, admin customer address creation, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
+| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard, and Phase 6 admin customer management are implemented. Phase 6 discount/content/settings admin surfaces are next. |
@@
 - 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin")` resolved `http://shop.test/admin` before browser verification.
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin` renders the dashboard on desktop and 390px mobile viewports with KPI cards, chart panel, top-products panel, and funnel counters; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 full-page/pagination/form/testing docs, Flux UI input/select/button/badge/modal/table docs, Laravel relationship aggregate docs, and Pest docs before the admin customer-management changes.
+- 2026-05-04: `php artisan make:livewire Admin/Customers/Index --class --no-interaction`, `php artisan make:livewire Admin/Customers/Show --class --no-interaction`, and `php artisan make:test --pest Admin/CustomerManagementTest --no-interaction` created the admin customer slice files.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin customer-management changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Admin/CustomerManagementTest.php` passed: 4 tests, 26 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Admin` passed after the admin customer-management changes: 11 tests, 69 assertions.
+- 2026-05-04: `php artisan route:list --path=admin/customers` confirmed `admin.customers.index` and `admin.customers.show` Livewire routes.
+- 2026-05-04: `npm run build` passed after the customer admin Blade/sidebar changes.
+- 2026-05-04: `php artisan test --compact` passed after the admin customer-management changes: 137 tests, 667 assertions.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin customer-management changes.
+- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/customers")` resolved `http://shop.test/admin/customers` before browser verification.
+- 2026-05-04: Playwright MCP verified `http://shop.test/admin/customers` renders the customer table, seeded customer detail renders at `/admin/customers/1`, and the address modal creates a default address; current Playwright console checks reported no warnings/errors.
@@
 - Admin order API routes currently use the existing session `auth` middleware and store-user membership checks because Sanctum is not installed yet.
 - Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; visitor counts remain zero until the analytics event tables are implemented.
+- Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.
@@
-- Theme/page/navigation admin management UI is still missing even though the Phase 3 data layer, seeders, services, and storefront consumption exist.
+- Discount and theme/page/navigation admin management UI is still missing even though the relevant data layer, seeders, services, and storefront/checkout consumption exist.
@@
-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order management, and order API surfaces are implemented, with known auth/token, media UI, customer/content/settings admin UI, search/analytics/app/webhook API, browser-test, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer management, and order API surfaces are implemented, with known auth/token, media UI, discount/content/settings admin UI, search/analytics/app/webhook API, browser-test, and database CHECK constraint gaps tracked above.
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/specs/progress.md

type: update

unified_diff:
@@ -9,3 +9,3 @@
 - Status: in progress
-- Active slice: Phase 6 - admin customer management surfaces
+- Active slice: Phase 6 - admin discounts/content/settings surfaces
 - Started from: Laravel Livewire/Fortify starter kit with no shop domain tables or models present
@@ -29,10 +29,10 @@
 | Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables, Phase 4 cart/checkout/pricing tables, and Phase 5 order/payment/fulfillment tables are implemented: customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines. Search/analytics/app tables are still missing. |
-| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, and token-gated order lookup under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. Storefront search/suggest, analytics, app/webhook APIs, and broader admin REST surfaces are still missing. |
-| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog and operations shell now includes dashboard KPI/date-range reporting, orders-over-time bars, top-product summaries, funnel counters, product index/form, collection index/form, inventory list, order index/detail, order search/status filters, bank-transfer payment confirmation, refund creation, fulfillment creation, and shipment status transitions with auth protection and store scoping. Customer/content/settings admin pages are still missing. |
+| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, and token-gated order lookup under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. Storefront search/suggest, analytics, app/webhook APIs, and broader admin REST surfaces are still missing. |
+| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin catalog and operations shell now includes dashboard KPI/date-range reporting, orders-over-time bars, top-product summaries, funnel counters, product index/form, collection index/form, inventory list, order index/detail, order search/status filters, bank-transfer payment confirmation, refund creation, fulfillment creation, shipment status transitions, customer index/detail, customer order history, customer address create/edit/delete/default actions, and order-to-customer links with auth protection and store scoping. Discount/content/settings admin pages are still missing. |
 | Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, search, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, and customer order list/detail views render seeded and runtime data. |
-| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
+| Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `CartService`, `DiscountService`, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, admin dashboard order aggregates, admin customer address management, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
 | Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout, customer guard/provider, customer login/register, login rate limiting, session hardening, and resource policies implemented. Customer password reset and Sanctum token auth still missing. |
 | Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access, 6 collections, 25 products, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files, 2 theme settings rows, 6 pages, 4 navigation menus, 17 navigation items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, and no product media/runtime carts. Order/payment seed data is still missing. |
-| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, search, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin product/collection/inventory/order pages, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
-| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, and the Phase 6 admin dashboard are implemented. Phase 6 customer/content/settings admin surfaces are next. |
+| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, search, DB-backed content pages, seeded navigation, admin dashboard on desktop/mobile, admin product/collection/inventory/order/customer pages, admin customer address creation, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated browser tests are still missing. |
+| Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard, and Phase 6 admin customer management are implemented. Phase 6 discount/content/settings admin surfaces are next. |

@@ -160,2 +160,13 @@
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin` renders the dashboard on desktop and 390px mobile viewports with KPI cards, chart panel, top-products panel, and funnel counters; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 full-page/pagination/form/testing docs, Flux UI input/select/button/badge/modal/table docs, Laravel relationship aggregate docs, and Pest docs before the admin customer-management changes.
+- 2026-05-04: `php artisan make:livewire Admin/Customers/Index --class --no-interaction`, `php artisan make:livewire Admin/Customers/Show --class --no-interaction`, and `php artisan make:test --pest Admin/CustomerManagementTest --no-interaction` created the admin customer slice files.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the admin customer-management changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Admin/CustomerManagementTest.php` passed: 4 tests, 26 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Admin` passed after the admin customer-management changes: 11 tests, 69 assertions.
+- 2026-05-04: `php artisan route:list --path=admin/customers` confirmed `admin.customers.index` and `admin.customers.show` Livewire routes.
+- 2026-05-04: `npm run build` passed after the customer admin Blade/sidebar changes.
+- 2026-05-04: `php artisan test --compact` passed after the admin customer-management changes: 137 tests, 667 assertions.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the admin customer-management changes.
+- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/customers")` resolved `http://shop.test/admin/customers` before browser verification.
+- 2026-05-04: Playwright MCP verified `http://shop.test/admin/customers` renders the customer table, seeded customer detail renders at `/admin/customers/1`, and the address modal creates a default address; current Playwright console checks reported no warnings/errors.

@@ -190,2 +201,3 @@
 - Admin dashboard sales and top-product metrics are derived from paid or partially refunded orders; visitor counts remain zero until the analytics event tables are implemented.
+- Customer addresses are scoped through the locked customer id because `customer_addresses` intentionally belongs to customers and has no direct `store_id` column.

@@ -200,3 +212,3 @@
 - Product options can be displayed and edited as text in the admin form, but full option matrix generation and option-value reassignment UI are not complete.
-- Theme/page/navigation admin management UI is still missing even though the Phase 3 data layer, seeders, services, and storefront consumption exist.
+- Discount and theme/page/navigation admin management UI is still missing even though the relevant data layer, seeders, services, and storefront/checkout consumption exist.
 - Storefront search/suggest, analytics event capture, app/webhook APIs, and broader admin REST endpoints outside order management are still missing.
@@ -208,2 +220,2 @@

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order management, and order API surfaces are implemented, with known auth/token, media UI, customer/content/settings admin UI, search/analytics/app/webhook API, browser-test, and database CHECK constraint gaps tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer order views, admin dashboard/order/customer management, and order API surfaces are implemented, with known auth/token, media UI, discount/content/settings admin UI, search/analytics/app/webhook API, browser-test, and database CHECK constraint gaps tracked above.

status: completed


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


message: The progress record now includes customer management and moves the active gap to discounts/content/settings. I’m doing final worktree and diff checks before committing the customer slice.

phase: commentary


type: message

role: assistant

text: The progress record now includes customer management and moves the active gap to discounts/content/settings. I’m doing final worktree and diff checks before committing the customer slice.


command:
/bin/zsh
-lc
git diff --stat

type: unknown

cmd: git diff --stat

aggregated_output:
 resources/views/layouts/app/sidebar.blade.php      |  4 ++++
 .../views/livewire/admin/orders/show.blade.php     |  5 ++++
 routes/web.php                                     |  4 ++++
 specs/progress.md                                  | 28 +++++++++++++++-------
 4 files changed, 33 insertions(+), 8 deletions(-)


command:
/bin/zsh
-lc
git status --short

cmd: git status --short

aggregated_output:
 M resources/views/layouts/app/sidebar.blade.php
 M resources/views/livewire/admin/orders/show.blade.php
 M routes/web.php
 M specs/progress.md
?? app/Livewire/Admin/Customers/
?? resources/views/livewire/admin/customers/
?? tests/Feature/Admin/CustomerManagementTest.php


command:
/bin/zsh
-lc
git diff --check

cmd: git diff --check


output:
Chunk ID: c2b1b6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 68
Output:
 M resources/views/layouts/app/sidebar.blade.php
 M resources/views/livewire/admin/orders/show.blade.php
 M routes/web.php
 M specs/progress.md
?? app/Livewire/Admin/Customers/
?? resources/views/livewire/admin/customers/
?? tests/Feature/Admin/CustomerManagementTest.php


output:
Chunk ID: f9648d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


output:
Chunk ID: c2f659
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 80
Output:
 resources/views/layouts/app/sidebar.blade.php      |  4 ++++
 .../views/livewire/admin/orders/show.blade.php     |  5 ++++
 routes/web.php                                     |  4 ++++
 specs/progress.md                                  | 28 +++++++++++++++-------
 4 files changed, 33 insertions(+), 8 deletions(-)


cmd: git add app/Livewire/Admin/Customers resources/views/livewire/admin/customers tests/Feature/Admin/CustomerManagementTest.php resources/views/layouts/app/sidebar.blade.php resources/views/livewire/admin/orders/show.blade.php routes/web.php specs/progress.md


command: git add app/Livewire/Admin/Customers resources/views/livewire/admin/customers tests/Feature/Admin/CustomerManagementTest.php resources/views/layouts/app/sidebar.blade.php resources/views/livewire/admin/orders/show.blade.php routes/web.php specs/progress.md


output:
Chunk ID: 594fe5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


aggregated_output:
A  app/Livewire/Admin/Customers/Index.php
A  app/Livewire/Admin/Customers/Show.php
M  resources/views/layouts/app/sidebar.blade.php
A  resources/views/livewire/admin/customers/index.blade.php
A  resources/views/livewire/admin/customers/show.blade.php
M  resources/views/livewire/admin/orders/show.blade.php
M  routes/web.php
M  specs/progress.md
A  tests/Feature/Admin/CustomerManagementTest.php


command:
/bin/zsh
-lc
git diff --cached --stat

cmd: git diff --cached --stat

aggregated_output:
 app/Livewire/Admin/Customers/Index.php             |  66 +++++++
 app/Livewire/Admin/Customers/Show.php              | 207 +++++++++++++++++++++
 resources/views/layouts/app/sidebar.blade.php      |   4 +
 .../views/livewire/admin/customers/index.blade.php |  57 ++++++
 .../views/livewire/admin/customers/show.blade.php  | 184 ++++++++++++++++++
 .../views/livewire/admin/orders/show.blade.php     |   5 +
 routes/web.php                                     |   4 +
 specs/progress.md                                  |  28 ++-
 tests/Feature/Admin/CustomerManagementTest.php     | 164 ++++++++++++++++
 9 files changed, 711 insertions(+), 8 deletions(-)


command:
/bin/zsh
-lc
git diff --cached --check

cmd: git diff --cached --check


output:
Chunk ID: bc5495
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 99
Output:
A  app/Livewire/Admin/Customers/Index.php
A  app/Livewire/Admin/Customers/Show.php
M  resources/views/layouts/app/sidebar.blade.php
A  resources/views/livewire/admin/customers/index.blade.php
A  resources/views/livewire/admin/customers/show.blade.php
M  resources/views/livewire/admin/orders/show.blade.php
M  routes/web.php
M  specs/progress.md
A  tests/Feature/Admin/CustomerManagementTest.php


output:
Chunk ID: e41dca
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 165
Output:
 app/Livewire/Admin/Customers/Index.php             |  66 +++++++
 app/Livewire/Admin/Customers/Show.php              | 207 +++++++++++++++++++++
 resources/views/layouts/app/sidebar.blade.php      |   4 +
 .../views/livewire/admin/customers/index.blade.php |  57 ++++++
 .../views/livewire/admin/customers/show.blade.php  | 184 ++++++++++++++++++
 .../views/livewire/admin/orders/show.blade.php     |   5 +
 routes/web.php                                     |   4 +
 specs/progress.md                                  |  28 ++-
 tests/Feature/Admin/CustomerManagementTest.php     | 164 ++++++++++++++++
 9 files changed, 711 insertions(+), 8 deletions(-)


output:
Chunk ID: b1d246
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


cmd: git commit -m "Add admin customer management"


command: git commit -m "Add admin customer management"

aggregated_output:
[2026-05-04-codex-5-5-goal 33f99a3d] Add admin customer management
 9 files changed, 711 insertions(+), 8 deletions(-)
 create mode 100644 app/Livewire/Admin/Customers/Index.php
 create mode 100644 app/Livewire/Admin/Customers/Show.php
 create mode 100644 resources/views/livewire/admin/customers/index.blade.php
 create mode 100644 resources/views/livewire/admin/customers/show.blade.php
 create mode 100644 tests/Feature/Admin/CustomerManagementTest.php


output:
Chunk ID: 0819a6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 114
Output:
[2026-05-04-codex-5-5-goal 33f99a3d] Add admin customer management
 9 files changed, 711 insertions(+), 8 deletions(-)
 create mode 100644 app/Livewire/Admin/Customers/Index.php
 create mode 100644 app/Livewire/Admin/Customers/Show.php
 create mode 100644 resources/views/livewire/admin/customers/index.blade.php
 create mode 100644 resources/views/livewire/admin/customers/show.blade.php
 create mode 100644 tests/Feature/Admin/CustomerManagementTest.php


message: Committed customer management as `33f99a3d Add admin customer management`. I’m moving to the next admin gap: discount list/form surfaces.


text: Committed customer management as `33f99a3d Add admin customer management`. I’m moving to the next admin gap: discount list/form surfaces.


cmd: sed -n '1238,1465p' specs/03-ADMIN-UI.md


packages: laravel/framework livewire/livewire livewire/flux pestphp/pest

queries: Livewire full page components forms validation pagination tests

queries: Flux form input select radio checkbox switch textarea button badge

queries: Laravel validation enum Rule unique form request Eloquent update create

queries: Pest Livewire form validation component tests


cmd: sed -n '1,260p' database/factories/DiscountFactory.php


cmd: sed -n '1,240p' database/migrations/2026_05_03_225427_create_discounts_table.php


command: sed -n '1238,1465p' specs/03-ADMIN-UI.md

type: read

aggregated_output:
**Livewire component:** `App\Livewire\Admin\Discounts\Index`

**Blade view:** `resources/views/livewire/admin/discounts/index.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `search` | `string` | Search by discount code |
| `statusFilter` | `string` | `all`, `active`, `expired`, `scheduled` |

| Computed Property | Purpose |
|-------------------|---------|
| `discounts` | Paginated, filtered discounts |

#### Layout

- Heading "Discounts" with "Create discount" primary button
- Search input and status filter

#### Table Columns

| Column | Content |
|--------|---------|
| Code | Discount code or "Automatic" label |
| Type | Flux badge: "Code" or "Automatic" |
| Value | e.g., "10%" or "$5.00" or "Free shipping" |
| Usage | "3 / 100" (used / limit) or "3 / unlimited" |
| Status | Flux badge: active=green, expired=red, scheduled=yellow |
| Dates | Start and end dates |

### 10.2 Discount Form

**Route:** `GET /admin/discounts/create` and `GET /admin/discounts/{discount}/edit`

**Livewire component:** `App\Livewire\Admin\Discounts\Form`

**Blade view:** `resources/views/livewire/admin/discounts/form.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `discount` | `?Discount` | The discount being edited |
| `type` | `string` | `code` or `automatic` |
| `code` | `string` | Discount code (if type is code) |
| `valueType` | `string` | `percent`, `fixed`, `free_shipping` |
| `valueAmount` | `?int` | Discount value (percentage or fixed amount) |
| `minimumPurchaseAmount` | `?int` | Minimum cart value for eligibility |
| `specificProductIds` | `array<int>` | Specific products the discount applies to |
| `specificCollectionIds` | `array<int>` | Specific collections the discount applies to |
| `usageLimit` | `?int` | Maximum total uses |
| `onePerCustomer` | `bool` | Limit to one use per customer |
| `startsAt` | `string` | Start datetime |
| `endsAt` | `?string` | End datetime (optional) |
| `isActive` | `bool` | Whether the discount is active |
| `productSearch` | `string` | Search for products to add |
| `collectionSearch` | `string` | Search for collections to add |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves the discount |
| `generateCode()` | Auto-generates a random discount code |
| `addProduct(int $productId)` | Adds product to specific products list |
| `removeProduct(int $productId)` | Removes product |
| `addCollection(int $collectionId)` | Adds collection to specific collections |
| `removeCollection(int $collectionId)` | Removes collection |

#### Layout Wireframe (Single Column)

```
+==================================================================+
| TYPE CARD                                                         |
|  ( ) Discount code - Customers enter a code at checkout          |
|  (o) Automatic discount - Applied automatically at checkout      |
+==================================================================+
| CODE CARD (shown when type = "code")                             |
|  Discount code: [SUMMER20             ]  [Generate]              |
+==================================================================+
| VALUE CARD                                                        |
|  ( ) Percentage                                                   |
|  (o) Fixed amount                                                 |
|  ( ) Free shipping                                                |
|                                                                   |
|  Amount: [5.00                        ] (hidden for free shipping)|
+==================================================================+
| CONDITIONS CARD                                                   |
|  Minimum purchase amount: [0.00       ]                          |
|  (Leave empty for no minimum)                                     |
|                                                                   |
|  Specific products:                                               |
|  [Search products...          ]                                   |
|  +---results dropdown---+                                         |
|  Selected: Blue Shirt [x]  Red Hat [x]                           |
|                                                                   |
|  Specific collections: (same pattern as products)                 |
+==================================================================+
| USAGE LIMITS CARD                                                 |
|  Total usage limit: [100              ] (or leave blank)         |
|  [x] Limit to one use per customer                               |
+==================================================================+
| ACTIVE DATES CARD                                                 |
|  Start date: [2024-01-15 00:00       ]                           |
|  End date:   [2024-02-15 00:00       ] (or leave blank)          |
+==================================================================+
| STATUS                                                            |
|  [switch: ON]  Active                                             |
+==================================================================+
| STICKY SAVE BAR                          [Discard]  [Save]       |
+==================================================================+
```

#### Section Details

**Type section:** Two radio buttons - "Discount code" (description: "Customers enter a code at checkout") and "Automatic discount" (description: "Applied automatically at checkout"). Selecting type is live-bound.

**Code input:** Shown only when type = "code". Text input with placeholder "SUMMER20" and a "Generate" ghost button that auto-generates a random code.

**Value section:** Three radio buttons for value type: Percentage, Fixed amount, Free shipping. When not free shipping, a number input appears for the amount (label changes based on type: "Percentage" or "Amount").

**Conditions section:**
- Minimum purchase amount: number input with description "Leave empty for no minimum"
- Specific products: search input with live debounce. Results appear in a scrollable dropdown. Selected products shown as a list with remove (X) buttons.
- Specific collections: same pattern as products.

**Usage limits section:**
- Total usage limit number input (placeholder: "Unlimited")
- "Limit to one use per customer" checkbox

**Active dates section:**
- Start date: datetime-local input
- End date: datetime-local input with description "Leave empty for no end date"

**Status:** Toggle switch with text label "Active" or "Disabled".

Sticky save bar at the bottom (same pattern as product form).

---

## 11. Settings Pages

### 11.1 General Settings

**Route:** `GET /admin/settings`

**Livewire component:** `App\Livewire\Admin\Settings\General`

**Blade view:** `resources/views/livewire/admin/settings/general.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `storeName` | `string` | Store display name |
| `storeHandle` | `string` | Store handle (read-only or with warning) |
| `defaultCurrency` | `string` | Currency code (e.g., "EUR") |
| `defaultLocale` | `string` | Locale code (e.g., "en") |
| `timezone` | `string` | Timezone string |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves store settings |

#### Layout

Two-column layout per section: left side has section label and description, right side has form fields.

**Store details section:**
- Left: Heading "Store details" + description "Basic information about your store."
- Right: Store name input, Store handle input (disabled, with description: "The store handle cannot be changed after creation.")

Separator between sections.

**Defaults section:**
- Left: Heading "Defaults" + description "Currency, language, and timezone settings."
- Right:
  - Default currency: Select (EUR/USD/GBP and more)
  - Default locale: Select (English/German/French and more)
  - Timezone: Select (populated from PHP timezone list)

Save button at the bottom (primary variant, right-aligned).

### 11.2 Domains Settings

Domains is a tab within the Settings page (`/admin/settings`), not a separate route. The Settings page has tabs: General, Domains, Shipping, Taxes, Checkout, Notifications.

**Livewire component:** `App\Livewire\Admin\Settings\Domains`

**Blade view:** `resources/views/livewire/admin/settings/domains.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `domains` | `Collection` | Store domains |
| `newHostname` | `string` | Hostname for new domain |
| `newType` | `string` | Type for new domain: `storefront`, `admin`, `api` |

| Action | Behavior |
|--------|----------|
| `addDomain()` | Creates a new domain record |
| `removeDomain(int $domainId)` | Removes a domain |
| `setPrimary(int $domainId)` | Sets domain as primary |

#### Layout

- Heading "Domains"
- Table of existing domains:

| Column | Content |
|--------|---------|
| Hostname | Domain hostname |
| Type | Flux badge: storefront/admin/api |
| Primary | Flux badge (green) "Primary" if `is_primary` |
| TLS | Flux badge showing TLS mode |
| Actions | "Set Primary" button, "Delete" button |

- "Add domain" button opens a modal

#### Add Domain Modal

| Property | Value |
|----------|-------|
| Modal name | `add-domain` |
| Max width | md |
| Heading | "Add domain" |


output:
Chunk ID: aea658
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


output:
Chunk ID: 45b465
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2366
Output:

**Livewire component:** `App\Livewire\Admin\Discounts\Index`

**Blade view:** `resources/views/livewire/admin/discounts/index.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `search` | `string` | Search by discount code |
| `statusFilter` | `string` | `all`, `active`, `expired`, `scheduled` |

| Computed Property | Purpose |
|-------------------|---------|
| `discounts` | Paginated, filtered discounts |

#### Layout

- Heading "Discounts" with "Create discount" primary button
- Search input and status filter

#### Table Columns

| Column | Content |
|--------|---------|
| Code | Discount code or "Automatic" label |
| Type | Flux badge: "Code" or "Automatic" |
| Value | e.g., "10%" or "$5.00" or "Free shipping" |
| Usage | "3 / 100" (used / limit) or "3 / unlimited" |
| Status | Flux badge: active=green, expired=red, scheduled=yellow |
| Dates | Start and end dates |

### 10.2 Discount Form

**Route:** `GET /admin/discounts/create` and `GET /admin/discounts/{discount}/edit`

**Livewire component:** `App\Livewire\Admin\Discounts\Form`

**Blade view:** `resources/views/livewire/admin/discounts/form.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `discount` | `?Discount` | The discount being edited |
| `type` | `string` | `code` or `automatic` |
| `code` | `string` | Discount code (if type is code) |
| `valueType` | `string` | `percent`, `fixed`, `free_shipping` |
| `valueAmount` | `?int` | Discount value (percentage or fixed amount) |
| `minimumPurchaseAmount` | `?int` | Minimum cart value for eligibility |
| `specificProductIds` | `array<int>` | Specific products the discount applies to |
| `specificCollectionIds` | `array<int>` | Specific collections the discount applies to |
| `usageLimit` | `?int` | Maximum total uses |
| `onePerCustomer` | `bool` | Limit to one use per customer |
| `startsAt` | `string` | Start datetime |
| `endsAt` | `?string` | End datetime (optional) |
| `isActive` | `bool` | Whether the discount is active |
| `productSearch` | `string` | Search for products to add |
| `collectionSearch` | `string` | Search for collections to add |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves the discount |
| `generateCode()` | Auto-generates a random discount code |
| `addProduct(int $productId)` | Adds product to specific products list |
| `removeProduct(int $productId)` | Removes product |
| `addCollection(int $collectionId)` | Adds collection to specific collections |
| `removeCollection(int $collectionId)` | Removes collection |

#### Layout Wireframe (Single Column)

```
+==================================================================+
| TYPE CARD                                                         |
|  ( ) Discount code - Customers enter a code at checkout          |
|  (o) Automatic discount - Applied automatically at checkout      |
+==================================================================+
| CODE CARD (shown when type = "code")                             |
|  Discount code: [SUMMER20             ]  [Generate]              |
+==================================================================+
| VALUE CARD                                                        |
|  ( ) Percentage                                                   |
|  (o) Fixed amount                                                 |
|  ( ) Free shipping                                                |
|                                                                   |
|  Amount: [5.00                        ] (hidden for free shipping)|
+==================================================================+
| CONDITIONS CARD                                                   |
|  Minimum purchase amount: [0.00       ]                          |
|  (Leave empty for no minimum)                                     |
|                                                                   |
|  Specific products:                                               |
|  [Search products...          ]                                   |
|  +---results dropdown---+                                         |
|  Selected: Blue Shirt [x]  Red Hat [x]                           |
|                                                                   |
|  Specific collections: (same pattern as products)                 |
+==================================================================+
| USAGE LIMITS CARD                                                 |
|  Total usage limit: [100              ] (or leave blank)         |
|  [x] Limit to one use per customer                               |
+==================================================================+
| ACTIVE DATES CARD                                                 |
|  Start date: [2024-01-15 00:00       ]                           |
|  End date:   [2024-02-15 00:00       ] (or leave blank)          |
+==================================================================+
| STATUS                                                            |
|  [switch: ON]  Active                                             |
+==================================================================+
| STICKY SAVE BAR                          [Discard]  [Save]       |
+==================================================================+
```

#### Section Details

**Type section:** Two radio buttons - "Discount code" (description: "Customers enter a code at checkout") and "Automatic discount" (description: "Applied automatically at checkout"). Selecting type is live-bound.

**Code input:** Shown only when type = "code". Text input with placeholder "SUMMER20" and a "Generate" ghost button that auto-generates a random code.

**Value section:** Three radio buttons for value type: Percentage, Fixed amount, Free shipping. When not free shipping, a number input appears for the amount (label changes based on type: "Percentage" or "Amount").

**Conditions section:**
- Minimum purchase amount: number input with description "Leave empty for no minimum"
- Specific products: search input with live debounce. Results appear in a scrollable dropdown. Selected products shown as a list with remove (X) buttons.
- Specific collections: same pattern as products.

**Usage limits section:**
- Total usage limit number input (placeholder: "Unlimited")
- "Limit to one use per customer" checkbox

**Active dates section:**
- Start date: datetime-local input
- End date: datetime-local input with description "Leave empty for no end date"

**Status:** Toggle switch with text label "Active" or "Disabled".

Sticky save bar at the bottom (same pattern as product form).

---

## 11. Settings Pages

### 11.1 General Settings

**Route:** `GET /admin/settings`

**Livewire component:** `App\Livewire\Admin\Settings\General`

**Blade view:** `resources/views/livewire/admin/settings/general.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `storeName` | `string` | Store display name |
| `storeHandle` | `string` | Store handle (read-only or with warning) |
| `defaultCurrency` | `string` | Currency code (e.g., "EUR") |
| `defaultLocale` | `string` | Locale code (e.g., "en") |
| `timezone` | `string` | Timezone string |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves store settings |

#### Layout

Two-column layout per section: left side has section label and description, right side has form fields.

**Store details section:**
- Left: Heading "Store details" + description "Basic information about your store."
- Right: Store name input, Store handle input (disabled, with description: "The store handle cannot be changed after creation.")

Separator between sections.

**Defaults section:**
- Left: Heading "Defaults" + description "Currency, language, and timezone settings."
- Right:
  - Default currency: Select (EUR/USD/GBP and more)
  - Default locale: Select (English/German/French and more)
  - Timezone: Select (populated from PHP timezone list)

Save button at the bottom (primary variant, right-aligned).

### 11.2 Domains Settings

Domains is a tab within the Settings page (`/admin/settings`), not a separate route. The Settings page has tabs: General, Domains, Shipping, Taxes, Checkout, Notifications.

**Livewire component:** `App\Livewire\Admin\Settings\Domains`

**Blade view:** `resources/views/livewire/admin/settings/domains.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `domains` | `Collection` | Store domains |
| `newHostname` | `string` | Hostname for new domain |
| `newType` | `string` | Type for new domain: `storefront`, `admin`, `api` |

| Action | Behavior |
|--------|----------|
| `addDomain()` | Creates a new domain record |
| `removeDomain(int $domainId)` | Removes a domain |
| `setPrimary(int $domainId)` | Sets domain as primary |

#### Layout

- Heading "Domains"
- Table of existing domains:

| Column | Content |
|--------|---------|
| Hostname | Domain hostname |
| Type | Flux badge: storefront/admin/api |
| Primary | Flux badge (green) "Primary" if `is_primary` |
| TLS | Flux badge showing TLS mode |
| Actions | "Set Primary" button, "Delete" button |

- "Add domain" button opens a modal

#### Add Domain Modal

| Property | Value |
|----------|-------|
| Modal name | `add-domain` |
| Max width | md |
| Heading | "Add domain" |


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Components Pagination > Large result set
Components Pagination | livewire/flux@2.x | Source: https://fluxui.dev/components/pagination#livewire/flux

Large result set

When working with large result sets, the pagination component automatically adapts to show a reasonable number of page links. It shows the first and last pages, along with a window of pages around the current page, and adds ellipses for any gaps to ensure efficient navigation through numerous pages.

<flux:pagination :paginator="$orders" />

---

## Testing See also
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## See also

- **[Actions](/docs/4.x/actions)** — Test component actions and interactions
- **[Forms](/docs/4.x/forms)** — Test form submissions and validation
- **[Events](/docs/4.x/events)** — Test event dispatching and listening
- **[Components](/docs/4.x/components)** — Create testable component structure

---

## Components Radio > flux:radio
Components Radio | livewire/flux@2.x | Source: https://fluxui.dev/components/radio#livewire/flux

flux:radio

<ui-table-scroll-area>

Prop

Description

    label

Label text displayed above the radio button. When provided, wraps the radio button in a flux:field component with an adjacent flux:label component. See the field component.

    description

Help text displayed below the radio button. When provided alongside label, appears between the label and radio button within the flux:field wrapper. See the field component.

    value

Value associated with the radio button when used in a group.

    checked

If true, the radio button is selected by default.

    disabled

Prevents user interaction with the radio button.

    icon

Name of the icon to display (for segmented variant).

</ui-table-scroll-area>

<ui-table-scroll-area>

Slot

Description

    default

Custom content for card variant.

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

    data-flux-radio

Applied to the root element for styling and identification.

    data-checked

Applied when the radio button is selected.

</ui-table-scroll-area>

---

## Http Tests Validation Assertions assertInvalid
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertInvalid

Assert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:

```php
$response->assertInvalid(['name', 'email']);
```

You may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:

```php
$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);
```

---

## Components Radio > Buttons
Components Radio | livewire/flux@2.x | Source: https://fluxui.dev/components/radio#livewire/flux

Buttons

Button-style radio options that look like a toolbar. Perfect for action selections and grouped controls where you want a more prominent visual style.

<flux:radio.group variant="buttons" class="w-full *:flex-1" label="Feedback type">    <flux:radio icon="bug-ant" checked>Bug report</flux:radio>    <flux:radio icon="light-bulb">Suggestion</flux:radio>    <flux:radio icon="question-mark-circle">Question</flux:radio></flux:radio.group>

---

## Attribute Validate
Attribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate

The `#[Validate]` attribute associates validation rules with component properties, enabling automatic real-time validation and clean rule declaration.

---

## Redirecting Redirecting to full-page components
Redirecting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/redirecting

## Redirecting to full-page components

Because Livewire uses Laravel's built-in redirection feature, you can use all of the redirection methods available to you in a typical Laravel application.

For example, if you are using a Livewire component as a full-page component for a route like so:

```php
Route::livewire('/posts', 'pages::show-posts');
```

You can redirect to it simply by using the route path:

```php
public function save()
{
    // ...

    $this->redirect('/posts');
}
```

---

## Quickstart Test it out
Quickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart

## Test it out

With everything in place, let's test the component!

Start your Laravel development server if it's not already running:

```shell
php artisan serve
```

Visit `http://localhost:8000/post/create` in your browser (or `http://yourapp.test/post/create` if using Valet, Herd, or a similar tool).

You should see a simple form with two fields and a submit button.

**Try the following:**

1. **Test validation:** Click "Save Post" without filling in any fields. You'll see red error messages appear instantly below each field—no page reload required.

2. **Test submission:** Fill in both fields and click "Save Post". You should see a debug screen showing the values you entered.

This demonstrates Livewire's core power: reactive data binding, real-time validation, and form handling—all written in PHP without touching JavaScript.

---

## Lazy Full-page lazy loading
Lazy | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lazy

## Full-page lazy loading

You can lazy load or defer full-page Livewire components using route methods.

### Lazy loading full pages

Use `->lazy()` to load the component when it enters the viewport:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy();
```

### Deferring full pages

Use `->defer()` to load the component immediately after the page loads:

```php
Route::livewire('/dashboard', 'pages::dashboard')->defer();
```

### Disabling lazy/defer loading

If a component is lazy or deferred by default (via the `#[Lazy]` or `#[Defer]` attribute), you can opt-out using `enabled: false`:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy(enabled: false);
Route::livewire('/dashboard', 'pages::dashboard')->defer(enabled: false);
```

---

## Validation See also
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## See also

- **[Forms](/docs/4.x/forms)** — Validate form inputs with real-time feedback
- **[Properties](/docs/4.x/properties)** — Validate property values before persisting
- **[Validate Attribute](/docs/4.x/attribute-validate)** — Use #[Validate] for property validation
- **[Actions](/docs/4.x/actions)** — Validate data in action methods

---

## Components Checkbox > Buttons
Components Checkbox | livewire/flux@2.x | Source: https://fluxui.dev/components/checkbox#livewire/flux

Buttons

Button-style checkbox options that look like a toolbar. Perfect for toggle controls and feature selections where you want a more prominent visual style.

<flux:checkbox.group wire:model="features" label="Features" variant="buttons">    <flux:checkbox value="notifications" icon="bell" label="Notifications" />    <flux:checkbox value="analytics" icon="chart-bar" label="Analytics" />    <flux:checkbox value="backups" icon="cloud-arrow-up" label="Backups" /></flux:checkbox.group>

---

## Validation Available Validation Rules enum
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### enum

The `Enum` rule is a class-based rule that validates whether the field under validation contains a valid enum value. The `Enum` rule accepts the name of the enum as its only constructor argument. When validating primitive values, a backed Enum should be provided to the `Enum` rule:

```php
use App\Enums\ServerStatus;
use Illuminate\Validation\Rule;

$request->validate([
    'status' => [Rule::enum(ServerStatus::class)],
]);
```

The `Enum` rule's `only` and `except` methods may be used to limit which enum cases should be considered valid:

```php
Rule::enum(ServerStatus::class)
    ->only([ServerStatus::Pending, ServerStatus::Active]);

Rule::enum(ServerStatus::class)
    ->except([ServerStatus::Pending, ServerStatus::Active]);
```

The `when` method may be used to conditionally modify the `Enum` rule:

```php
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;

Rule::enum(ServerStatus::class)
    ->when(
        Auth::user()->isAdmin(),
        fn ($rule) => $rule->only(...),
        fn ($rule) => $rule->only(...),
    );
```

---

## Attribute Title Only for full-page components
Attribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title

## Only for full-page components

> [!info] Full-page components only
> The `#[Title]` attribute only works for full-page components accessed via routes. Regular components rendered within other views don't use titles—they inherit the parent page's title.

---

## Quickstart Write the component
Quickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart

## Write the component

Open `resources/views/pages/post/⚡create.blade.php` and replace its contents with the following:

```blade
<?php

use Livewire\Component;

new class extends Component {
    public string $title = '';

    public string $content = '';

    public function save()
    {
        $this->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        dd($this->title, $this->content);
    }
};
?>

<form wire:submit="save">
    <label>
        Title
        <input type="text" wire:model="title">
        @error('title') <span style="color: red;">{{ $message }}</span> @enderror
    </label>

    <label>
        Content
        <textarea wire:model="content" rows="5"></textarea>
        @error('content') <span style="color: red;">{{ $message }}</span> @enderror
    </label>

    <button type="submit">Save Post</button>
</form>
```

> [!info] Don't worry about styling
> This form is intentionally unstyled so we can focus on Livewire's functionality. In a real application, you'd add CSS or use a framework like Tailwind.

Here's what's happening in the code above:

**Component properties:**
- `public string $title = '';` — Declares a public property for the post title
- `public string $content = '';` — Declares a public property for the post content

**Component methods:**
- `public function save()` — Called when the form is submitted. Validates the data and dumps the output for testing.

**Livewire directives:**
- `wire:submit="save"` — Calls the `save()` method when the form is submitted, preventing the default page reload
- `wire:model="title"` — Creates two-way data binding between the input and the `$title` property. As you type, the property updates automatically
- `wire:model="content"` — Same two-way binding for the textarea and `$content` property
- `@error('title')` and `@error('content')` — Display validation error messages when validation fails

> [!warning] Livewire components MUST have a single root element
> Components must have exactly one root HTML element. In this example, the `<form>` element is the single root. Multiple root elements or HTML comments outside the root element will cause an error. When rendering [full-page components](/docs/4.x/pages), named slots for the layout can be placed outside the root element.

> [!tip] In a real application
> The `save()` method uses `dd()` to dump the values for testing purposes. In a production application, you would typically save the data to a database and redirect:
> ```php
> public function save()
> {
>     $validated = $this->validate([
>         'title' => 'required|max:255',
>         'content' => 'required',
>     ]);
>
>     Post::create($validated); // Assumes you have a Post model and database table
>
>     return $this->redirect('/posts');
> }
> ```

---

## Validation Deprecated `[#Rule]` attribute
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Deprecated `[#Rule]` attribute

When Livewire v3 first launched, it used the term "Rule" instead of "Validate" for it's validation attributes (`#[Rule]`).

Because of naming conflicts with Laravel rule objects, this has since been changed to `#[Validate]`. Both are supported in Livewire v3, however it is recommended that you change all occurrences of `#[Rule]` with `#[Validate]` to stay current.

---

## Components Input > With buttons
Components Input | livewire/flux@2.x | Source: https://fluxui.dev/components/input#livewire/flux

With buttons

Attach buttons to the beginning or end of an input element.

<flux:input.group>    <flux:input placeholder="Post title" />    <flux:button icon="plus">New post</flux:button></flux:input.group><flux:input.group>    <flux:select class="max-w-fit">        <flux:select.option selected>USD</flux:select.option>            </flux:select>    <flux:input placeholder="$99.99" /></flux:input.group>

---

## Components Select > Checkbox indicator
Components Select | livewire/flux@2.x | Source: https://fluxui.dev/components/select#livewire/flux

            Checkbox indicator

If you prefer a checkbox indicator instead of the default checkmark icon, you can use the indicator="checkbox" prop.

<flux:select variant="listbox" indicator="checkbox" multiple>    ...</flux:select>

---

## Components Button > Full width
Components Button | livewire/flux@2.x | Source: https://fluxui.dev/components/button#livewire/flux

Full width

A button that spans the full width of the container.

<flux:button variant="primary" class="w-full">Send invite</flux:button>

---

## Attribute Validate Example: Contact form
Attribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate

## Example: Contact form

Here's a complete contact form with validation:

```php
<?php // resources/views/pages/⚡contact.blade.php

use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Mail;

new class extends Component {
    #[Validate('required|min:2', as: 'name')]
    public $name = '';

    #[Validate('required|email')]
    public $email = '';

    #[Validate('required')]
    public $subject = '';

    #[Validate('required|min:10', as: 'message')]
    public $message = '';

    public function submit()
    {
        $validated = $this->validate();

        Mail::to('support@example.com')->send(new ContactMessage($validated));

        session()->flash('success', 'Message sent successfully!');

        $this->reset();
    }
};
?>

<div>
    @if (session('success'))
        <div class="alert">{{ session('success') }}</div>
    @endif

    <form wire:submit="submit">
        <div>
            <input type="text" wire:model.live.blur="name" placeholder="Your name">
            @error('name') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="email" wire:model.live.blur="email" placeholder="Your email">
            @error('email') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="text" wire:model.live.blur="subject" placeholder="Subject">
            @error('subject') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <textarea wire:model.live.blur="message" placeholder="Your message"></textarea>
            @error('message') <span class="error">{{ $message }}</span> @enderror
        </div>

        <button type="submit">Send Message</button>
    </form>
</div>
```

Users get immediate feedback as they fill out the form, with friendly field names and helpful error messages.

---

## Components Radio > Segmented with icons
Components Radio | livewire/flux@2.x | Source: https://fluxui.dev/components/radio#livewire/flux

Segmented with icons

Combine segmented radio buttons with icon prefixes.

<flux:radio.group wire:model="role" variant="segmented">    <flux:radio label="Admin" icon="wrench" />    <flux:radio label="Editor" icon="pencil-square" />    <flux:radio label="Viewer" icon="eye" /></flux:radio.group>

---

## Validation Accessing errors in JavaScript
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Accessing errors in JavaScript

Livewire provides a `$errors` magic property for client-side access to validation errors:

```blade
<form wire:submit="save">
    <input type="email" wire:model="email">

    <div wire:show="$errors.has('email')">
        <span wire:text="$errors.first('email')"></span>
    </div>

    <button type="submit">Save</button>
</form>
```

### Available methods

- `$errors.has('field')` - Check if a field has errors
- `$errors.missing('field')` - Check if a field doesn't have errors
- `$errors.first('field')` - Get the first error message for a field
- `$errors.get('field')` - Get all error messages for a field
- `$errors.all()` - Get all errors for all fields
- `$errors.clear()` - Clear all errors
- `$errors.clear('field')` - Clear errors for a specific field

When using Alpine.js, access `$errors` through `$wire.$errors`.

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

	<button type="submit">Save</button>
</form>
```

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Validation Available Validation Rules unique:_table_,_column_
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### unique:_table_,_column_

The field under validation must not exist within the given database table.

**Specifying a Custom Table / Column Name:**

Instead of specifying the table name directly, you may specify the Eloquent model which should be used to determine the table name:

```php
'email' => 'unique:App\Models\User,email_address'
```

The `column` option may be used to specify the field's corresponding database column. If the `column` option is not specified, the name of the field under validation will be used.

```php
'email' => 'unique:users,email_address'
```

**Specifying a Custom Database Connection**

Occasionally, you may need to set a custom connection for database queries made by the Validator. To accomplish this, you may prepend the connection name to the table name:

```php
'email' => 'unique:connection.users,email_address'
```

**Forcing a Unique Rule to Ignore a Given ID:**

Sometimes, you may wish to ignore a given ID during unique validation. For example, consider an "update profile" screen that includes the user's name, email address, and location. You will probably want to verify that the email address is unique. However, if the user only changes the name field and not the email field, you do not want a validation error to be thrown because the user is already the owner of the email address in question.

To instruct the validator to ignore the user's ID, we'll use the `Rule` class to fluently define the rule. In this example, we'll also specify the validation rules as an array instead of using the `|` character to delimit the rules:

```php
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

Validator::make($data, [
    'email' => [
        'required',
        Rule::unique('users')->ignore($user->id),
    ],
]);
```

> [!WARNING]
> You should never pass any user controlled request input into the `ignore` method. Instead, you should only pass a system generated unique ID such as an auto-incrementing ID or UUID from an Eloquent model instance. Otherwise, your application will be vulnerable to an SQL injection attack.

Instead of passing the model key's value to the `ignore` method, you may also pass the entire model instance. Laravel will automatically extract the key from the model:

```php
Rule::unique('users')->ignore($user)
```

If your table uses a primary key column name other than `id`, you may specify the name of the column when calling the `ignore` method:

```php
Rule::unique('users')->ignore($user->id, 'user_id')
```

By default, the `unique` rule will check the uniqueness of the column matching the name of the attribute being validated. However, you may pass a different column name as the second argument to the `unique` method:

```php
Rule::unique('users', 'email_address')->ignore($user->id)
```

**Adding Additional Where Clauses:**

You may specify additional query conditions by customizing the query using the `where` method. For example, let's add a query condition that scopes the query to only search records that have an `account_id` column value of `1`:

```php
'email' => Rule::unique('users')->where(fn (Builder $query) => $query->where('account_id', 1))
```

**Ignoring Soft Deleted Records in Unique Checks:**

By default, the unique rule includes soft deleted records when determining uniqueness. To exclude soft deleted records from the uniqueness check, you may invoke the `withoutTrashed` method:

```php
Rule::unique('users')->withoutTrashed();
```

If your model uses a column name other than `deleted_at` for soft deleted records, you may provide the column name when invoking the `withoutTrashed` method:

```php
Rule::unique('users')->withoutTrashed('was_deleted_at');
```

---

## Search Full-Text Search Running Full-Text Queries
Search | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/search.md

### Running Full-Text Queries

Once the index is in place, use the `whereFullText` query builder method to search against it. Laravel will generate the appropriate SQL for your database driver — for example, `MATCH(...) AGAINST(...)` on MariaDB and MySQL, and `to_tsvector(...) @@ plainto_tsquery(...)` on PostgreSQL:

```php
$articles = Article::whereFullText('body', 'web developer')->get();
```

When using MariaDB and MySQL, results are automatically ordered by relevance score. On PostgreSQL, `whereFullText` filters matching records but does not order them by relevance — if you need automatic relevance ordering on PostgreSQL, consider using [Scout's database engine](#database-engine), which handles this for you.

If you created a composite full-text index across multiple columns, you may search against all of them by passing the same array of columns to `whereFullText`:

```php
$articles = Article::whereFullText(
    ['title', 'body'], 'web developer'
)->get();
```

The `orWhereFullText` method may be used to add a full-text search clause as an "or" condition. For complete details, consult the [query builder documentation](/docs/{{version}}/queries#full-text-where-clauses).

---

## Components Field > With badge
Components Field | livewire/flux@2.x | Source: https://fluxui.dev/components/field#livewire/flux

With badge

Badges allow you to enhance a field with additional information such as being "required" or "optional" when it might not be expected.

<flux:field>    <flux:label badge="Required">Email</flux:label>    <flux:input type="email" required />    <flux:error name="email" /></flux:field><flux:field>    <flux:label badge="Optional">Phone number</flux:label>    <flux:input type="phone" placeholder="(555) 555-5555" mask="(999) 999-9999"  />    <flux:error name="phone" /></flux:field>

---

## Forms See also
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## See also

- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback
- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties
- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms
- **[Actions](/docs/4.x/actions)** — Process form submissions with actions
- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission

---

## Validation Testing validation
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Testing validation

Livewire provides useful testing utilities for validation scenarios, such as the `assertHasErrors()` method.

Below is a basic test case that ensures validation errors are thrown if no input is set for the `title` property:

```php
<?php

namespace Tests\Feature\Livewire;

use App\Livewire\CreatePost;
use Livewire\Livewire;
use Tests\TestCase;

class CreatePostTest extends TestCase
{
    public function test_cant_create_post_without_title()
    {
        Livewire::test(CreatePost::class)
            ->set('content', 'Sample content...')
            ->call('save')
            ->assertHasErrors('title');
    }
}
```

In addition to testing the presence of errors, `assertHasErrors` allows you to also narrow down the assertion to specific rules by passing the rules to assert against as the second argument to the method:

```php
public function test_cant_create_post_with_title_shorter_than_3_characters()
{
    Livewire::test(CreatePost::class)
        ->set('title', 'Sa')
        ->set('content', 'Sample content...')
        ->call('save')
        ->assertHasErrors(['title' => ['min:3']]);
}
```

You can also assert the presence of validation errors for multiple properties at the same time:

```php
public function test_cant_create_post_without_title_and_content()
{
    Livewire::test(CreatePost::class)
        ->call('save')
        ->assertHasErrors(['title', 'content']);
}
```

For more information on other testing utilities provided by Livewire, check out the [testing documentation](/docs/4.x/testing).

_[Truncated]_