From f2b29d47a1af4ddda7ae3d228b3adb419a9fc9fc Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Fri, 10 Jul 2026 18:51:09 -0400 Subject: [PATCH] =?UTF-8?q?feat(inventory):=20Stage=205B=20=E2=80=94=20inv?= =?UTF-8?q?entory=20page=20UI=20with=20search,=20filter=20chips,=20detail?= =?UTF-8?q?=20panel,=20quantity=20adjust=20dialog,=20and=20snack=20bar=20f?= =?UTF-8?q?eedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development/build_execution_tracker.md | 25 +- docs/development/master_development_brief.md | 10 +- .../test/widgets/inventory_page_test.dart | 259 ++++++++++++++++++ 3 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 kell_creations_apps/packages/feature_inventory/test/widgets/inventory_page_test.dart diff --git a/docs/development/build_execution_tracker.md b/docs/development/build_execution_tracker.md index 4eddc88..710f9fe 100644 --- a/docs/development/build_execution_tracker.md +++ b/docs/development/build_execution_tracker.md @@ -2,9 +2,9 @@ ## Current status -- main baseline updated through: feat/inventory-domain-expansion (Stage 5A complete) -- next branch: feat/inventory-page-ui (Stage 5B) or feat/orders-domain (Stage 5C) -- current stage: Stage 5A complete — inventory domain expansion with write actions +- main baseline updated through: feat/inventory-page-ui (Stage 5B complete) +- next branch: feat/orders-domain (Stage 5C) or feat/bulk-operator-workflows (Stage 7B) +- current stage: Stage 5B complete — inventory page UI with search, filter chips, detail panel, quantity adjust dialog, and snack bar feedback ## Slice tracker @@ -266,6 +266,25 @@ - analyze: passed (dart analyze — no issues found) - brief updated: yes +### feat/inventory-page-ui + +- status: merged to main +- date: 2026-07-10 +- inspection: complete +- implementation: complete +- files changed: + - `feature_inventory/lib/src/presentation/inventory_page.dart` — full inventory page with search bar, status filter chips (All / In Stock / Low Stock / Out of Stock / Draft), item count display, `InventoryItemCard` list, `initialFilter` and `initialQuery` constructor params + - `feature_inventory/lib/src/presentation/widgets/inventory_status_chip.dart` — `InventoryStatusChip` widget mapping `InventoryStatus` to label and color + - `feature_inventory/lib/src/presentation/widgets/inventory_detail_panel.dart` — `InventoryDetailPanel` with name, SKU, description, quantity, price, status chip, supplier, location, reorder point, last updated; Adjust Quantity and View Product action buttons; `isUpdating` loading indicator + - `feature_inventory/lib/src/presentation/widgets/adjust_quantity_dialog.dart` — `AdjustQuantityDialog` with +/- delta input, reason field, and `AdjustQuantityDialogResult` value object + - `feature_inventory/lib/src/presentation/inventory_action_snack_bar.dart` — `showInventoryActionSnackBar()` helper for success/failure feedback + - `feature_inventory/lib/feature_inventory.dart` — expanded barrel exports for all new presentation types + - `feature_inventory/test/widgets/inventory_page_test.dart` — 26 new widget tests covering `InventoryStatusChip` (4), `InventoryDetailPanel` (11), `InventoryPage` (9), `AdjustQuantityDialogResult` (2) + - `kell_creations_apps/tools/run_all_tests.sh` — added `feature_inventory` to `TESTABLE` list +- tests: passed (75/75 feature_inventory, 311/311 feature_wordpress, 24/24 kell_web, 14/14 kell_mobile, 41/41 design_system, 20/20 core — 485 total) +- analyze: passed (dart analyze --fatal-infos — no issues found) +- brief updated: yes + ### chore/analyze-cleanup-and-tracker-sync - status: merged to main diff --git a/docs/development/master_development_brief.md b/docs/development/master_development_brief.md index 287dcc3..406d621 100644 --- a/docs/development/master_development_brief.md +++ b/docs/development/master_development_brief.md @@ -125,16 +125,18 @@ Rules: - `dart analyze` clean - `core` tests passing - `design_system` tests passing +- `feature_inventory` tests passing - `feature_wordpress` tests passing - `kell_web` tests passing - `kell_mobile` tests passing - latest reported count for `core`: `20/20 passed` - latest reported count for `design_system`: `41/41 passed` +- latest reported count for `feature_inventory`: `75/75 passed` - latest reported count for `feature_wordpress`: `311/311 passed` - latest reported count for `kell_web`: `24/24 passed` - latest reported count for `kell_mobile`: `14/14 passed` -- total: `410/410 passed` -- baseline commit: merge of `feat/bulk-status-actions` + analyze cleanup (2026-05-30) +- total: `485/485 passed` +- baseline commit: merge of `feat/inventory-page-ui` (Stage 5B complete, 2026-07-10) #### Baseline test coverage (established 2026-05-22) @@ -150,8 +152,8 @@ No minimum thresholds are enforced — this is visibility-only tracking. Coverag ### Next recommended branch -**`feat/bulk-operator-workflows`** — Stage 7B: Additional bulk actions (bulk publish, bulk move to pending review), or **`feat/integrations-contracts`** — Stage 8A: Integration abstractions. -Branch from latest `main`. Stage 7A (bulk move-to-draft) is complete. Stage 7B adds incremental bulk actions. Stage 8A begins infrastructure activation required for all subsequent feature work (Stages 9–16). +**`feat/orders-domain`** — Stage 5C: Orders domain expansion (read + write operations, matching the pattern established by `feat/inventory-domain-expansion`), or **`feat/bulk-operator-workflows`** — Stage 7B: Additional bulk actions (bulk publish, bulk move to pending review). +Branch from latest `main`. Stage 5B (inventory page UI) is complete. Stage 5C adds write operations and domain expansion to the orders feature. Stage 7B adds incremental bulk actions to the publishing workflow. --- diff --git a/kell_creations_apps/packages/feature_inventory/test/widgets/inventory_page_test.dart b/kell_creations_apps/packages/feature_inventory/test/widgets/inventory_page_test.dart new file mode 100644 index 0000000..4e57389 --- /dev/null +++ b/kell_creations_apps/packages/feature_inventory/test/widgets/inventory_page_test.dart @@ -0,0 +1,259 @@ +import 'package:design_system/design_system.dart'; +import 'package:feature_inventory/feature_inventory.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +Widget _wrap(Widget child) { + return MaterialApp( + theme: buildKcTheme(), + home: Scaffold(body: child), + ); +} + +InventoryItem _makeItem({ + String id = 'test-1', + String sku = 'TEST-001', + String name = 'Test Item', + String description = 'A test item description', + String imageUrl = '', + String category = 'Test Category', + int quantityOnHand = 10, + double unitPrice = 9.99, + int reorderPoint = 3, + String? supplier, + String? location, + InventoryStatus status = InventoryStatus.inStock, + DateTime? lastUpdated, +}) { + return InventoryItem( + id: id, + sku: sku, + name: name, + description: description, + imageUrl: imageUrl, + category: category, + quantityOnHand: quantityOnHand, + unitPrice: unitPrice, + reorderPoint: reorderPoint, + supplier: supplier, + location: location, + status: status, + lastUpdated: lastUpdated ?? DateTime(2026, 7, 1), + ); +} + +// ── InventoryStatusChip ──────────────────────────────────────────────────────── + +void main() { + group('InventoryStatusChip', () { + testWidgets('shows In Stock label for inStock status', (tester) async { + await tester.pumpWidget(_wrap(const InventoryStatusChip(status: InventoryStatus.inStock))); + expect(find.text('In Stock'), findsOneWidget); + }); + + testWidgets('shows Low Stock label for lowStock status', (tester) async { + await tester.pumpWidget(_wrap(const InventoryStatusChip(status: InventoryStatus.lowStock))); + expect(find.text('Low Stock'), findsOneWidget); + }); + + testWidgets('shows Out of Stock label for outOfStock status', (tester) async { + await tester.pumpWidget(_wrap(const InventoryStatusChip(status: InventoryStatus.outOfStock))); + expect(find.text('Out of Stock'), findsOneWidget); + }); + + testWidgets('shows Draft label for draft status', (tester) async { + await tester.pumpWidget(_wrap(const InventoryStatusChip(status: InventoryStatus.draft))); + expect(find.text('Draft'), findsOneWidget); + }); + }); + + // ── InventoryDetailPanel ───────────────────────────────────────────────────── + + group('InventoryDetailPanel', () { + testWidgets('displays item name and SKU', (tester) async { + final item = _makeItem(name: 'Floral Bowl Cozy', sku: 'BC-FLR-001'); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('Floral Bowl Cozy'), findsOneWidget); + expect(find.text('SKU: BC-FLR-001'), findsOneWidget); + }); + + testWidgets('displays description when non-empty', (tester) async { + final item = _makeItem(description: 'Hand-sewn fabric bowl cozy.'); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('Hand-sewn fabric bowl cozy.'), findsOneWidget); + }); + + testWidgets('displays quantity on hand', (tester) async { + final item = _makeItem(quantityOnHand: 18); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('18'), findsOneWidget); + }); + + testWidgets('displays unit price formatted', (tester) async { + final item = _makeItem(unitPrice: 12.99); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('\$12.99'), findsOneWidget); + }); + + testWidgets('shows Adjust Quantity button when not updating', (tester) async { + final item = _makeItem(); + await tester.pumpWidget( + _wrap(InventoryDetailPanel(item: item, isUpdating: false, onAdjustQuantity: () {})), + ); + expect(find.text('Adjust Quantity'), findsOneWidget); + }); + + testWidgets('shows loading indicator when isUpdating is true', (tester) async { + final item = _makeItem(); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: true))); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('Adjust Quantity'), findsNothing); + }); + + testWidgets('calls onAdjustQuantity when button tapped', (tester) async { + var called = false; + final item = _makeItem(); + await tester.pumpWidget( + _wrap( + InventoryDetailPanel( + item: item, + isUpdating: false, + onAdjustQuantity: () => called = true, + ), + ), + ); + await tester.tap(find.text('Adjust Quantity')); + expect(called, true); + }); + + testWidgets('shows View Product button when onViewProduct provided', (tester) async { + final item = _makeItem(); + await tester.pumpWidget( + _wrap(InventoryDetailPanel(item: item, isUpdating: false, onViewProduct: () {})), + ); + expect(find.text('View Product'), findsOneWidget); + }); + + testWidgets('hides View Product button when onViewProduct is null', (tester) async { + final item = _makeItem(); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('View Product'), findsNothing); + }); + + testWidgets('shows location and supplier when provided', (tester) async { + final item = _makeItem(location: 'Shelf A1', supplier: 'Kell Creations'); + await tester.pumpWidget( + _wrap(SingleChildScrollView(child: InventoryDetailPanel(item: item, isUpdating: false))), + ); + await tester.pump(); + expect(find.text('Shelf A1'), findsOneWidget); + expect(find.text('Kell Creations'), findsOneWidget); + }); + + testWidgets('shows status chip', (tester) async { + final item = _makeItem(status: InventoryStatus.lowStock); + await tester.pumpWidget(_wrap(InventoryDetailPanel(item: item, isUpdating: false))); + expect(find.text('Low Stock'), findsOneWidget); + }); + }); + + // ── InventoryPage ──────────────────────────────────────────────────────────── + + group('InventoryPage', () { + testWidgets('shows loading state initially', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + // Pump one frame — the async load has started but not completed. + await tester.pump(); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // Let the pending timer complete so the test teardown is clean. + await tester.pumpAndSettle(); + }); + + testWidgets('shows search bar after load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets('shows filter chips after load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + expect(find.byType(FilterChip), findsWidgets); + expect(find.text('All'), findsOneWidget); + expect(find.text('In Stock'), findsOneWidget); + expect(find.text('Low Stock'), findsOneWidget); + }); + + testWidgets('shows item count after load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + expect(find.text('6 items'), findsOneWidget); + }); + + testWidgets('shows item cards after load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + expect(find.byType(InventoryItemCard), findsWidgets); + }); + + testWidgets('search filters items by name', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField), 'cozy'); + await tester.pump(); + + expect(find.text('1 item'), findsOneWidget); + }); + + testWidgets('filter chip filters items by status', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo))); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Low Stock').first); + await tester.pump(); + + expect(find.text('2 items'), findsOneWidget); + }); + + testWidgets('applies initialFilter on load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo, initialFilter: 'inStock'))); + await tester.pumpAndSettle(); + // 2 inStock items in seed data (items 1 and 4) + expect(find.text('2 items'), findsOneWidget); + }); + + testWidgets('applies initialQuery on load', (tester) async { + final repo = FakeInventoryRepository(); + await tester.pumpWidget(_wrap(InventoryPage(repository: repo, initialQuery: 'ocean'))); + await tester.pumpAndSettle(); + expect(find.text('1 item'), findsOneWidget); + }); + }); + + // ── AdjustQuantityDialogResult ──────────────────────────────────────────────── + + group('AdjustQuantityDialogResult', () { + test('stores delta and reason', () { + const result = AdjustQuantityDialogResult(delta: 5, reason: 'restock'); + expect(result.delta, 5); + expect(result.reason, 'restock'); + }); + + test('stores negative delta for removal', () { + const result = AdjustQuantityDialogResult(delta: -3, reason: 'sold'); + expect(result.delta, -3); + expect(result.reason, 'sold'); + }); + }); +}