diff --git a/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart index 1afa7ff..9ceb74e 100644 --- a/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart +++ b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart @@ -1,4 +1,5 @@ import 'package:feature_inventory/feature_inventory.dart'; +import 'package:feature_orders/feature_orders.dart'; import 'package:feature_wordpress/feature_wordpress.dart'; /// Holds the concrete service implementations used by the app. @@ -8,14 +9,20 @@ import 'package:feature_wordpress/feature_wordpress.dart'; /// production backends are ready. class AppServices { final InventoryRepository inventoryRepository; + final OrdersRepository ordersRepository; final ProductPublishingRepository productPublishingRepository; - const AppServices({required this.inventoryRepository, required this.productPublishingRepository}); + const AppServices({ + required this.inventoryRepository, + required this.ordersRepository, + required this.productPublishingRepository, + }); /// Creates an [AppServices] backed by fake, in-memory repositories. factory AppServices.fake() { return AppServices( inventoryRepository: FakeInventoryRepository(), + ordersRepository: FakeOrdersRepository(), productPublishingRepository: FakeProductPublishingRepository(), ); } diff --git a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart index 2648437..2d041a5 100644 --- a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart +++ b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart @@ -1,4 +1,5 @@ import 'package:feature_inventory/feature_inventory.dart'; +import 'package:feature_orders/feature_orders.dart'; import 'package:feature_wordpress/feature_wordpress.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,6 @@ import '../composition/app_scope.dart'; import '../pages/dashboard_page.dart'; import '../pages/finance_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart'; -import '../pages/orders_placeholder_page.dart'; import '../pages/policy_placeholder_page.dart'; import '../shell/app_shell.dart'; @@ -50,10 +50,10 @@ abstract final class AppRoutes { case orders: return _buildRoute( settings, - (context) => const AppShell( + (context) => AppShell( selectedRoute: orders, title: 'Orders', - child: OrdersPlaceholderPage(), + child: OrdersPage(repository: AppScope.of(context).ordersRepository), ), ); case finance: diff --git a/kell_creations_apps/apps/kell_web/pubspec.lock b/kell_creations_apps/apps/kell_web/pubspec.lock index 6d4447d..6b004a7 100644 --- a/kell_creations_apps/apps/kell_web/pubspec.lock +++ b/kell_creations_apps/apps/kell_web/pubspec.lock @@ -78,6 +78,13 @@ packages: relative: true source: path version: "0.0.1" + feature_orders: + dependency: "direct main" + description: + path: "../../packages/feature_orders" + relative: true + source: path + version: "0.0.1" feature_wordpress: dependency: "direct main" description: diff --git a/kell_creations_apps/apps/kell_web/pubspec.yaml b/kell_creations_apps/apps/kell_web/pubspec.yaml index 77eea1a..f73d2ae 100644 --- a/kell_creations_apps/apps/kell_web/pubspec.yaml +++ b/kell_creations_apps/apps/kell_web/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: path: ../../packages/design_system feature_inventory: path: ../../packages/feature_inventory + feature_orders: + path: ../../packages/feature_orders feature_wordpress: path: ../../packages/feature_wordpress diff --git a/kell_creations_apps/packages/feature_orders/.dart_tool/package_config.json b/kell_creations_apps/packages/feature_orders/.dart_tool/package_config.json new file mode 100644 index 0000000..c245196 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/.dart_tool/package_config.json @@ -0,0 +1,178 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "async", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/async-2.13.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "boolean_selector", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/boolean_selector-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "characters", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/characters-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "clock", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "design_system", + "rootUri": "../../design_system", + "packageUri": "lib/", + "languageVersion": "3.11" + }, + { + "name": "fake_async", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/fake_async-1.3.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "flutter", + "rootUri": "file:///D:/develop/flutter/packages/flutter", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "flutter_lints", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_lints-6.0.0", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_test", + "rootUri": "file:///D:/develop/flutter/packages/flutter_test", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "leak_tracker", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker-11.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_flutter_testing", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_testing", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_testing-3.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "lints", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/lints-6.1.0", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "matcher", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/matcher-0.12.19", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "material_color_utilities", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/material_color_utilities-0.13.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "meta", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "path", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "sky_engine", + "rootUri": "file:///D:/develop/flutter/bin/cache/pkg/sky_engine", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "source_span", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "stream_channel", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "test_api", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/test_api-0.7.10", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "vector_math", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/vector_math-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "vm_service", + "rootUri": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache/hosted/pub.dev/vm_service-15.0.2", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "feature_orders", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.11" + } + ], + "generator": "pub", + "generatorVersion": "3.11.4", + "flutterRoot": "file:///D:/develop/flutter", + "flutterVersion": "3.41.6", + "pubCache": "file:///C:/Users/mtkel/AppData/Local/Pub/Cache" +} diff --git a/kell_creations_apps/packages/feature_orders/.dart_tool/package_graph.json b/kell_creations_apps/packages/feature_orders/.dart_tool/package_graph.json new file mode 100644 index 0000000..f7e1e30 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/.dart_tool/package_graph.json @@ -0,0 +1,232 @@ +{ + "roots": [ + "feature_orders" + ], + "packages": [ + { + "name": "feature_orders", + "version": "0.0.1", + "dependencies": [ + "design_system", + "flutter" + ], + "devDependencies": [ + "flutter_lints", + "flutter_test" + ] + }, + { + "name": "flutter_lints", + "version": "6.0.0", + "dependencies": [ + "lints" + ] + }, + { + "name": "flutter_test", + "version": "0.0.0", + "dependencies": [ + "clock", + "collection", + "fake_async", + "flutter", + "leak_tracker_flutter_testing", + "matcher", + "meta", + "path", + "stack_trace", + "stream_channel", + "test_api", + "vector_math" + ] + }, + { + "name": "design_system", + "version": "0.0.1", + "dependencies": [ + "flutter" + ] + }, + { + "name": "flutter", + "version": "0.0.0", + "dependencies": [ + "characters", + "collection", + "material_color_utilities", + "meta", + "sky_engine", + "vector_math" + ] + }, + { + "name": "lints", + "version": "6.1.0", + "dependencies": [] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "leak_tracker_flutter_testing", + "version": "3.0.10", + "dependencies": [ + "flutter", + "leak_tracker", + "leak_tracker_testing", + "matcher", + "meta" + ] + }, + { + "name": "vector_math", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "fake_async", + "version": "1.3.3", + "dependencies": [ + "clock", + "collection" + ] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "matcher", + "version": "0.12.19", + "dependencies": [ + "async", + "meta", + "stack_trace", + "term_glyph", + "test_api" + ] + }, + { + "name": "test_api", + "version": "0.7.10", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" + ] + }, + { + "name": "sky_engine", + "version": "0.0.0", + "dependencies": [] + }, + { + "name": "material_color_utilities", + "version": "0.13.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "characters", + "version": "1.4.1", + "dependencies": [] + }, + { + "name": "async", + "version": "2.13.1", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "leak_tracker_testing", + "version": "3.0.2", + "dependencies": [ + "leak_tracker", + "matcher", + "meta" + ] + }, + { + "name": "leak_tracker", + "version": "11.0.2", + "dependencies": [ + "clock", + "collection", + "meta", + "path", + "vm_service" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_span", + "version": "1.10.2", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "boolean_selector", + "version": "2.1.2", + "dependencies": [ + "source_span", + "string_scanner" + ] + }, + { + "name": "vm_service", + "version": "15.0.2", + "dependencies": [] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/kell_creations_apps/packages/feature_orders/.dart_tool/version b/kell_creations_apps/packages/feature_orders/.dart_tool/version new file mode 100644 index 0000000..0719cf9 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/.dart_tool/version @@ -0,0 +1 @@ +3.41.6 \ No newline at end of file diff --git a/kell_creations_apps/packages/feature_orders/analysis_options.yaml b/kell_creations_apps/packages/feature_orders/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/kell_creations_apps/packages/feature_orders/build/native_assets/windows/native_assets.json b/kell_creations_apps/packages/feature_orders/build/native_assets/windows/native_assets.json new file mode 100644 index 0000000..523bfc7 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/build/native_assets/windows/native_assets.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill b/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill new file mode 100644 index 0000000..f69ab61 Binary files /dev/null and b/kell_creations_apps/packages/feature_orders/build/test_cache/build/b70f9274ac3b5ec9754be8774a3c9a1c.cache.dill.track.dill differ diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/AssetManifest.bin b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/AssetManifest.bin new file mode 100644 index 0000000..86d111f Binary files /dev/null and b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/AssetManifest.bin differ diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/FontManifest.json b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/FontManifest.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/FontManifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NOTICES.Z b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NOTICES.Z new file mode 100644 index 0000000..817dfc1 Binary files /dev/null and b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NOTICES.Z differ diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NativeAssetsManifest.json b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NativeAssetsManifest.json new file mode 100644 index 0000000..523bfc7 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/NativeAssetsManifest.json @@ -0,0 +1 @@ +{"format-version":[1,0,0],"native-assets":{}} \ No newline at end of file diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/ink_sparkle.frag b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/ink_sparkle.frag new file mode 100644 index 0000000..794ba24 Binary files /dev/null and b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/ink_sparkle.frag differ diff --git a/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/stretch_effect.frag b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/stretch_effect.frag new file mode 100644 index 0000000..c014a19 Binary files /dev/null and b/kell_creations_apps/packages/feature_orders/build/unit_test_assets/shaders/stretch_effect.frag differ diff --git a/kell_creations_apps/packages/feature_orders/lib/feature_orders.dart b/kell_creations_apps/packages/feature_orders/lib/feature_orders.dart new file mode 100644 index 0000000..df547cb --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/feature_orders.dart @@ -0,0 +1,8 @@ +library; + +export 'src/data/fake_orders_repository.dart'; +export 'src/domain/order.dart'; +export 'src/domain/order_item.dart'; +export 'src/domain/order_status.dart'; +export 'src/domain/orders_repository.dart'; +export 'src/presentation/orders_page.dart'; diff --git a/kell_creations_apps/packages/feature_orders/lib/src/application/get_orders.dart b/kell_creations_apps/packages/feature_orders/lib/src/application/get_orders.dart new file mode 100644 index 0000000..dd5748f --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/application/get_orders.dart @@ -0,0 +1,11 @@ +import '../domain/order.dart'; +import '../domain/orders_repository.dart'; + +/// Use case: retrieve all orders from the repository. +class GetOrders { + final OrdersRepository repository; + + GetOrders(this.repository); + + Future> call() => repository.getOrders(); +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/application/orders_controller.dart b/kell_creations_apps/packages/feature_orders/lib/src/application/orders_controller.dart new file mode 100644 index 0000000..353f4e1 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/application/orders_controller.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; + +import '../domain/order.dart'; +import 'get_orders.dart'; + +/// Controller that manages the orders workspace state. +class OrdersController extends ChangeNotifier { + final GetOrders _getOrders; + + OrdersController(this._getOrders); + + bool isLoading = false; + List orders = []; + Order? selectedOrder; + Object? error; + + /// Loads all orders. + Future load() async { + isLoading = true; + error = null; + notifyListeners(); + + try { + orders = await _getOrders(); + // Auto-select the first order if nothing is selected. + selectedOrder ??= orders.isNotEmpty ? orders.first : null; + } catch (e) { + error = e; + } finally { + isLoading = false; + notifyListeners(); + } + } + + /// Selects an order for detail view. + void selectOrder(Order order) { + selectedOrder = order; + notifyListeners(); + } +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/data/fake_orders_repository.dart b/kell_creations_apps/packages/feature_orders/lib/src/data/fake_orders_repository.dart new file mode 100644 index 0000000..57f6575 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/data/fake_orders_repository.dart @@ -0,0 +1,144 @@ +import '../domain/order.dart'; +import '../domain/order_item.dart'; +import '../domain/order_status.dart'; +import '../domain/orders_repository.dart'; + +/// Stubbed implementation of [OrdersRepository] with sample +/// Kell Creations orders. No real WooCommerce or shipping API calls are made. +class FakeOrdersRepository implements OrdersRepository { + final List _orders = [ + Order( + id: 'KC-1001', + customerName: 'Sarah Mitchell', + customerEmail: 'sarah.mitchell@example.com', + orderDate: DateTime(2026, 4, 1), + status: OrderStatus.delivered, + shippingAddress: '123 Maple St, Asheville, NC 28801', + items: const [ + OrderItem( + productName: 'Floral Bowl Cozy', + sku: 'BC-FLR-001', + quantity: 2, + unitPrice: 12.99, + ), + OrderItem( + productName: 'Citrus Coaster Set', + sku: 'CS-CIT-002', + quantity: 1, + unitPrice: 16.50, + ), + ], + ), + Order( + id: 'KC-1002', + customerName: 'James Thornton', + customerEmail: 'james.thornton@example.com', + orderDate: DateTime(2026, 4, 2), + status: OrderStatus.shipped, + shippingAddress: '456 Oak Ave, Knoxville, TN 37902', + items: const [ + OrderItem( + productName: 'Ocean Nightlight', + sku: 'NL-OCN-003', + quantity: 1, + unitPrice: 19.99, + ), + OrderItem( + productName: 'Sublimated Slate Coaster', + sku: 'SC-SUB-006', + quantity: 3, + unitPrice: 14.99, + ), + ], + ), + Order( + id: 'KC-1003', + customerName: 'Emily Chen', + customerEmail: 'emily.chen@example.com', + orderDate: DateTime(2026, 4, 3), + status: OrderStatus.processing, + shippingAddress: '789 Pine Rd, Charlotte, NC 28202', + items: const [ + OrderItem( + productName: 'Fabric Jar Gripper', + sku: 'JG-BLU-004', + quantity: 4, + unitPrice: 8.50, + ), + ], + ), + Order( + id: 'KC-1004', + customerName: 'David Park', + customerEmail: 'david.park@example.com', + orderDate: DateTime(2026, 4, 3), + status: OrderStatus.pending, + shippingAddress: '321 Birch Ln, Greenville, SC 29601', + items: const [ + OrderItem( + productName: 'Skillet Handle Sleeve', + sku: 'SH-SUN-005', + quantity: 2, + unitPrice: 10.99, + ), + OrderItem( + productName: 'Floral Bowl Cozy', + sku: 'BC-FLR-001', + quantity: 1, + unitPrice: 12.99, + ), + ], + ), + Order( + id: 'KC-1005', + customerName: 'Rachel Adams', + customerEmail: 'rachel.adams@example.com', + orderDate: DateTime(2026, 3, 28), + status: OrderStatus.cancelled, + shippingAddress: '654 Elm St, Richmond, VA 23220', + items: const [ + OrderItem( + productName: 'Citrus Coaster Set', + sku: 'CS-CIT-002', + quantity: 2, + unitPrice: 16.50, + ), + ], + ), + Order( + id: 'KC-1006', + customerName: 'Maria Gonzalez', + customerEmail: 'maria.gonzalez@example.com', + orderDate: DateTime(2026, 4, 4), + status: OrderStatus.pending, + shippingAddress: '987 Cedar Dr, Atlanta, GA 30301', + items: const [ + OrderItem( + productName: 'Ocean Nightlight', + sku: 'NL-OCN-003', + quantity: 1, + unitPrice: 19.99, + ), + OrderItem( + productName: 'Fabric Jar Gripper', + sku: 'JG-BLU-004', + quantity: 2, + unitPrice: 8.50, + ), + OrderItem( + productName: 'Sublimated Slate Coaster', + sku: 'SC-SUB-006', + quantity: 1, + unitPrice: 14.99, + ), + ], + ), + ]; + + @override + Future> getOrders() async { + // Simulate network latency. + await Future.delayed(const Duration(milliseconds: 300)); + return List.unmodifiable(_orders); + } +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/domain/order.dart b/kell_creations_apps/packages/feature_orders/lib/src/domain/order.dart new file mode 100644 index 0000000..b9d66bf --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/domain/order.dart @@ -0,0 +1,29 @@ +import 'order_item.dart'; +import 'order_status.dart'; + +/// A customer order placed through the Kell Creations store. +class Order { + final String id; + final String customerName; + final String customerEmail; + final DateTime orderDate; + final OrderStatus status; + final List items; + final String shippingAddress; + + const Order({ + required this.id, + required this.customerName, + required this.customerEmail, + required this.orderDate, + required this.status, + required this.items, + required this.shippingAddress, + }); + + /// The total value of the order. + double get total => items.fold(0, (sum, item) => sum + item.lineTotal); + + /// The number of individual items in the order. + int get itemCount => items.fold(0, (sum, item) => sum + item.quantity); +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/domain/order_item.dart b/kell_creations_apps/packages/feature_orders/lib/src/domain/order_item.dart new file mode 100644 index 0000000..67609dd --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/domain/order_item.dart @@ -0,0 +1,17 @@ +/// A single line item within an [Order]. +class OrderItem { + final String productName; + final String sku; + final int quantity; + final double unitPrice; + + const OrderItem({ + required this.productName, + required this.sku, + required this.quantity, + required this.unitPrice, + }); + + /// The total price for this line item. + double get lineTotal => quantity * unitPrice; +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/domain/order_status.dart b/kell_creations_apps/packages/feature_orders/lib/src/domain/order_status.dart new file mode 100644 index 0000000..4844ed7 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/domain/order_status.dart @@ -0,0 +1,17 @@ +/// The fulfilment status of a customer order. +enum OrderStatus { + /// Order has been placed but not yet processed. + pending, + + /// Order is being prepared / packed. + processing, + + /// Order has been shipped to the customer. + shipped, + + /// Order has been delivered. + delivered, + + /// Order was cancelled before fulfilment. + cancelled, +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/domain/orders_repository.dart b/kell_creations_apps/packages/feature_orders/lib/src/domain/orders_repository.dart new file mode 100644 index 0000000..e6be39b --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/domain/orders_repository.dart @@ -0,0 +1,7 @@ +import 'order.dart'; + +/// Contract for fetching and managing customer orders. +abstract class OrdersRepository { + /// Returns all orders. + Future> getOrders(); +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart new file mode 100644 index 0000000..ae4995f --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/orders_page.dart @@ -0,0 +1,98 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../application/get_orders.dart'; +import '../application/orders_controller.dart'; +import '../domain/orders_repository.dart'; +import 'widgets/order_card.dart'; +import 'widgets/order_detail_panel.dart'; + +/// The main Orders page. +/// +/// Displays a list of orders on the left and a detail panel on the right. +/// Users can select an order to view its full details. +class OrdersPage extends StatefulWidget { + final OrdersRepository repository; + + const OrdersPage({super.key, required this.repository}); + + @override + State createState() => _OrdersPageState(); +} + +class _OrdersPageState extends State { + late final OrdersController controller; + + @override + void initState() { + super.initState(); + controller = OrdersController(GetOrders(widget.repository)); + controller.load(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, _) { + if (controller.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.error != null) { + return const Center(child: Text('Failed to load orders.')); + } + + return LayoutBuilder( + builder: (context, constraints) { + // On narrow screens show only the list; on wide screens show + // a master-detail layout. + if (constraints.maxWidth < 800) { + return _buildOrderList(); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 380, child: _buildOrderList()), + const SizedBox(width: KcSpacing.md), + Expanded(child: _buildDetail()), + ], + ); + }, + ); + }, + ); + } + + Widget _buildOrderList() { + return ListView.separated( + itemCount: controller.orders.length, + separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.sm), + itemBuilder: (context, index) { + final order = controller.orders[index]; + return SizedBox( + height: 160, + child: OrderCard( + order: order, + isSelected: order.id == controller.selectedOrder?.id, + onTap: () => controller.selectOrder(order), + ), + ); + }, + ); + } + + Widget _buildDetail() { + final selected = controller.selectedOrder; + if (selected == null) { + return const Center(child: Text('Select an order to view details')); + } + return OrderDetailPanel(order: selected); + } +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_card.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_card.dart new file mode 100644 index 0000000..b8baeec --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_card.dart @@ -0,0 +1,84 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/order.dart'; +import 'order_status_chip.dart'; + +/// A card displaying a summary of an [Order]. +/// +/// Shows the order ID, customer name, date, total, item count, and status. +/// Highlights when [isSelected] is true. +class OrderCard extends StatelessWidget { + final Order order; + final bool isSelected; + final VoidCallback? onTap; + + const OrderCard({super.key, required this.order, this.isSelected = false, this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(KcSpacing.md), + decoration: BoxDecoration( + color: KcColors.surface, + border: Border.all( + color: isSelected ? KcColors.denimBlue : KcColors.border, + width: isSelected ? 2 : 1, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow(blurRadius: 8, offset: Offset(0, 2), color: Color(0x11000000)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + order.id, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + OrderStatusChip(status: order.status), + ], + ), + const SizedBox(height: KcSpacing.xs), + Text(order.customerName, style: Theme.of(context).textTheme.bodyMedium), + const SizedBox(height: KcSpacing.sm), + Row( + children: [ + Text( + '\$${order.total.toStringAsFixed(2)}', + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(width: KcSpacing.sm), + Text( + '${order.itemCount} item${order.itemCount == 1 ? '' : 's'}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: KcColors.neutral), + ), + ], + ), + const Spacer(), + Text( + _formatDate(order.orderDate), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ], + ), + ), + ); + } + + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart new file mode 100644 index 0000000..53b7756 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_detail_panel.dart @@ -0,0 +1,140 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/order.dart'; +import 'order_status_chip.dart'; + +/// A detail panel that shows the full information for the selected [Order]. +/// +/// Includes customer info, shipping address, line items table, and order total. +class OrderDetailPanel extends StatelessWidget { + final Order order; + + const OrderDetailPanel({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return KcCard( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Header ───────────────────────────────────────────────── + Row( + children: [ + Expanded(child: Text('Order ${order.id}', style: theme.textTheme.headlineMedium)), + const SizedBox(width: KcSpacing.sm), + OrderStatusChip(status: order.status), + ], + ), + const SizedBox(height: KcSpacing.md), + + // ── Customer info ────────────────────────────────────────── + Text('Customer', style: theme.textTheme.titleLarge), + const SizedBox(height: KcSpacing.sm), + _MetadataRow(label: 'Name', value: order.customerName), + _MetadataRow(label: 'Email', value: order.customerEmail), + _MetadataRow(label: 'Order Date', value: _formatDate(order.orderDate)), + const SizedBox(height: KcSpacing.md), + + // ── Shipping address ─────────────────────────────────────── + Text('Shipping Address', style: theme.textTheme.titleLarge), + const SizedBox(height: KcSpacing.sm), + Text(order.shippingAddress, style: theme.textTheme.bodyLarge), + const SizedBox(height: KcSpacing.md), + + // ── Line items ───────────────────────────────────────────── + Text('Items', style: theme.textTheme.titleLarge), + const SizedBox(height: KcSpacing.sm), + ...order.items.map( + (item) => Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.productName, style: theme.textTheme.bodyMedium), + Text( + 'SKU: ${item.sku}', + style: theme.textTheme.bodySmall?.copyWith(color: KcColors.neutral), + ), + ], + ), + ), + SizedBox( + width: 40, + child: Text( + 'x${item.quantity}', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + SizedBox( + width: 80, + child: Text( + '\$${item.lineTotal.toStringAsFixed(2)}', + style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + textAlign: TextAlign.end, + ), + ), + ], + ), + ), + ), + const Divider(height: KcSpacing.lg), + + // ── Total ────────────────────────────────────────────────── + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Total: ', style: theme.textTheme.titleLarge), + Text( + '\$${order.total.toStringAsFixed(2)}', + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700), + ), + ], + ), + ], + ), + ), + ); + } + + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } +} + +class _MetadataRow extends StatelessWidget { + final String label; + final String value; + + const _MetadataRow({required this.label, required this.value}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: KcSpacing.xs), + child: Row( + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: KcColors.neutral, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded(child: Text(value, style: Theme.of(context).textTheme.bodyMedium)), + ], + ), + ); + } +} diff --git a/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_status_chip.dart b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_status_chip.dart new file mode 100644 index 0000000..102453e --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/lib/src/presentation/widgets/order_status_chip.dart @@ -0,0 +1,33 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +import '../../domain/order_status.dart'; + +/// A chip that displays the [OrderStatus] of an order using the +/// design-system [KcStatusChip]. +class OrderStatusChip extends StatelessWidget { + final OrderStatus status; + + const OrderStatusChip({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(OrderStatus status) { + switch (status) { + case OrderStatus.pending: + return ('Pending', const Color(0xFFFFF8E1), KcColors.warning); + case OrderStatus.processing: + return ('Processing', const Color(0xFFE3F2FD), KcColors.denimBlue); + case OrderStatus.shipped: + return ('Shipped', const Color(0xFFE0F7FA), KcColors.deepTeal); + case OrderStatus.delivered: + return ('Delivered', const Color(0xFFE8F5E9), KcColors.success); + case OrderStatus.cancelled: + return ('Cancelled', const Color(0xFFFFEBEE), KcColors.danger); + } + } +} diff --git a/kell_creations_apps/packages/feature_orders/pubspec.lock b/kell_creations_apps/packages/feature_orders/pubspec.lock new file mode 100644 index 0000000..70c036c --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/pubspec.lock @@ -0,0 +1,212 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + design_system: + dependency: "direct main" + description: + path: "../design_system" + relative: true + source: path + version: "0.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.11.4 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/kell_creations_apps/packages/feature_orders/pubspec.yaml b/kell_creations_apps/packages/feature_orders/pubspec.yaml new file mode 100644 index 0000000..644cec0 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/pubspec.yaml @@ -0,0 +1,22 @@ +name: feature_orders +description: "Order management feature for Kell Creations." +version: 0.0.1 +publish_to: "none" +homepage: + +environment: + sdk: ^3.11.4 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + design_system: + path: ../design_system + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: diff --git a/kell_creations_apps/packages/feature_orders/test/fake_orders_repository_test.dart b/kell_creations_apps/packages/feature_orders/test/fake_orders_repository_test.dart new file mode 100644 index 0000000..3658985 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/fake_orders_repository_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; + +void main() { + late FakeOrdersRepository repository; + + setUp(() { + repository = FakeOrdersRepository(); + }); + + group('FakeOrdersRepository', () { + test('getOrders returns six sample orders', () async { + final orders = await repository.getOrders(); + expect(orders.length, 6); + }); + + test('getOrders returns orders with expected IDs', () async { + final orders = await repository.getOrders(); + final ids = orders.map((o) => o.id).toList(); + expect(ids, contains('KC-1001')); + expect(ids, contains('KC-1002')); + expect(ids, contains('KC-1003')); + expect(ids, contains('KC-1004')); + expect(ids, contains('KC-1005')); + expect(ids, contains('KC-1006')); + }); + + test('getOrders returns orders with various statuses', () async { + final orders = await repository.getOrders(); + final statuses = orders.map((o) => o.status).toSet(); + expect(statuses, contains(OrderStatus.pending)); + expect(statuses, contains(OrderStatus.processing)); + expect(statuses, contains(OrderStatus.shipped)); + expect(statuses, contains(OrderStatus.delivered)); + expect(statuses, contains(OrderStatus.cancelled)); + }); + + test('order totals are computed correctly', () async { + final orders = await repository.getOrders(); + // KC-1001: 2 * 12.99 + 1 * 16.50 = 42.48 + final order1001 = orders.firstWhere((o) => o.id == 'KC-1001'); + expect(order1001.total, closeTo(42.48, 0.01)); + }); + + test('order item count is computed correctly', () async { + final orders = await repository.getOrders(); + // KC-1001: 2 + 1 = 3 items + final order1001 = orders.firstWhere((o) => o.id == 'KC-1001'); + expect(order1001.itemCount, 3); + }); + }); +} diff --git a/kell_creations_apps/packages/feature_orders/test/feature_orders_test.dart b/kell_creations_apps/packages/feature_orders/test/feature_orders_test.dart new file mode 100644 index 0000000..7b65c2c --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/feature_orders_test.dart @@ -0,0 +1,20 @@ +// This file ensures the barrel export compiles correctly. +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; + +void main() { + test('barrel export exposes Order', () { + // Verify the domain model is accessible through the barrel export. + final order = Order( + id: 'test', + customerName: 'Test', + customerEmail: 'test@test.com', + orderDate: DateTime(2026, 1, 1), + status: OrderStatus.pending, + items: const [], + shippingAddress: '123 Test St', + ); + expect(order.id, 'test'); + }); +} diff --git a/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart b/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart new file mode 100644 index 0000000..8029a47 --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/orders_controller_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; +import 'package:feature_orders/src/application/get_orders.dart'; +import 'package:feature_orders/src/application/orders_controller.dart'; + +void main() { + late FakeOrdersRepository repository; + late OrdersController controller; + + setUp(() { + repository = FakeOrdersRepository(); + controller = OrdersController(GetOrders(repository)); + }); + + tearDown(() { + controller.dispose(); + }); + + group('OrdersController', () { + test('starts with empty state', () { + expect(controller.isLoading, false); + expect(controller.orders, isEmpty); + expect(controller.selectedOrder, isNull); + expect(controller.error, isNull); + }); + + test('load populates orders and auto-selects first', () async { + await controller.load(); + + expect(controller.isLoading, false); + expect(controller.orders.length, 6); + expect(controller.selectedOrder, isNotNull); + expect(controller.selectedOrder!.id, 'KC-1001'); + expect(controller.error, isNull); + }); + + test('selectOrder updates selectedOrder', () async { + await controller.load(); + + final third = controller.orders[2]; + controller.selectOrder(third); + + expect(controller.selectedOrder!.id, third.id); + }); + }); +} diff --git a/kell_creations_apps/packages/feature_orders/test/widgets/order_card_test.dart b/kell_creations_apps/packages/feature_orders/test/widgets/order_card_test.dart new file mode 100644 index 0000000..1ad2abd --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/widgets/order_card_test.dart @@ -0,0 +1,72 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; +import 'package:feature_orders/src/presentation/widgets/order_card.dart'; + +void main() { + final sampleOrder = Order( + id: 'KC-9999', + customerName: 'Test Customer', + customerEmail: 'test@example.com', + orderDate: DateTime(2026, 4, 1), + status: OrderStatus.processing, + shippingAddress: '123 Test St, Test City, TS 00000', + items: const [ + OrderItem(productName: 'Test Product', sku: 'TP-001', quantity: 2, unitPrice: 10.00), + ], + ); + + Widget buildTestWidget({bool isSelected = false, VoidCallback? onTap}) { + return MaterialApp( + theme: buildKcTheme(), + home: Scaffold( + body: SizedBox( + height: 200, + width: 400, + child: OrderCard(order: sampleOrder, isSelected: isSelected, onTap: onTap), + ), + ), + ); + } + + group('OrderCard', () { + testWidgets('displays order ID', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('KC-9999'), findsOneWidget); + }); + + testWidgets('displays customer name', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('Test Customer'), findsOneWidget); + }); + + testWidgets('displays total', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('\$20.00'), findsOneWidget); + }); + + testWidgets('displays item count', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('2 items'), findsOneWidget); + }); + + testWidgets('displays status chip', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('Processing'), findsOneWidget); + }); + + testWidgets('displays date', (tester) async { + await tester.pumpWidget(buildTestWidget()); + expect(find.text('2026-04-01'), findsOneWidget); + }); + + testWidgets('calls onTap when tapped', (tester) async { + var tapped = false; + await tester.pumpWidget(buildTestWidget(onTap: () => tapped = true)); + await tester.tap(find.text('KC-9999')); + expect(tapped, true); + }); + }); +} diff --git a/kell_creations_apps/packages/feature_orders/test/widgets/order_detail_panel_test.dart b/kell_creations_apps/packages/feature_orders/test/widgets/order_detail_panel_test.dart new file mode 100644 index 0000000..ab7eabe --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/widgets/order_detail_panel_test.dart @@ -0,0 +1,75 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; +import 'package:feature_orders/src/presentation/widgets/order_detail_panel.dart'; + +void main() { + final sampleOrder = Order( + id: 'KC-9999', + customerName: 'Test Customer', + customerEmail: 'test@example.com', + orderDate: DateTime(2026, 4, 1), + status: OrderStatus.shipped, + shippingAddress: '123 Test St, Test City, TS 00000', + items: const [ + OrderItem(productName: 'Test Product', sku: 'TP-001', quantity: 2, unitPrice: 10.00), + OrderItem(productName: 'Another Product', sku: 'AP-002', quantity: 1, unitPrice: 15.50), + ], + ); + + Widget buildTestWidget(Order order) { + return MaterialApp( + theme: buildKcTheme(), + home: Scaffold( + body: SingleChildScrollView(child: OrderDetailPanel(order: order)), + ), + ); + } + + group('OrderDetailPanel', () { + testWidgets('displays order ID in header', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('Order KC-9999'), findsOneWidget); + }); + + testWidgets('displays status chip', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('Shipped'), findsOneWidget); + }); + + testWidgets('displays customer name', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('Test Customer'), findsOneWidget); + }); + + testWidgets('displays customer email', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('test@example.com'), findsOneWidget); + }); + + testWidgets('displays shipping address', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('123 Test St, Test City, TS 00000'), findsOneWidget); + }); + + testWidgets('displays line item product names', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('Test Product'), findsOneWidget); + expect(find.text('Another Product'), findsOneWidget); + }); + + testWidgets('displays line item SKUs', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + expect(find.text('SKU: TP-001'), findsOneWidget); + expect(find.text('SKU: AP-002'), findsOneWidget); + }); + + testWidgets('displays order total', (tester) async { + await tester.pumpWidget(buildTestWidget(sampleOrder)); + // 2 * 10.00 + 1 * 15.50 = 35.50 + expect(find.text('\$35.50'), findsOneWidget); + }); + }); +} diff --git a/kell_creations_apps/packages/feature_orders/test/widgets/order_status_chip_test.dart b/kell_creations_apps/packages/feature_orders/test/widgets/order_status_chip_test.dart new file mode 100644 index 0000000..4db39ae --- /dev/null +++ b/kell_creations_apps/packages/feature_orders/test/widgets/order_status_chip_test.dart @@ -0,0 +1,42 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:feature_orders/feature_orders.dart'; +import 'package:feature_orders/src/presentation/widgets/order_status_chip.dart'; + +void main() { + Widget buildTestWidget(OrderStatus status) { + return MaterialApp( + theme: buildKcTheme(), + home: Scaffold(body: OrderStatusChip(status: status)), + ); + } + + group('OrderStatusChip', () { + testWidgets('shows Pending label for pending status', (tester) async { + await tester.pumpWidget(buildTestWidget(OrderStatus.pending)); + expect(find.text('Pending'), findsOneWidget); + }); + + testWidgets('shows Processing label for processing status', (tester) async { + await tester.pumpWidget(buildTestWidget(OrderStatus.processing)); + expect(find.text('Processing'), findsOneWidget); + }); + + testWidgets('shows Shipped label for shipped status', (tester) async { + await tester.pumpWidget(buildTestWidget(OrderStatus.shipped)); + expect(find.text('Shipped'), findsOneWidget); + }); + + testWidgets('shows Delivered label for delivered status', (tester) async { + await tester.pumpWidget(buildTestWidget(OrderStatus.delivered)); + expect(find.text('Delivered'), findsOneWidget); + }); + + testWidgets('shows Cancelled label for cancelled status', (tester) async { + await tester.pumpWidget(buildTestWidget(OrderStatus.cancelled)); + expect(find.text('Cancelled'), findsOneWidget); + }); + }); +}