Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'app_services.dart';
|
||||
|
||||
/// An [InheritedWidget] that exposes [AppServices] to the widget tree.
|
||||
///
|
||||
/// Wrap the app (or a subtree) with [AppScope] and retrieve the services
|
||||
/// anywhere below via [AppScope.of(context)].
|
||||
class AppScope extends InheritedWidget {
|
||||
final AppServices services;
|
||||
|
||||
const AppScope({super.key, required this.services, required super.child});
|
||||
|
||||
/// Returns the nearest [AppServices] from the widget tree.
|
||||
///
|
||||
/// Throws if no [AppScope] ancestor is found.
|
||||
static AppServices of(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<AppScope>();
|
||||
assert(scope != null, 'No AppScope found in the widget tree');
|
||||
return scope!.services;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AppScope oldWidget) => services != oldWidget.services;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
|
||||
/// Holds the concrete service implementations used by the app.
|
||||
///
|
||||
/// The [AppServices.fake] factory wires up the in-memory fakes that live
|
||||
/// inside each feature package. Swap this factory for a real one when
|
||||
/// production backends are ready.
|
||||
class AppServices {
|
||||
final InventoryRepository inventoryRepository;
|
||||
final ProductPublishingRepository productPublishingRepository;
|
||||
|
||||
const AppServices({required this.inventoryRepository, required this.productPublishingRepository});
|
||||
|
||||
/// Creates an [AppServices] backed by fake, in-memory repositories.
|
||||
factory AppServices.fake() {
|
||||
return AppServices(
|
||||
inventoryRepository: FakeInventoryRepository(),
|
||||
productPublishingRepository: FakeProductPublishingRepository(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'composition/app_scope.dart';
|
||||
import 'composition/app_services.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const KellWebApp());
|
||||
final services = AppServices.fake();
|
||||
runApp(AppScope(services: services, child: const KellWebApp()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:feature_inventory/feature_inventory.dart';
|
|||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../composition/app_scope.dart';
|
||||
import '../pages/dashboard_page.dart';
|
||||
import '../pages/finance_placeholder_page.dart';
|
||||
import '../pages/integrations_placeholder_page.dart';
|
||||
|
|
@ -23,41 +24,60 @@ abstract final class AppRoutes {
|
|||
case dashboard:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
(context) =>
|
||||
const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()),
|
||||
);
|
||||
case inventory:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
||||
(context) => AppShell(
|
||||
selectedRoute: inventory,
|
||||
title: 'Inventory',
|
||||
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||
),
|
||||
);
|
||||
case products:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
(context) => AppShell(
|
||||
selectedRoute: products,
|
||||
title: 'Products',
|
||||
child: ProductPublishingPage(),
|
||||
child: ProductPublishingPage(
|
||||
repository: AppScope.of(context).productPublishingRepository,
|
||||
),
|
||||
),
|
||||
);
|
||||
case orders:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: orders, title: 'Orders', child: OrdersPlaceholderPage()),
|
||||
(context) => const AppShell(
|
||||
selectedRoute: orders,
|
||||
title: 'Orders',
|
||||
child: OrdersPlaceholderPage(),
|
||||
),
|
||||
);
|
||||
case finance:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: finance, title: 'Finance', child: FinancePlaceholderPage()),
|
||||
(context) => const AppShell(
|
||||
selectedRoute: finance,
|
||||
title: 'Finance',
|
||||
child: FinancePlaceholderPage(),
|
||||
),
|
||||
);
|
||||
case policy:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: policy, title: 'Policy', child: PolicyPlaceholderPage()),
|
||||
(context) => const AppShell(
|
||||
selectedRoute: policy,
|
||||
title: 'Policy',
|
||||
child: PolicyPlaceholderPage(),
|
||||
),
|
||||
);
|
||||
case integrations:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
(context) => const AppShell(
|
||||
selectedRoute: integrations,
|
||||
title: 'Integrations',
|
||||
child: IntegrationsPlaceholderPage(),
|
||||
|
|
@ -66,12 +86,19 @@ abstract final class AppRoutes {
|
|||
default:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
||||
(context) => AppShell(
|
||||
selectedRoute: inventory,
|
||||
title: 'Inventory',
|
||||
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static MaterialPageRoute<dynamic> _buildRoute(RouteSettings settings, Widget page) {
|
||||
return MaterialPageRoute<dynamic>(settings: settings, builder: (_) => page);
|
||||
static MaterialPageRoute<dynamic> _buildRoute(
|
||||
RouteSettings settings,
|
||||
Widget Function(BuildContext context) pageBuilder,
|
||||
) {
|
||||
return MaterialPageRoute<dynamic>(settings: settings, builder: pageBuilder);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/app.dart';
|
||||
import 'package:kell_web/composition/app_scope.dart';
|
||||
import 'package:kell_web/composition/app_services.dart';
|
||||
|
||||
Widget _buildTestApp() {
|
||||
return AppScope(services: AppServices.fake(), child: const KellWebApp());
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('app shell loads dashboard route', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Kell Creations'), findsOneWidget);
|
||||
|
|
@ -12,7 +18,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('dashboard shows summary cards', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Overview'), findsOneWidget);
|
||||
|
|
@ -23,7 +29,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('dashboard shows quick actions section', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll down to reveal the quick actions section.
|
||||
|
|
@ -41,7 +47,7 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('dashboard shows recent activity empty state', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpWidget(_buildTestApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll down to reveal the recent activity section.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../theme/kc_colors.dart';
|
||||
|
||||
class KcStatusChip extends StatelessWidget {
|
||||
final String label;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/main.dart';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('app renders inventory page', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
group('KcStatusChip', () {
|
||||
testWidgets('renders label text', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: KcStatusChip(label: 'Active', background: Colors.green, foreground: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Kell Creations'), findsOneWidget);
|
||||
expect(find.text('Inventory'), findsOneWidget);
|
||||
expect(find.text('Active'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('KcCard', () {
|
||||
testWidgets('renders child widget', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(body: KcCard(child: Text('Hello'))),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('buildKcTheme', () {
|
||||
test('returns a ThemeData', () {
|
||||
final theme = buildKcTheme();
|
||||
expect(theme, isA<ThemeData>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
library;
|
||||
|
||||
export 'src/presentation/inventory_page.dart';
|
||||
export 'src/data/fake_inventory_repository.dart';
|
||||
export 'src/domain/inventory_item.dart';
|
||||
export 'src/domain/inventory_repository.dart';
|
||||
export 'src/domain/inventory_status.dart';
|
||||
export 'src/presentation/inventory_page.dart';
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import '../application/get_inventory_items.dart';
|
||||
import '../application/inventory_controller.dart';
|
||||
import '../data/fake_inventory_repository.dart';
|
||||
import '../domain/inventory_repository.dart';
|
||||
import 'widgets/inventory_item_card.dart';
|
||||
|
||||
class InventoryPage extends StatefulWidget {
|
||||
const InventoryPage({super.key});
|
||||
final InventoryRepository repository;
|
||||
|
||||
const InventoryPage({super.key, required this.repository});
|
||||
|
||||
@override
|
||||
State<InventoryPage> createState() => _InventoryPageState();
|
||||
|
|
@ -19,7 +21,7 @@ class _InventoryPageState extends State<InventoryPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = InventoryController(GetInventoryItems(FakeInventoryRepository()));
|
||||
controller = InventoryController(GetInventoryItems(widget.repository));
|
||||
controller.load();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
name: feature_inventory
|
||||
description: "A new Flutter package project."
|
||||
version: 0.0.1
|
||||
publish_to: "none"
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
|
|
|
|||
|
|
@ -3,10 +3,37 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
|
||||
void main() {
|
||||
test('adds one to input values', () {
|
||||
final calculator = Calculator();
|
||||
expect(calculator.addOne(2), 3);
|
||||
expect(calculator.addOne(-7), -6);
|
||||
expect(calculator.addOne(0), 1);
|
||||
group('InventoryStatus', () {
|
||||
test('has four values', () {
|
||||
expect(InventoryStatus.values.length, 4);
|
||||
});
|
||||
|
||||
test('contains expected statuses', () {
|
||||
expect(InventoryStatus.values, contains(InventoryStatus.inStock));
|
||||
expect(InventoryStatus.values, contains(InventoryStatus.lowStock));
|
||||
expect(InventoryStatus.values, contains(InventoryStatus.outOfStock));
|
||||
expect(InventoryStatus.values, contains(InventoryStatus.draft));
|
||||
});
|
||||
});
|
||||
|
||||
group('FakeInventoryRepository', () {
|
||||
late FakeInventoryRepository repository;
|
||||
|
||||
setUp(() {
|
||||
repository = FakeInventoryRepository();
|
||||
});
|
||||
|
||||
test('returns six sample items', () async {
|
||||
final items = await repository.getInventoryItems();
|
||||
expect(items.length, 6);
|
||||
});
|
||||
|
||||
test('returns items with expected names', () async {
|
||||
final items = await repository.getInventoryItems();
|
||||
final names = items.map((i) => i.name).toList();
|
||||
expect(names, contains('Floral Bowl Cozy'));
|
||||
expect(names, contains('Citrus Coaster Set'));
|
||||
expect(names, contains('Ocean Nightlight'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
library;
|
||||
|
||||
export 'src/data/fake_product_publishing_repository.dart';
|
||||
export 'src/domain/product_draft.dart';
|
||||
export 'src/domain/product_publishing_repository.dart';
|
||||
export 'src/domain/publish_status.dart';
|
||||
export 'src/presentation/product_publishing_page.dart';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import '../application/get_product_drafts.dart';
|
||||
import '../application/product_publishing_controller.dart';
|
||||
import '../application/publish_product.dart';
|
||||
import '../data/fake_product_publishing_repository.dart';
|
||||
import '../domain/product_publishing_repository.dart';
|
||||
import 'widgets/product_draft_card.dart';
|
||||
import 'widgets/product_preview_panel.dart';
|
||||
|
||||
|
|
@ -13,7 +13,9 @@ import 'widgets/product_preview_panel.dart';
|
|||
/// Displays a list of product drafts on the left and a preview panel on the
|
||||
/// right. Users can select a draft to preview and publish it to the store.
|
||||
class ProductPublishingPage extends StatefulWidget {
|
||||
const ProductPublishingPage({super.key});
|
||||
final ProductPublishingRepository repository;
|
||||
|
||||
const ProductPublishingPage({super.key, required this.repository});
|
||||
|
||||
@override
|
||||
State<ProductPublishingPage> createState() => _ProductPublishingPageState();
|
||||
|
|
@ -25,7 +27,7 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final repo = FakeProductPublishingRepository();
|
||||
final repo = widget.repository;
|
||||
controller = ProductPublishingController(GetProductDrafts(repo), PublishProduct(repo));
|
||||
controller.load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||
import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart';
|
||||
|
||||
void main() {
|
||||
late FakeProductPublishingRepository repository;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:feature_wordpress/feature_wordpress.dart';
|
|||
import 'package:feature_wordpress/src/application/get_product_drafts.dart';
|
||||
import 'package:feature_wordpress/src/application/product_publishing_controller.dart';
|
||||
import 'package:feature_wordpress/src/application/publish_product.dart';
|
||||
import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart';
|
||||
|
||||
void main() {
|
||||
late FakeProductPublishingRepository repository;
|
||||
|
|
|
|||
Loading…
Reference in New Issue