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
13 changed files with 333 additions and 66 deletions
Showing only changes of commit c7c12b3b0d - Show all commits

View File

@ -0,0 +1,18 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'routing/app_routes.dart';
class KellWebApp extends StatelessWidget {
const KellWebApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Kell Creations',
debugShowCheckedModeBanner: false,
theme: buildKcTheme(),
initialRoute: AppRoutes.inventory,
onGenerateRoute: AppRoutes.onGenerateRoute,
);
}
}

View File

@ -1,21 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:feature_inventory/feature_inventory.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'app.dart';
void main() { void main() {
runApp(const KellWebApp()); runApp(const KellWebApp());
} }
class KellWebApp extends StatelessWidget {
const KellWebApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Kell Creations',
debugShowCheckedModeBanner: false,
theme: buildKcTheme(),
home: const InventoryPage(),
);
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class DashboardPlaceholderPage extends StatelessWidget {
const DashboardPlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Dashboard page coming soon'));
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class FinancePlaceholderPage extends StatelessWidget {
const FinancePlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Finance page coming soon'));
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class IntegrationsPlaceholderPage extends StatelessWidget {
const IntegrationsPlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Integrations page coming soon'));
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class OrdersPlaceholderPage extends StatelessWidget {
const OrdersPlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Orders page coming soon'));
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class PolicyPlaceholderPage extends StatelessWidget {
const PolicyPlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Policy page coming soon'));
}
}

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class ProductsPlaceholderPage extends StatelessWidget {
const ProductsPlaceholderPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Products page coming soon'));
}
}

View File

@ -0,0 +1,81 @@
import 'package:feature_inventory/feature_inventory.dart';
import 'package:flutter/material.dart';
import '../pages/dashboard_placeholder_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 '../pages/products_placeholder_page.dart';
import '../shell/app_shell.dart';
abstract final class AppRoutes {
static const String dashboard = '/';
static const String inventory = '/inventory';
static const String products = '/products';
static const String orders = '/orders';
static const String finance = '/finance';
static const String policy = '/policy';
static const String integrations = '/integrations';
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case dashboard:
return _buildRoute(
settings,
const AppShell(
selectedRoute: dashboard,
title: 'Dashboard',
child: DashboardPlaceholderPage(),
),
);
case inventory:
return _buildRoute(
settings,
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
);
case products:
return _buildRoute(
settings,
const AppShell(
selectedRoute: products,
title: 'Products',
child: ProductsPlaceholderPage(),
),
);
case orders:
return _buildRoute(
settings,
const AppShell(selectedRoute: orders, title: 'Orders', child: OrdersPlaceholderPage()),
);
case finance:
return _buildRoute(
settings,
const AppShell(selectedRoute: finance, title: 'Finance', child: FinancePlaceholderPage()),
);
case policy:
return _buildRoute(
settings,
const AppShell(selectedRoute: policy, title: 'Policy', child: PolicyPlaceholderPage()),
);
case integrations:
return _buildRoute(
settings,
const AppShell(
selectedRoute: integrations,
title: 'Integrations',
child: IntegrationsPlaceholderPage(),
),
);
default:
return _buildRoute(
settings,
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
);
}
}
static MaterialPageRoute<dynamic> _buildRoute(RouteSettings settings, Widget page) {
return MaterialPageRoute<dynamic>(settings: settings, builder: (_) => page);
}
}

View File

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import '../routing/app_routes.dart';
class AppShell extends StatelessWidget {
final String selectedRoute;
final String title;
final Widget child;
const AppShell({
super.key,
required this.selectedRoute,
required this.title,
required this.child,
});
@override
Widget build(BuildContext context) {
final index = _routeToIndex(selectedRoute);
return Scaffold(
appBar: AppBar(title: const Text('Kell Creations')),
body: Row(
children: [
NavigationRail(
selectedIndex: index,
onDestinationSelected: (selectedIndex) {
final route = _indexToRoute(selectedIndex);
if (route != selectedRoute) {
Navigator.of(context).pushReplacementNamed(route);
}
},
labelType: NavigationRailLabelType.all,
minWidth: 88,
minExtendedWidth: 200,
leading: const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Icon(Icons.storefront_outlined, size: 32),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.dashboard_outlined),
selectedIcon: Icon(Icons.dashboard),
label: Text('Dashboard'),
),
NavigationRailDestination(
icon: Icon(Icons.inventory_2_outlined),
selectedIcon: Icon(Icons.inventory_2),
label: Text('Inventory'),
),
NavigationRailDestination(
icon: Icon(Icons.sell_outlined),
selectedIcon: Icon(Icons.sell),
label: Text('Products'),
),
NavigationRailDestination(
icon: Icon(Icons.receipt_long_outlined),
selectedIcon: Icon(Icons.receipt_long),
label: Text('Orders'),
),
NavigationRailDestination(
icon: Icon(Icons.attach_money_outlined),
selectedIcon: Icon(Icons.attach_money),
label: Text('Finance'),
),
NavigationRailDestination(
icon: Icon(Icons.policy_outlined),
selectedIcon: Icon(Icons.policy),
label: Text('Policy'),
),
NavigationRailDestination(
icon: Icon(Icons.hub_outlined),
selectedIcon: Icon(Icons.hub),
label: Text('Integrations'),
),
],
),
const VerticalDivider(width: 1),
Expanded(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 8),
Expanded(child: child),
],
),
),
),
],
),
);
}
static int _routeToIndex(String route) {
switch (route) {
case AppRoutes.dashboard:
return 0;
case AppRoutes.inventory:
return 1;
case AppRoutes.products:
return 2;
case AppRoutes.orders:
return 3;
case AppRoutes.finance:
return 4;
case AppRoutes.policy:
return 5;
case AppRoutes.integrations:
return 6;
default:
return 1;
}
}
static String _indexToRoute(int index) {
switch (index) {
case 0:
return AppRoutes.dashboard;
case 1:
return AppRoutes.inventory;
case 2:
return AppRoutes.products;
case 3:
return AppRoutes.orders;
case 4:
return AppRoutes.finance;
case 5:
return AppRoutes.policy;
case 6:
return AppRoutes.integrations;
default:
return AppRoutes.inventory;
}
}
}

View File

@ -1,12 +1,12 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:kell_web/main.dart'; import 'package:kell_web/app.dart';
void main() { void main() {
testWidgets('app renders inventory page', (WidgetTester tester) async { testWidgets('app shell loads inventory route', (WidgetTester tester) async {
await tester.pumpWidget(const KellWebApp()); await tester.pumpWidget(const KellWebApp());
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Kell Creations'), findsOneWidget); expect(find.text('Kell Creations'), findsOneWidget);
expect(find.text('Inventory'), findsOneWidget); expect(find.text('Inventory'), findsWidgets);
}); });
} }

View File

@ -1,5 +1,6 @@
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../application/get_inventory_items.dart'; import '../application/get_inventory_items.dart';
import '../application/inventory_controller.dart'; import '../application/inventory_controller.dart';
import '../data/fake_inventory_repository.dart'; import '../data/fake_inventory_repository.dart';
@ -33,54 +34,38 @@ class _InventoryPageState extends State<InventoryPage> {
return AnimatedBuilder( return AnimatedBuilder(
animation: controller, animation: controller,
builder: (context, _) { builder: (context, _) {
return Scaffold( if (controller.isLoading) {
appBar: AppBar(title: const Text('Kell Creations')), return const Center(child: CircularProgressIndicator());
body: Padding( }
padding: const EdgeInsets.all(KcSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Inventory', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: KcSpacing.sm),
Text(
'Manage handmade products, stock levels, and readiness.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: KcSpacing.lg),
if (controller.isLoading)
const Expanded(child: Center(child: CircularProgressIndicator()))
else if (controller.error != null)
Expanded(child: Center(child: Text('Failed to load inventory data.')))
else
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
int crossAxisCount = 1;
if (width >= 1200) {
crossAxisCount = 3;
} else if (width >= 700) {
crossAxisCount = 2;
}
return GridView.builder( if (controller.error != null) {
itemCount: controller.items.length, return const Center(child: Text('Failed to load inventory data.'));
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( }
crossAxisCount: crossAxisCount,
crossAxisSpacing: KcSpacing.md, return LayoutBuilder(
mainAxisSpacing: KcSpacing.md, builder: (context, constraints) {
childAspectRatio: 1.5, final width = constraints.maxWidth;
),
itemBuilder: (context, index) { int crossAxisCount = 1;
return InventoryItemCard(item: controller.items[index]); if (width >= 1200) {
}, crossAxisCount = 3;
); } else if (width >= 700) {
}, crossAxisCount = 2;
), }
),
], return GridView.builder(
), itemCount: controller.items.length,
), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: KcSpacing.md,
mainAxisSpacing: KcSpacing.md,
childAspectRatio: 1.5,
),
itemBuilder: (context, index) {
return InventoryItemCard(item: controller.items[index]);
},
);
},
); );
}, },
); );