From 73fdcf99e78fd6966deddc5b8d0f7895a7819da2 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Fri, 10 Jul 2026 18:52:29 -0400 Subject: [PATCH] chore(inventory): stage uncommitted presentation layer files from Stage 5B --- .../lib/feature_inventory.dart | 5 + .../inventory_action_snack_bar.dart | 28 ++ .../lib/src/presentation/inventory_page.dart | 368 ++++++++++++++++-- .../widgets/adjust_quantity_dialog.dart | 142 +++++++ .../widgets/inventory_detail_panel.dart | 147 +++++++ .../widgets/inventory_status_chip.dart | 29 ++ 6 files changed, 681 insertions(+), 38 deletions(-) create mode 100644 kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_action_snack_bar.dart create mode 100644 kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/adjust_quantity_dialog.dart create mode 100644 kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_detail_panel.dart create mode 100644 kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_status_chip.dart diff --git a/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart b/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart index a6cf36d..bedc104 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart @@ -10,4 +10,9 @@ export 'src/domain/inventory_adjustment_result.dart'; export 'src/domain/inventory_item.dart'; export 'src/domain/inventory_repository.dart'; export 'src/domain/inventory_status.dart'; +export 'src/presentation/inventory_action_snack_bar.dart'; export 'src/presentation/inventory_page.dart'; +export 'src/presentation/widgets/adjust_quantity_dialog.dart'; +export 'src/presentation/widgets/inventory_detail_panel.dart'; +export 'src/presentation/widgets/inventory_item_card.dart'; +export 'src/presentation/widgets/inventory_status_chip.dart'; diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_action_snack_bar.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_action_snack_bar.dart new file mode 100644 index 0000000..7b04188 --- /dev/null +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_action_snack_bar.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import '../domain/inventory_adjustment_result.dart'; + +/// Shows a [SnackBar] reflecting the outcome of an inventory write action. +/// +/// Displays a success message when [result.success] is true, or an error +/// message (with optional detail) when it is false. +void showInventoryActionSnackBar( + BuildContext context, + InventoryAdjustmentResult result, { + String successMessage = 'Quantity updated.', +}) { + if (!context.mounted) return; + + final isSuccess = result.success; + final message = isSuccess + ? successMessage + : 'Update failed.${result.errorMessage != null ? ' ${result.errorMessage}' : ''}'; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: isSuccess ? Colors.green.shade700 : Colors.red.shade700, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 3), + ), + ); +} diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart index 8f1267d..3a9391d 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart @@ -6,9 +6,18 @@ import '../application/create_inventory_item.dart'; import '../application/get_inventory_items.dart'; import '../application/inventory_controller.dart'; import '../application/update_inventory_item.dart'; +import '../domain/inventory_item.dart'; import '../domain/inventory_repository.dart'; +import 'inventory_action_snack_bar.dart'; +import 'widgets/adjust_quantity_dialog.dart'; +import 'widgets/inventory_detail_panel.dart'; import 'widgets/inventory_item_card.dart'; +/// The main inventory workspace page. +/// +/// Provides a search bar, status filter chips, a scrollable item list, and a +/// detail panel that appears when an item is selected. Quantity adjustments are +/// performed via a modal dialog and confirmed with a SnackBar. class InventoryPage extends StatefulWidget { final InventoryRepository repository; @@ -39,81 +48,106 @@ class InventoryPage extends StatefulWidget { } class _InventoryPageState extends State { - late final InventoryController controller; + late final InventoryController _controller; + late final TextEditingController _searchController; @override void initState() { super.initState(); - controller = InventoryController( + _controller = InventoryController( GetInventoryItems(widget.repository), AdjustInventoryQuantity(widget.repository), CreateInventoryItem(widget.repository), UpdateInventoryItem(widget.repository), ); + _searchController = TextEditingController(text: widget.initialQuery ?? ''); // Apply any initial filter / query before loading. if (widget.initialFilter != null) { - controller.activeFilter = widget.initialFilter; + _controller.activeFilter = widget.initialFilter; } if (widget.initialQuery != null && widget.initialQuery!.isNotEmpty) { - controller.searchQuery = widget.initialQuery!; + _controller.searchQuery = widget.initialQuery!; } - controller.load().then((_) { - // After data is loaded, try to pre-select by SKU if requested. + _controller.addListener(_onControllerChanged); + + _controller.load().then((_) { if (widget.initialSelectedSku != null) { - controller.selectBySku(widget.initialSelectedSku!); + _controller.selectBySku(widget.initialSelectedSku!); } }); } @override void dispose() { - controller.dispose(); + _controller.removeListener(_onControllerChanged); + _controller.dispose(); + _searchController.dispose(); super.dispose(); } + void _onControllerChanged() { + final result = _controller.lastActionResult; + if (result != null && !_controller.isUpdating) { + showInventoryActionSnackBar( + context, + result, + successMessage: result.success ? 'Quantity updated.' : 'Update failed.', + ); + // Consume the result so it is not shown again on the next rebuild. + _controller.lastActionResult = null; + } + } + + Future _handleAdjustQuantity(InventoryItem item) async { + final dialogResult = await showAdjustQuantityDialog(context, item); + if (dialogResult == null) return; + await _controller.adjustQuantity(item.id, dialogResult.delta, dialogResult.reason); + } + @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: controller, + animation: _controller, builder: (context, _) { - if (controller.isLoading) { - return const Center(child: CircularProgressIndicator()); + if (_controller.isLoading) { + return const KcLoadingState(message: 'Loading inventory…'); } - if (controller.error != null) { - return const Center(child: Text('Failed to load inventory data.')); + if (_controller.error != null) { + return KcErrorState(message: 'Failed to load inventory data.', onRetry: _controller.load); } return LayoutBuilder( builder: (context, constraints) { - final width = constraints.maxWidth; + final isWide = constraints.maxWidth >= KcBreakpoints.medium; - int crossAxisCount = 1; - if (width >= 1200) { - crossAxisCount = 3; - } else if (width >= 700) { - crossAxisCount = 2; - } - - return GridView.builder( - itemCount: controller.items.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: KcSpacing.md, - mainAxisSpacing: KcSpacing.md, - childAspectRatio: 1.5, - ), - itemBuilder: (context, index) { - final item = controller.items[index]; - return InventoryItemCard( - item: item, - onViewProduct: widget.onViewProduct != null - ? () => widget.onViewProduct!(item.sku) - : null, - ); - }, + return Column( + children: [ + _InventoryToolbar( + searchController: _searchController, + activeFilter: _controller.activeFilter, + itemCount: _controller.items.length, + onSearchChanged: (q) { + _controller.setSearchQuery(q); + }, + onFilterChanged: _controller.setFilter, + ), + Expanded( + child: isWide + ? _WideLayout( + controller: _controller, + onAdjustQuantity: _handleAdjustQuantity, + onViewProduct: widget.onViewProduct, + ) + : _NarrowLayout( + controller: _controller, + onAdjustQuantity: _handleAdjustQuantity, + onViewProduct: widget.onViewProduct, + ), + ), + ], ); }, ); @@ -121,3 +155,261 @@ class _InventoryPageState extends State { ); } } + +// ── Toolbar ──────────────────────────────────────────────────────────────────── + +class _InventoryToolbar extends StatelessWidget { + final TextEditingController searchController; + final String? activeFilter; + final int itemCount; + final ValueChanged onSearchChanged; + final ValueChanged onFilterChanged; + + const _InventoryToolbar({ + required this.searchController, + required this.activeFilter, + required this.itemCount, + required this.onSearchChanged, + required this.onFilterChanged, + }); + + static const _filters = [ + (label: 'All', value: null), + (label: 'In Stock', value: 'inStock'), + (label: 'Low Stock', value: 'lowStock'), + (label: 'Out of Stock', value: 'outOfStock'), + (label: 'Draft', value: 'draft'), + ]; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(KcSpacing.md, KcSpacing.md, KcSpacing.md, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search bar + TextField( + controller: searchController, + onChanged: onSearchChanged, + decoration: InputDecoration( + hintText: 'Search by name or SKU…', + prefixIcon: const Icon(Icons.search), + suffixIcon: searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + tooltip: 'Clear search', + onPressed: () { + searchController.clear(); + onSearchChanged(''); + }, + ) + : null, + border: const OutlineInputBorder(), + isDense: true, + ), + ), + const SizedBox(height: KcSpacing.sm), + + // Filter chips + item count + Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _filters.map((f) { + final isSelected = activeFilter == f.value; + return Padding( + padding: const EdgeInsets.only(right: KcSpacing.xs), + child: FilterChip( + label: Text(f.label), + selected: isSelected, + onSelected: (_) => onFilterChanged(f.value), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(width: KcSpacing.sm), + Text( + '$itemCount item${itemCount == 1 ? '' : 's'}', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ], + ), + const SizedBox(height: KcSpacing.sm), + ], + ), + ); + } +} + +// ── Wide layout (master + detail side-by-side) ───────────────────────────────── + +class _WideLayout extends StatelessWidget { + final InventoryController controller; + final Future Function(InventoryItem) onAdjustQuantity; + final void Function(String sku)? onViewProduct; + + const _WideLayout({ + required this.controller, + required this.onAdjustQuantity, + required this.onViewProduct, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Master list — 40% width + SizedBox( + width: 340, + child: _ItemList(controller: controller, onViewProduct: onViewProduct), + ), + const VerticalDivider(width: 1), + // Detail panel — remaining width + Expanded( + child: controller.selectedItem == null + ? const KcEmptyState( + icon: Icons.inventory_2_outlined, + message: 'Select an item from the list to view details.', + ) + : InventoryDetailPanel( + item: controller.selectedItem!, + isUpdating: controller.isUpdating, + onAdjustQuantity: () => onAdjustQuantity(controller.selectedItem!), + onViewProduct: onViewProduct != null + ? () => onViewProduct!(controller.selectedItem!.sku) + : null, + ), + ), + ], + ); + } +} + +// ── Narrow layout (list only; tap navigates to detail) ───────────────────────── + +class _NarrowLayout extends StatelessWidget { + final InventoryController controller; + final Future Function(InventoryItem) onAdjustQuantity; + final void Function(String sku)? onViewProduct; + + const _NarrowLayout({ + required this.controller, + required this.onAdjustQuantity, + required this.onViewProduct, + }); + + @override + Widget build(BuildContext context) { + return _ItemList( + controller: controller, + onViewProduct: onViewProduct, + onTap: (item) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => _InventoryDetailPage( + item: item, + controller: controller, + onAdjustQuantity: onAdjustQuantity, + onViewProduct: onViewProduct, + ), + ), + ); + }, + ); + } +} + +// ── Shared item list ─────────────────────────────────────────────────────────── + +class _ItemList extends StatelessWidget { + final InventoryController controller; + final void Function(String sku)? onViewProduct; + + /// Optional tap handler for narrow layout navigation. + final void Function(InventoryItem)? onTap; + + const _ItemList({required this.controller, required this.onViewProduct, this.onTap}); + + @override + Widget build(BuildContext context) { + if (controller.items.isEmpty) { + return const KcEmptyState( + icon: Icons.inventory_2_outlined, + message: 'No items found. Try adjusting your search or filter.', + ); + } + + return ListView.separated( + padding: const EdgeInsets.all(KcSpacing.md), + itemCount: controller.items.length, + separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.sm), + itemBuilder: (context, index) { + final item = controller.items[index]; + final isSelected = controller.selectedItem?.id == item.id; + + return GestureDetector( + onTap: () { + controller.selectItem(item); + onTap?.call(item); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected ? Theme.of(context).colorScheme.primary : Colors.transparent, + width: 2, + ), + ), + child: InventoryItemCard( + item: item, + onViewProduct: onViewProduct != null ? () => onViewProduct!(item.sku) : null, + ), + ), + ); + }, + ); + } +} + +// ── Narrow detail page (pushed on mobile) ───────────────────────────────────── + +class _InventoryDetailPage extends StatelessWidget { + final InventoryItem item; + final InventoryController controller; + final Future Function(InventoryItem) onAdjustQuantity; + final void Function(String sku)? onViewProduct; + + const _InventoryDetailPage({ + required this.item, + required this.controller, + required this.onAdjustQuantity, + required this.onViewProduct, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(item.name)), + body: AnimatedBuilder( + animation: controller, + builder: (context, _) { + // Use the refreshed item from the controller if available. + final current = controller.selectedItem?.id == item.id ? controller.selectedItem! : item; + return InventoryDetailPanel( + item: current, + isUpdating: controller.isUpdating, + onAdjustQuantity: () => onAdjustQuantity(current), + onViewProduct: onViewProduct != null ? () => onViewProduct!(current.sku) : null, + ); + }, + ), + ); + } +} diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/adjust_quantity_dialog.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/adjust_quantity_dialog.dart new file mode 100644 index 0000000..30b9ea7 --- /dev/null +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/adjust_quantity_dialog.dart @@ -0,0 +1,142 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../domain/inventory_item.dart'; + +/// Result returned by [showAdjustQuantityDialog]. +class AdjustQuantityDialogResult { + final int delta; + final String reason; + + const AdjustQuantityDialogResult({required this.delta, required this.reason}); +} + +/// Shows a modal dialog for adjusting the quantity of [item]. +/// +/// Returns an [AdjustQuantityDialogResult] if the operator confirms, or `null` +/// if they cancel. +Future showAdjustQuantityDialog( + BuildContext context, + InventoryItem item, +) { + return showDialog( + context: context, + builder: (_) => _AdjustQuantityDialog(item: item), + ); +} + +// ── Private dialog widget ────────────────────────────────────────────────────── + +class _AdjustQuantityDialog extends StatefulWidget { + final InventoryItem item; + + const _AdjustQuantityDialog({required this.item}); + + @override + State<_AdjustQuantityDialog> createState() => _AdjustQuantityDialogState(); +} + +class _AdjustQuantityDialogState extends State<_AdjustQuantityDialog> { + final _deltaController = TextEditingController(); + final _reasonController = TextEditingController(); + final _formKey = GlobalKey(); + + /// Whether the adjustment is an addition (+) or removal (−). + bool _isAddition = true; + + @override + void dispose() { + _deltaController.dispose(); + _reasonController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final currentQty = widget.item.quantityOnHand; + + return AlertDialog( + title: const Text('Adjust Quantity'), + content: SizedBox( + width: 360, + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Current quantity display + Text( + 'Current quantity: $currentQty', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + const SizedBox(height: KcSpacing.md), + + // Add / Remove toggle + SegmentedButton( + segments: const [ + ButtonSegment(value: true, label: Text('Add'), icon: Icon(Icons.add)), + ButtonSegment(value: false, label: Text('Remove'), icon: Icon(Icons.remove)), + ], + selected: {_isAddition}, + onSelectionChanged: (s) => setState(() => _isAddition = s.first), + ), + const SizedBox(height: KcSpacing.md), + + // Amount field + TextFormField( + controller: _deltaController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + labelText: 'Amount', + hintText: 'e.g. 5', + border: OutlineInputBorder(), + ), + autofocus: true, + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Enter an amount'; + final n = int.tryParse(v.trim()); + if (n == null || n <= 0) return 'Must be a positive number'; + if (!_isAddition && n > currentQty) { + return 'Cannot remove more than $currentQty'; + } + return null; + }, + ), + const SizedBox(height: KcSpacing.md), + + // Reason field + TextFormField( + controller: _reasonController, + decoration: const InputDecoration( + labelText: 'Reason', + hintText: 'e.g. restock, sold, damaged', + border: OutlineInputBorder(), + ), + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Enter a reason'; + return null; + }, + ), + ], + ), + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), + FilledButton(onPressed: _submit, child: const Text('Confirm')), + ], + ); + } + + void _submit() { + if (!_formKey.currentState!.validate()) return; + final amount = int.parse(_deltaController.text.trim()); + final delta = _isAddition ? amount : -amount; + Navigator.of( + context, + ).pop(AdjustQuantityDialogResult(delta: delta, reason: _reasonController.text.trim())); + } +} diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_detail_panel.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_detail_panel.dart new file mode 100644 index 0000000..0fe2da4 --- /dev/null +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_detail_panel.dart @@ -0,0 +1,147 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import '../../domain/inventory_item.dart'; +import 'inventory_status_chip.dart'; + +/// A detail panel that displays full information for a selected [InventoryItem] +/// and exposes callbacks for quantity adjustment and item editing. +class InventoryDetailPanel extends StatelessWidget { + final InventoryItem item; + final bool isUpdating; + + /// Called when the operator taps "Adjust Quantity". + final VoidCallback? onAdjustQuantity; + + /// Called when the operator taps "View Product" (cross-feature handoff). + final VoidCallback? onViewProduct; + + const InventoryDetailPanel({ + super.key, + required this.item, + required this.isUpdating, + this.onAdjustQuantity, + this.onViewProduct, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SingleChildScrollView( + padding: const EdgeInsets.all(KcSpacing.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header ────────────────────────────────────────────────────────── + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Text(item.name, style: theme.textTheme.headlineSmall)), + InventoryStatusChip(status: item.status), + ], + ), + const SizedBox(height: KcSpacing.xs), + Text( + 'SKU: ${item.sku}', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + const SizedBox(height: KcSpacing.md), + + // ── Description ────────────────────────────────────────────────────── + if (item.description.isNotEmpty) ...[ + Text(item.description, style: theme.textTheme.bodyMedium), + const SizedBox(height: KcSpacing.md), + ], + + const Divider(), + const SizedBox(height: KcSpacing.md), + + // ── Stock info ─────────────────────────────────────────────────────── + KcSectionHeader(title: 'Stock'), + const SizedBox(height: KcSpacing.sm), + _InfoRow(label: 'Quantity on hand', value: '${item.quantityOnHand}'), + _InfoRow(label: 'Reorder point', value: '${item.reorderPoint}'), + _InfoRow(label: 'Unit price', value: '\$${item.unitPrice.toStringAsFixed(2)}'), + const SizedBox(height: KcSpacing.md), + + // ── Location / supplier ────────────────────────────────────────────── + if (item.location != null || item.supplier != null) ...[ + KcSectionHeader(title: 'Storage'), + const SizedBox(height: KcSpacing.sm), + if (item.location != null) _InfoRow(label: 'Location', value: item.location!), + if (item.supplier != null) _InfoRow(label: 'Supplier', value: item.supplier!), + const SizedBox(height: KcSpacing.md), + ], + + // ── Category ──────────────────────────────────────────────────────── + if (item.category.isNotEmpty) ...[ + KcSectionHeader(title: 'Category'), + const SizedBox(height: KcSpacing.sm), + Text(item.category, style: theme.textTheme.bodyMedium), + const SizedBox(height: KcSpacing.md), + ], + + // ── Last updated ───────────────────────────────────────────────────── + Text( + 'Last updated: ${_formatDate(item.lastUpdated)}', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + + const SizedBox(height: KcSpacing.xl), + + // ── Actions ────────────────────────────────────────────────────────── + if (isUpdating) + const Center(child: CircularProgressIndicator()) + else + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FilledButton.icon( + onPressed: onAdjustQuantity, + icon: const Icon(Icons.tune), + label: const Text('Adjust Quantity'), + ), + if (onViewProduct != null) ...[ + const SizedBox(height: KcSpacing.sm), + OutlinedButton.icon( + onPressed: onViewProduct, + icon: const Icon(Icons.open_in_new, size: 16), + label: const Text('View Product'), + ), + ], + ], + ), + ], + ), + ); + } + + static String _formatDate(DateTime dt) => + '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}'; +} + +// ── Private helpers ──────────────────────────────────────────────────────────── + +class _InfoRow extends StatelessWidget { + final String label; + final String value; + + const _InfoRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(bottom: KcSpacing.xs), + child: Row( + children: [ + SizedBox( + width: 140, + child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral)), + ), + Expanded(child: Text(value, style: theme.textTheme.bodyMedium)), + ], + ), + ); + } +} diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_status_chip.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_status_chip.dart new file mode 100644 index 0000000..e06cb8d --- /dev/null +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/widgets/inventory_status_chip.dart @@ -0,0 +1,29 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import '../../domain/inventory_status.dart'; + +/// A status chip for an [InventoryStatus] value, using the shared [KcStatusChip]. +class InventoryStatusChip extends StatelessWidget { + final InventoryStatus status; + + const InventoryStatusChip({super.key, required this.status}); + + @override + Widget build(BuildContext context) { + final (label, bg, fg) = _style(status); + return KcStatusChip(label: label, background: bg, foreground: fg); + } + + static (String, Color, Color) _style(InventoryStatus status) { + switch (status) { + case InventoryStatus.inStock: + return ('In Stock', const Color(0xFFE8F5E9), KcColors.success); + case InventoryStatus.lowStock: + return ('Low Stock', const Color(0xFFFFF8E1), KcColors.warning); + case InventoryStatus.outOfStock: + return ('Out of Stock', const Color(0xFFFFEBEE), KcColors.danger); + case InventoryStatus.draft: + return ('Draft', const Color(0xFFECEFF1), KcColors.neutral); + } + } +}