Add Kell Creations operations app foundation with feature slices and WooCommerce read-only integration #1
|
|
@ -11,7 +11,7 @@ class KellWebApp extends StatelessWidget {
|
|||
title: 'Kell Creations',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildKcTheme(),
|
||||
initialRoute: AppRoutes.inventory,
|
||||
initialRoute: AppRoutes.dashboard,
|
||||
onGenerateRoute: AppRoutes.onGenerateRoute,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../routing/app_routes.dart';
|
||||
import '../shell/widgets/empty_state_panel.dart';
|
||||
import '../shell/widgets/section_header.dart';
|
||||
import '../shell/widgets/summary_card.dart';
|
||||
|
||||
/// The main dashboard page showing stub summary data.
|
||||
///
|
||||
/// Displays KPI cards (total products, in-stock, low-stock, draft) and a
|
||||
/// quick-actions section. All data is hard-coded stub data until backend
|
||||
/// integration is added.
|
||||
class DashboardPage extends StatelessWidget {
|
||||
const DashboardPage({super.key});
|
||||
|
||||
// ── Stub data ──────────────────────────────────────────────────────────
|
||||
|
||||
static const int _totalProducts = 24;
|
||||
static const int _inStock = 18;
|
||||
static const int _lowStock = 4;
|
||||
static const int _draft = 2;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
const SectionHeader(title: 'Overview'),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
_buildSummaryGrid(context),
|
||||
const SizedBox(height: KcSpacing.xl),
|
||||
SectionHeader(
|
||||
title: 'Quick Actions',
|
||||
action: TextButton(
|
||||
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.inventory),
|
||||
child: const Text('Go to Inventory'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: KcSpacing.xl),
|
||||
const SectionHeader(title: 'Recent Activity'),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
const EmptyStatePanel(
|
||||
icon: Icons.history,
|
||||
message:
|
||||
'No recent activity yet.\nActivity will appear here once orders and updates are tracked.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Summary cards grid ─────────────────────────────────────────────────
|
||||
|
||||
Widget _buildSummaryGrid(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
|
||||
int crossAxisCount = 2;
|
||||
if (width >= 900) {
|
||||
crossAxisCount = 4;
|
||||
} else if (width >= 600) {
|
||||
crossAxisCount = 2;
|
||||
}
|
||||
|
||||
final cards = [
|
||||
const SummaryCard(
|
||||
icon: Icons.inventory_2,
|
||||
iconColor: KcColors.denimBlue,
|
||||
label: 'Total Products',
|
||||
value: '$_totalProducts',
|
||||
),
|
||||
const SummaryCard(
|
||||
icon: Icons.check_circle_outline,
|
||||
iconColor: KcColors.success,
|
||||
label: 'In Stock',
|
||||
value: '$_inStock',
|
||||
),
|
||||
const SummaryCard(
|
||||
icon: Icons.warning_amber_rounded,
|
||||
iconColor: KcColors.warning,
|
||||
label: 'Low Stock',
|
||||
value: '$_lowStock',
|
||||
),
|
||||
const SummaryCard(
|
||||
icon: Icons.edit_note,
|
||||
iconColor: KcColors.neutral,
|
||||
label: 'Draft',
|
||||
value: '$_draft',
|
||||
),
|
||||
];
|
||||
|
||||
return GridView.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: KcSpacing.md,
|
||||
mainAxisSpacing: KcSpacing.md,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1.8,
|
||||
children: cards,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── Quick actions ──────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return KcCard(
|
||||
child: Wrap(
|
||||
spacing: KcSpacing.sm,
|
||||
runSpacing: KcSpacing.sm,
|
||||
children: [
|
||||
_QuickActionChip(
|
||||
icon: Icons.add,
|
||||
label: 'New Product',
|
||||
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.products),
|
||||
),
|
||||
_QuickActionChip(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
label: 'View Inventory',
|
||||
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.inventory),
|
||||
),
|
||||
_QuickActionChip(
|
||||
icon: Icons.receipt_long_outlined,
|
||||
label: 'View Orders',
|
||||
onPressed: () => Navigator.of(context).pushReplacementNamed(AppRoutes.orders),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private helper widget ──────────────────────────────────────────────────
|
||||
|
||||
class _QuickActionChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _QuickActionChip({required this.icon, required this.label, required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(avatar: Icon(icon, size: 18), label: Text(label), onPressed: onPressed);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:feature_inventory/feature_inventory.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../pages/dashboard_placeholder_page.dart';
|
||||
import '../pages/dashboard_page.dart';
|
||||
import '../pages/finance_placeholder_page.dart';
|
||||
import '../pages/integrations_placeholder_page.dart';
|
||||
import '../pages/orders_placeholder_page.dart';
|
||||
|
|
@ -23,11 +23,7 @@ abstract final class AppRoutes {
|
|||
case dashboard:
|
||||
return _buildRoute(
|
||||
settings,
|
||||
const AppShell(
|
||||
selectedRoute: dashboard,
|
||||
title: 'Dashboard',
|
||||
child: DashboardPlaceholderPage(),
|
||||
),
|
||||
const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()),
|
||||
);
|
||||
case inventory:
|
||||
return _buildRoute(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A reusable empty-state panel shown when a section has no data yet.
|
||||
///
|
||||
/// Displays an [icon], a [message], and an optional [action] widget
|
||||
/// (e.g. a button to create the first item).
|
||||
class EmptyStatePanel extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String message;
|
||||
final Widget? action;
|
||||
|
||||
const EmptyStatePanel({super.key, required this.icon, required this.message, this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KcCard(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: KcSpacing.xl),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 48, color: KcColors.neutral),
|
||||
const SizedBox(height: KcSpacing.md),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: KcColors.neutral),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (action != null) ...[const SizedBox(height: KcSpacing.md), action!],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A reusable section header used across app pages.
|
||||
///
|
||||
/// Displays a [title] with an optional trailing [action] widget
|
||||
/// (e.g. a "View all" button).
|
||||
class SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? action;
|
||||
|
||||
const SectionHeader({super.key, required this.title, this.action});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: KcSpacing.sm),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
?action,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A stat / summary card that displays a [value] with a [label] and an [icon].
|
||||
///
|
||||
/// Used on the dashboard to show high-level KPIs such as total products,
|
||||
/// in-stock count, etc.
|
||||
class SummaryCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const SummaryCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return KcCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: iconColor, size: 28),
|
||||
const SizedBox(height: KcSpacing.sm),
|
||||
Text(value, style: theme.textTheme.headlineMedium?.copyWith(fontSize: 32)),
|
||||
const SizedBox(height: KcSpacing.xs),
|
||||
Text(label, style: theme.textTheme.bodyMedium?.copyWith(color: KcColors.neutral)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kell_web/app.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('app shell loads inventory route', (WidgetTester tester) async {
|
||||
testWidgets('app shell loads dashboard route', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Kell Creations'), findsOneWidget);
|
||||
expect(find.text('Inventory'), findsWidgets);
|
||||
expect(find.text('Dashboard'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('dashboard shows summary cards', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Overview'), findsOneWidget);
|
||||
expect(find.text('Total Products'), findsOneWidget);
|
||||
expect(find.text('In Stock'), findsOneWidget);
|
||||
expect(find.text('Low Stock'), findsOneWidget);
|
||||
expect(find.text('Draft'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('dashboard shows quick actions section', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll down to reveal the quick actions section.
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Quick Actions'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Quick Actions'), findsOneWidget);
|
||||
expect(find.text('New Product'), findsOneWidget);
|
||||
expect(find.text('View Inventory'), findsOneWidget);
|
||||
expect(find.text('View Orders'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('dashboard shows recent activity empty state', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const KellWebApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll down to reveal the recent activity section.
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Recent Activity'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Recent Activity'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue