From 601a7d0373da0081a4f4736ce30660990d4d24d9 Mon Sep 17 00:00:00 2001 From: Nabil Derouiche Date: Mon, 20 Apr 2026 20:20:15 +0100 Subject: [PATCH] feat(frontend): dashboard KPI complet avec navigation drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Livrables/rapport-projet.md | 77 +++++ frontend/lib/main.dart | 36 +++ frontend/lib/models/dashboard_kpi.dart | 71 +++++ .../lib/providers/dashboard_provider.dart | 27 ++ frontend/lib/screens/dashboard_screen.dart | 283 +++++++++++++++++- frontend/lib/services/dashboard_service.dart | 9 + frontend/lib/widgets/app_drawer.dart | 132 ++++++++ frontend/lib/widgets/kpi_card.dart | 86 ++++++ 8 files changed, 709 insertions(+), 12 deletions(-) create mode 100644 frontend/lib/models/dashboard_kpi.dart create mode 100644 frontend/lib/providers/dashboard_provider.dart create mode 100644 frontend/lib/services/dashboard_service.dart create mode 100644 frontend/lib/widgets/app_drawer.dart create mode 100644 frontend/lib/widgets/kpi_card.dart diff --git a/Livrables/rapport-projet.md b/Livrables/rapport-projet.md index 3a7d68c..c663aa4 100644 --- a/Livrables/rapport-projet.md +++ b/Livrables/rapport-projet.md @@ -477,4 +477,81 @@ L'écran de connexion (`LoginScreen`) présente une interface épurée et profes --- +## 5.5 Tableau de Bord KPI (Dashboard) + +### Objectif + +Le tableau de bord est l'écran central de l'application, accessible uniquement aux utilisateurs ayant le rôle **PDG**. Il synthétise en temps réel l'état de l'entreprise à travers des indicateurs clés de performance (KPI). + +### Données affichées + +L'écran récupère les données depuis l'endpoint `GET /api/dashboard` et les affiche en trois sections : + +**Section Ventes (mois en cours)** + +| KPI | Description | +|-----|-------------| +| Chiffre d'affaires | Montant total des ventes du mois en TND | +| Commandes ce mois | Nombre de bons de commande clients créés | +| Commandes en cours | Commandes en attente de livraison | + +**Section Achats & Production** + +| KPI | Description | +|-----|-------------| +| Achats en attente | Bons de commande fournisseurs non réceptionnés | +| OF en cours | Ordres de fabrication au statut LANCE | +| OF planifiés | Ordres de fabrication au statut PLANIFIE | + +**Section Stock** + +- Nombre d'articles dont le stock est inférieur au seuil minimum +- Liste détaillée des articles en alerte avec leur quantité en stock actuelle + +### Architecture technique + +La gestion du dashboard suit le pattern **Provider** : + +``` +GET /api/dashboard + │ +DashboardService.fetchKpis() + │ +DashboardProvider (ChangeNotifier) + ├── isLoading + ├── error + └── kpi: DashboardKpi + ├── VentesKpi + ├── AchatsKpi + ├── ProductionKpi + └── StockKpi + │ +DashboardScreen (Consumer) + ├── KpiCard × 6 (grille 2 colonnes) + └── _StockSection (liste alertes) +``` + +### Navigation (Drawer) + +Un menu latéral (`AppDrawer`) permet la navigation entre tous les modules : +- Tableau de bord +- Articles +- Ventes +- Achats +- Production +- Stock +- Déconnexion + +Le drawer affiche le rôle de l'utilisateur connecté et met en surbrillance la section active. + +### Comportement UI + +- **Pull-to-refresh** : glisser vers le bas recharge les KPIs +- **Bouton actualiser** dans l'AppBar +- **État chargement** : spinner centré +- **État erreur** : message avec bouton "Réessayer" +- **Alertes stock** : icône verte si tout va bien, rouge avec liste si alertes + +--- + *[Section à compléter avec captures d'écran lors de la finalisation du rapport]* diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 40b7635..e3d4e8d 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -3,14 +3,17 @@ import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; import 'providers/auth_provider.dart'; +import 'providers/dashboard_provider.dart'; import 'screens/login_screen.dart'; import 'screens/dashboard_screen.dart'; +import 'widgets/app_drawer.dart'; void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => AuthProvider()), + ChangeNotifierProvider(create: (_) => DashboardProvider()), ], child: const RayhanApp(), ), @@ -36,6 +39,11 @@ class RayhanApp extends StatelessWidget { routes: [ GoRoute(path: '/login', builder: (_, __) => const LoginScreen()), GoRoute(path: '/dashboard', builder: (_, __) => const DashboardScreen()), + GoRoute(path: '/articles', builder: (_, __) => const _PlaceholderScreen(title: 'Articles', route: '/articles')), + GoRoute(path: '/ventes', builder: (_, __) => const _PlaceholderScreen(title: 'Ventes', route: '/ventes')), + GoRoute(path: '/achats', builder: (_, __) => const _PlaceholderScreen(title: 'Achats', route: '/achats')), + GoRoute(path: '/production', builder: (_, __) => const _PlaceholderScreen(title: 'Production', route: '/production')), + GoRoute(path: '/stock', builder: (_, __) => const _PlaceholderScreen(title: 'Stock', route: '/stock')), ], ); @@ -54,3 +62,31 @@ class RayhanApp extends StatelessWidget { ); } } + +class _PlaceholderScreen extends StatelessWidget { + final String title; + final String route; + const _PlaceholderScreen({required this.title, required this.route}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(title)), + drawer: AppDrawer(currentRoute: route), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.construction_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text('Module $title', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text('En cours de développement', + style: TextStyle(color: Colors.grey[600])), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/models/dashboard_kpi.dart b/frontend/lib/models/dashboard_kpi.dart new file mode 100644 index 0000000..ad2c05f --- /dev/null +++ b/frontend/lib/models/dashboard_kpi.dart @@ -0,0 +1,71 @@ +class DashboardKpi { + final VentesKpi ventes; + final AchatsKpi achats; + final ProductionKpi production; + final StockKpi stock; + + DashboardKpi({ + required this.ventes, + required this.achats, + required this.production, + required this.stock, + }); + + factory DashboardKpi.fromJson(Map json) => DashboardKpi( + ventes: VentesKpi.fromJson(json['ventes'] ?? {}), + achats: AchatsKpi.fromJson(json['achats'] ?? {}), + production: ProductionKpi.fromJson(json['production'] ?? {}), + stock: StockKpi.fromJson(json['stock'] ?? {}), + ); +} + +class VentesKpi { + final int commandesEnCours; + final int nbCommandesMois; + final double chiffreAffairesMois; + + VentesKpi({ + required this.commandesEnCours, + required this.nbCommandesMois, + required this.chiffreAffairesMois, + }); + + factory VentesKpi.fromJson(Map json) => VentesKpi( + commandesEnCours: json['commandesEnCours'] ?? 0, + nbCommandesMois: json['nbCommandesMois'] ?? 0, + chiffreAffairesMois: (json['chiffreAffairesMois'] ?? 0).toDouble(), + ); +} + +class AchatsKpi { + final int commandesEnAttente; + + AchatsKpi({required this.commandesEnAttente}); + + factory AchatsKpi.fromJson(Map json) => + AchatsKpi(commandesEnAttente: json['commandesEnAttente'] ?? 0); +} + +class ProductionKpi { + final int ofEnCours; + final int ofPlanifies; + + ProductionKpi({required this.ofEnCours, required this.ofPlanifies}); + + factory ProductionKpi.fromJson(Map json) => ProductionKpi( + ofEnCours: json['ofEnCours'] ?? 0, + ofPlanifies: json['ofPlanifies'] ?? 0, + ); +} + +class StockKpi { + final int articlesEnAlerte; + final List articlesEnAlerteDetails; + + StockKpi({required this.articlesEnAlerte, required this.articlesEnAlerteDetails}); + + factory StockKpi.fromJson(Map json) => StockKpi( + articlesEnAlerte: json['articlesEnAlerte'] ?? 0, + articlesEnAlerteDetails: json['articlesEnAlerteDetails'] ?? [], + ); +} diff --git a/frontend/lib/providers/dashboard_provider.dart b/frontend/lib/providers/dashboard_provider.dart new file mode 100644 index 0000000..5d778db --- /dev/null +++ b/frontend/lib/providers/dashboard_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import '../models/dashboard_kpi.dart'; +import '../services/dashboard_service.dart'; + +class DashboardProvider extends ChangeNotifier { + DashboardKpi? _kpi; + bool _isLoading = false; + String? _error; + + DashboardKpi? get kpi => _kpi; + bool get isLoading => _isLoading; + String? get error => _error; + + Future load() async { + _isLoading = true; + _error = null; + notifyListeners(); + try { + _kpi = await DashboardService.fetchKpis(); + } catch (_) { + _error = 'Impossible de charger les données. Vérifiez la connexion.'; + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/frontend/lib/screens/dashboard_screen.dart b/frontend/lib/screens/dashboard_screen.dart index 2e18414..95ccc69 100644 --- a/frontend/lib/screens/dashboard_screen.dart +++ b/frontend/lib/screens/dashboard_screen.dart @@ -1,31 +1,290 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:go_router/go_router.dart'; -import '../providers/auth_provider.dart'; +import 'package:intl/intl.dart'; -class DashboardScreen extends StatelessWidget { +import '../providers/dashboard_provider.dart'; +import '../models/dashboard_kpi.dart'; +import '../widgets/kpi_card.dart'; +import '../widgets/app_drawer.dart'; + +class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().load(); + }); + } + @override Widget build(BuildContext context) { - final auth = context.watch(); + final provider = context.watch(); + final currencyFmt = + NumberFormat.currency(locale: 'fr_TN', symbol: 'TND', decimalDigits: 3); + final dateFmt = DateFormat('EEEE d MMMM yyyy', 'fr_FR'); return Scaffold( + backgroundColor: const Color(0xFFF5F7FA), appBar: AppBar( - title: const Text('Tableau de bord'), + backgroundColor: Colors.white, + elevation: 0, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Tableau de bord', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + Text( + dateFmt.format(DateTime.now()), + style: TextStyle(fontSize: 11, color: Colors.grey[500]), + ), + ], + ), actions: [ IconButton( - icon: const Icon(Icons.logout), - onPressed: () async { - await auth.logout(); - if (context.mounted) context.go('/login'); - }, + icon: const Icon(Icons.refresh_outlined), + tooltip: 'Actualiser', + onPressed: () => context.read().load(), ), + const SizedBox(width: 8), ], ), - body: const Center( - child: Text('Dashboard — en cours de développement'), + drawer: const AppDrawer(currentRoute: '/dashboard'), + body: _buildBody(provider, currencyFmt), + ); + } + + Widget _buildBody(DashboardProvider provider, NumberFormat currencyFmt) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.wifi_off_outlined, size: 56, color: Colors.grey[400]), + const SizedBox(height: 16), + Text(provider.error!, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey[600])), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: () => context.read().load(), + icon: const Icon(Icons.refresh), + label: const Text('Réessayer'), + ), + ], + ), + ), + ); + } + + final kpi = provider.kpi; + if (kpi == null) return const SizedBox.shrink(); + + return RefreshIndicator( + onRefresh: () => context.read().load(), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionHeader(title: 'Ventes — Ce mois'), + const SizedBox(height: 12), + _VentesGrid(kpi: kpi, currencyFmt: currencyFmt), + const SizedBox(height: 24), + + _SectionHeader(title: 'Achats & Production'), + const SizedBox(height: 12), + _AchatsProductionGrid(kpi: kpi), + const SizedBox(height: 24), + + _SectionHeader(title: 'Stock'), + const SizedBox(height: 12), + _StockSection(kpi: kpi), + const SizedBox(height: 32), + ], + ), ), ); } } + +class _SectionHeader extends StatelessWidget { + final String title; + const _SectionHeader({required this.title}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Color(0xFF1F2937), + ), + ); + } +} + +class _VentesGrid extends StatelessWidget { + final DashboardKpi kpi; + final NumberFormat currencyFmt; + + const _VentesGrid({required this.kpi, required this.currencyFmt}); + + @override + Widget build(BuildContext context) { + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.1, + children: [ + KpiCard( + title: "Chiffre d'affaires", + value: currencyFmt.format(kpi.ventes.chiffreAffairesMois), + subtitle: 'Ce mois', + icon: Icons.trending_up, + color: const Color(0xFF10B981), + ), + KpiCard( + title: 'Commandes ce mois', + value: '${kpi.ventes.nbCommandesMois}', + subtitle: 'Bons de commande', + icon: Icons.receipt_long_outlined, + color: const Color(0xFF3B82F6), + ), + KpiCard( + title: 'Commandes en cours', + value: '${kpi.ventes.commandesEnCours}', + subtitle: 'En attente livraison', + icon: Icons.pending_actions_outlined, + color: const Color(0xFFF59E0B), + ), + ], + ); + } +} + +class _AchatsProductionGrid extends StatelessWidget { + final DashboardKpi kpi; + const _AchatsProductionGrid({required this.kpi}); + + @override + Widget build(BuildContext context) { + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.1, + children: [ + KpiCard( + title: 'Achats en attente', + value: '${kpi.achats.commandesEnAttente}', + subtitle: 'Bons non réceptionnés', + icon: Icons.local_shipping_outlined, + color: const Color(0xFF8B5CF6), + ), + KpiCard( + title: 'OF en cours', + value: '${kpi.production.ofEnCours}', + subtitle: 'Ordres de fabrication', + icon: Icons.precision_manufacturing_outlined, + color: const Color(0xFFEF4444), + ), + KpiCard( + title: 'OF planifiés', + value: '${kpi.production.ofPlanifies}', + subtitle: 'En attente lancement', + icon: Icons.schedule_outlined, + color: const Color(0xFF6366F1), + ), + ], + ); + } +} + +class _StockSection extends StatelessWidget { + final DashboardKpi kpi; + const _StockSection({required this.kpi}); + + @override + Widget build(BuildContext context) { + final enAlerte = kpi.stock.articlesEnAlerte; + final details = kpi.stock.articlesEnAlerteDetails; + + return Column( + children: [ + KpiCard( + title: 'Articles en alerte stock', + value: '$enAlerte', + subtitle: enAlerte == 0 + ? 'Tous les articles sont suffisamment approvisionnés' + : 'Stock inférieur au seuil minimum', + icon: enAlerte == 0 + ? Icons.check_circle_outline + : Icons.warning_amber_outlined, + color: enAlerte == 0 + ? const Color(0xFF10B981) + : const Color(0xFFEF4444), + ), + if (details.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2)), + ], + ), + child: Column( + children: details.map((item) { + final m = item as Map; + return ListTile( + leading: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.warning_amber, + color: Colors.red[600], size: 18), + ), + title: Text( + m['reference'] ?? m['designation'] ?? 'Article', + style: const TextStyle( + fontWeight: FontWeight.w600, fontSize: 13), + ), + trailing: Text( + 'Stock: ${m['quantiteEnStock'] ?? 0}', + style: TextStyle(color: Colors.red[600], fontSize: 12), + ), + ); + }).toList(), + ), + ), + ], + ], + ); + } +} diff --git a/frontend/lib/services/dashboard_service.dart b/frontend/lib/services/dashboard_service.dart new file mode 100644 index 0000000..2185043 --- /dev/null +++ b/frontend/lib/services/dashboard_service.dart @@ -0,0 +1,9 @@ +import 'api_client.dart'; +import '../models/dashboard_kpi.dart'; + +class DashboardService { + static Future fetchKpis() async { + final response = await ApiClient.instance.get('/dashboard'); + return DashboardKpi.fromJson(response.data as Map); + } +} diff --git a/frontend/lib/widgets/app_drawer.dart b/frontend/lib/widgets/app_drawer.dart new file mode 100644 index 0000000..ed449cc --- /dev/null +++ b/frontend/lib/widgets/app_drawer.dart @@ -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(); + 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); + }, + ); + } +} diff --git a/frontend/lib/widgets/kpi_card.dart b/frontend/lib/widgets/kpi_card.dart new file mode 100644 index 0000000..5934b5b --- /dev/null +++ b/frontend/lib/widgets/kpi_card.dart @@ -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]), + ), + ], + ], + ), + ), + ); + } +}