feat(frontend): dashboard KPI complet avec navigation drawer
- DashboardProvider + DashboardService (GET /api/dashboard) - Modèle DashboardKpi (Ventes, Achats, Production, Stock) - DashboardScreen : grilles KpiCard, alertes stock, pull-to-refresh - KpiCard widget réutilisable (icône, couleur, valeur, sous-titre) - AppDrawer : navigation complète avec surbrillance route active - Placeholders pour modules Article/Ventes/Achats/Production/Stock - Rapport PFE : section 5.5 Dashboard (architecture, KPIs, UI) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
class AppDrawer extends StatelessWidget {
|
||||
final String currentRoute;
|
||||
|
||||
const AppDrawer({super.key, required this.currentRoute});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final auth = context.read<AuthProvider>();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Drawer(
|
||||
child: Column(
|
||||
children: [
|
||||
DrawerHeader(
|
||||
decoration: BoxDecoration(color: theme.colorScheme.primary),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 26,
|
||||
backgroundColor: Colors.white24,
|
||||
child: Icon(Icons.person, color: Colors.white, size: 28),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Rayhan ERP',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
auth.role?.replaceAll('ROLE_', '') ?? '',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.dashboard_outlined,
|
||||
label: 'Tableau de bord',
|
||||
route: '/dashboard',
|
||||
current: currentRoute,
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
label: 'Articles',
|
||||
route: '/articles',
|
||||
current: currentRoute,
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.shopping_cart_outlined,
|
||||
label: 'Ventes',
|
||||
route: '/ventes',
|
||||
current: currentRoute,
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.local_shipping_outlined,
|
||||
label: 'Achats',
|
||||
route: '/achats',
|
||||
current: currentRoute,
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.precision_manufacturing_outlined,
|
||||
label: 'Production',
|
||||
route: '/production',
|
||||
current: currentRoute,
|
||||
),
|
||||
_DrawerItem(
|
||||
icon: Icons.warehouse_outlined,
|
||||
label: 'Stock',
|
||||
route: '/stock',
|
||||
current: currentRoute,
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
title: const Text('Déconnexion',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
onTap: () async {
|
||||
await auth.logout();
|
||||
if (context.mounted) context.go('/login');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DrawerItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String route;
|
||||
final String current;
|
||||
|
||||
const _DrawerItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.route,
|
||||
required this.current,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selected = current == route;
|
||||
final color = selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: const Color(0xFF374151);
|
||||
|
||||
return ListTile(
|
||||
selected: selected,
|
||||
selectedTileColor:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.08),
|
||||
leading: Icon(icon, color: color),
|
||||
title: Text(label,
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: selected ? FontWeight.w600 : FontWeight.normal)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (current != route) context.go(route);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KpiCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final String? subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const KpiCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.value,
|
||||
this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withOpacity(0.15),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
),
|
||||
if (onTap != null)
|
||||
Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey[400]),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user