Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.41.6
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"format-version":[1,0,0],"native-assets":{}}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
{"format-version":[1,0,0],"native-assets":{}}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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';
|
||||
|
|
@ -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<List<Order>> call() => repository.getOrders();
|
||||
}
|
||||
|
|
@ -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<Order> orders = [];
|
||||
Order? selectedOrder;
|
||||
Object? error;
|
||||
|
||||
/// Loads all orders.
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Order> _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<List<Order>> getOrders() async {
|
||||
// Simulate network latency.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
return List.unmodifiable(_orders);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OrderItem> 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import 'order.dart';
|
||||
|
||||
/// Contract for fetching and managing customer orders.
|
||||
abstract class OrdersRepository {
|
||||
/// Returns all orders.
|
||||
Future<List<Order>> getOrders();
|
||||
}
|
||||
|
|
@ -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<OrdersPage> createState() => _OrdersPageState();
|
||||
}
|
||||
|
||||
class _OrdersPageState extends State<OrdersPage> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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')}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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:
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue