feat(kell-web): add shell routing and inventory vertical slice
Validate Docs / validate-docs (push) Successful in 53s
Details
Validate Docs / validate-docs (push) Successful in 53s
Details
This commit is contained in:
parent
417430d996
commit
c7c12b3b0d
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,29 +34,18 @@ 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(
|
if (controller.error != null) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
return const Center(child: Text('Failed to load inventory data.'));
|
||||||
children: [
|
}
|
||||||
Text('Inventory', style: Theme.of(context).textTheme.headlineMedium),
|
|
||||||
const SizedBox(height: KcSpacing.sm),
|
return LayoutBuilder(
|
||||||
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) {
|
builder: (context, constraints) {
|
||||||
final width = constraints.maxWidth;
|
final width = constraints.maxWidth;
|
||||||
|
|
||||||
int crossAxisCount = 1;
|
int crossAxisCount = 1;
|
||||||
if (width >= 1200) {
|
if (width >= 1200) {
|
||||||
crossAxisCount = 3;
|
crossAxisCount = 3;
|
||||||
|
|
@ -76,11 +66,6 @@ class _InventoryPageState extends State<InventoryPage> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue