Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1

Merged
mtkell merged 12 commits from feat/inventory-first-slice into main 2026-04-04 19:46:31 +00:00
36 changed files with 1609 additions and 4 deletions
Showing only changes of commit 3330ed23b3 - Show all commits

View File

@ -1,4 +1,5 @@
import 'package:feature_inventory/feature_inventory.dart'; import 'package:feature_inventory/feature_inventory.dart';
import 'package:feature_orders/feature_orders.dart';
import 'package:feature_wordpress/feature_wordpress.dart'; import 'package:feature_wordpress/feature_wordpress.dart';
/// Holds the concrete service implementations used by the app. /// Holds the concrete service implementations used by the app.
@ -8,14 +9,20 @@ import 'package:feature_wordpress/feature_wordpress.dart';
/// production backends are ready. /// production backends are ready.
class AppServices { class AppServices {
final InventoryRepository inventoryRepository; final InventoryRepository inventoryRepository;
final OrdersRepository ordersRepository;
final ProductPublishingRepository productPublishingRepository; 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. /// Creates an [AppServices] backed by fake, in-memory repositories.
factory AppServices.fake() { factory AppServices.fake() {
return AppServices( return AppServices(
inventoryRepository: FakeInventoryRepository(), inventoryRepository: FakeInventoryRepository(),
ordersRepository: FakeOrdersRepository(),
productPublishingRepository: FakeProductPublishingRepository(), productPublishingRepository: FakeProductPublishingRepository(),
); );
} }

View File

@ -1,4 +1,5 @@
import 'package:feature_inventory/feature_inventory.dart'; import 'package:feature_inventory/feature_inventory.dart';
import 'package:feature_orders/feature_orders.dart';
import 'package:feature_wordpress/feature_wordpress.dart'; import 'package:feature_wordpress/feature_wordpress.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -6,7 +7,6 @@ import '../composition/app_scope.dart';
import '../pages/dashboard_page.dart'; import '../pages/dashboard_page.dart';
import '../pages/finance_placeholder_page.dart'; import '../pages/finance_placeholder_page.dart';
import '../pages/integrations_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart';
import '../pages/orders_placeholder_page.dart';
import '../pages/policy_placeholder_page.dart'; import '../pages/policy_placeholder_page.dart';
import '../shell/app_shell.dart'; import '../shell/app_shell.dart';
@ -50,10 +50,10 @@ abstract final class AppRoutes {
case orders: case orders:
return _buildRoute( return _buildRoute(
settings, settings,
(context) => const AppShell( (context) => AppShell(
selectedRoute: orders, selectedRoute: orders,
title: 'Orders', title: 'Orders',
child: OrdersPlaceholderPage(), child: OrdersPage(repository: AppScope.of(context).ordersRepository),
), ),
); );
case finance: case finance:

View File

@ -78,6 +78,13 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
feature_orders:
dependency: "direct main"
description:
path: "../../packages/feature_orders"
relative: true
source: path
version: "0.0.1"
feature_wordpress: feature_wordpress:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -41,6 +41,8 @@ dependencies:
path: ../../packages/design_system path: ../../packages/design_system
feature_inventory: feature_inventory:
path: ../../packages/feature_inventory path: ../../packages/feature_inventory
feature_orders:
path: ../../packages/feature_orders
feature_wordpress: feature_wordpress:
path: ../../packages/feature_wordpress path: ../../packages/feature_wordpress

View File

@ -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"
}

View File

@ -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
}

View File

@ -0,0 +1 @@
3.41.6

View File

@ -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

View File

@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

View File

@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

View File

@ -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';

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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,
}

View File

@ -0,0 +1,7 @@
import 'order.dart';
/// Contract for fetching and managing customer orders.
abstract class OrdersRepository {
/// Returns all orders.
Future<List<Order>> getOrders();
}

View File

@ -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);
}
}

View File

@ -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')}';
}
}

View File

@ -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)),
],
),
);
}
}

View File

@ -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);
}
}
}

View File

@ -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"

View File

@ -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:

View File

@ -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);
});
});
}

View File

@ -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');
});
}

View File

@ -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);
});
});
}

View File

@ -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);
});
});
}

View File

@ -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);
});
});
}

View File

@ -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);
});
});
}