Merge feat/inventory-page-ui into main — Stage 5B complete
This commit is contained in:
commit
44689b5cdb
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: feat/inventory-domain-expansion (Stage 5A complete)
|
- main baseline updated through: feat/inventory-page-ui (Stage 5B complete)
|
||||||
- next branch: feat/inventory-page-ui (Stage 5B) or feat/orders-domain (Stage 5C)
|
- next branch: feat/orders-domain (Stage 5C) or feat/bulk-operator-workflows (Stage 7B)
|
||||||
- current stage: Stage 5A complete — inventory domain expansion with write actions
|
- current stage: Stage 5B complete — inventory page UI with search, filter chips, detail panel, quantity adjust dialog, and snack bar feedback
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
||||||
|
|
@ -266,6 +266,25 @@
|
||||||
- analyze: passed (dart analyze — no issues found)
|
- analyze: passed (dart analyze — no issues found)
|
||||||
- brief updated: yes
|
- 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
|
### chore/analyze-cleanup-and-tracker-sync
|
||||||
|
|
||||||
- status: merged to main
|
- status: merged to main
|
||||||
|
|
|
||||||
|
|
@ -125,16 +125,18 @@ Rules:
|
||||||
- `dart analyze` clean
|
- `dart analyze` clean
|
||||||
- `core` tests passing
|
- `core` tests passing
|
||||||
- `design_system` tests passing
|
- `design_system` tests passing
|
||||||
|
- `feature_inventory` tests passing
|
||||||
- `feature_wordpress` tests passing
|
- `feature_wordpress` tests passing
|
||||||
- `kell_web` tests passing
|
- `kell_web` tests passing
|
||||||
- `kell_mobile` tests passing
|
- `kell_mobile` tests passing
|
||||||
- latest reported count for `core`: `20/20 passed`
|
- latest reported count for `core`: `20/20 passed`
|
||||||
- latest reported count for `design_system`: `41/41 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 `feature_wordpress`: `311/311 passed`
|
||||||
- latest reported count for `kell_web`: `24/24 passed`
|
- latest reported count for `kell_web`: `24/24 passed`
|
||||||
- latest reported count for `kell_mobile`: `14/14 passed`
|
- latest reported count for `kell_mobile`: `14/14 passed`
|
||||||
- total: `410/410 passed`
|
- total: `485/485 passed`
|
||||||
- baseline commit: merge of `feat/bulk-status-actions` + analyze cleanup (2026-05-30)
|
- baseline commit: merge of `feat/inventory-page-ui` (Stage 5B complete, 2026-07-10)
|
||||||
|
|
||||||
#### Baseline test coverage (established 2026-05-22)
|
#### 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
|
### 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.
|
**`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 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).
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue