import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:inventory_app/models/depot.dart'; import 'package:inventory_app/models/tracking_number.dart'; import 'package:inventory_app/views/qr_view.dart'; import 'package:inventory_app/controllers/home_controller.dart'; import 'package:inventory_app/service/product_image_updater.dart'; import 'package:inventory_app/views/bureau_inventory_page.dart'; import 'package:inventory_app/views/my_scans.dart'; import 'package:inventory_app/views/product_view.dart'; import 'package:inventory_app/widgets/glass_widgets.dart'; import 'package:quickalert/quickalert.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:inventory_app/utils.dart'; import 'controllers/user_controller.dart'; class Home extends StatefulWidget { const Home({super.key}); @override State createState() => _HomeState(); } class _HomeState extends State { late final HomeController controller; final String _selectedState = 'Neuf'; @override void initState() { super.initState(); controller = Get.find(); // IMPORTANT: No new instance } @override Widget build(BuildContext context) { final primary = Colors.blue; return Scaffold( body: RefreshIndicator( onRefresh: () async { await controller.fetchLocations(); }, child: Stack( children: [ const FancyBackground(), /// Only the loading OR the content rebuild Obx(() { if (controller.isLoading.value) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Center(child: CircularProgressIndicator()), SizedBox(height: 20.0), OutlinedButton( onPressed: () async { await controller.fetchLocations(); }, child: Text("Rafraîchir si c'est trop long"), ), ], ); } return _buildContent(context, primary); }), ], ), ), floatingActionButton: FancyFAB( primary: primary, onTap: () => _showScannerSheet(context, primary), ), drawer: FancyDrawer( primary: primary, onLogout: controller.logout, onScanBureau: () => _scanAndShowBureauInventory(), ), ); } // ---------------- MAIN CONTENT ------------------- Widget _buildContent(BuildContext context, MaterialColor primary) { return SafeArea( child: Column( children: [ GlassAppBar( title: "Inventaire", primary: primary, onLogout: controller.logout, onCamera: controller.product.value != null ? () { final updater = ProductImageUpdater( baseUrl: Utils.url, ); updater.updateProductPictureFlow( context, controller.product.value!, ); } : null, ), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 6, 16, 100), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Counting SectionHeader( icon: Icons.tune_rounded, title: "Type de comptage", accent: primary, ), GlassCard( child: Obx( () => Column( children: [ TileRadio( value: "comptage1", groupValue: controller.selectedOption.value, label: 'Comptage 1', icon: Icons.filter_1, onChanged: (v) => controller.selectedOption.value = v!, ), TileRadio( value: "comptage3", groupValue: controller.selectedOption.value, label: 'Comptage Contrôle', icon: Icons.shield_rounded, onChanged: (v) => controller.selectedOption.value = v!, ), ], ), ), ), // Depot SectionHeader( icon: Icons.store_mall_directory_rounded, title: "Zone", accent: primary, ), GlassCard( child: Obx( () => AppDropdown( value: controller.selectedDepot.value, items: controller.depotList, hint: "Choisir une zone", labelBuilder: (d) => d.name ?? "", onChanged: (d) => controller.selectedDepot.value = d, ), ), ), // Product Info SectionHeader( icon: Icons.inventory_2_rounded, title: "Information article", accent: primary, ), GlassCard( child: Column( children: [ AppTextField( controller: controller.outputController, label: "Code article", icon: Icons.qr_code_2_rounded, enabled: false, ), const SizedBox(height: 12), AppTextField( controller: controller.labelController, label: "Libellé article", icon: Icons.label_rounded, enabled: false, ), const SizedBox(height: 12), AppTextField( controller: controller.unitController, label: "Unité article", icon: Icons.scale_rounded, enabled: false, ), const SizedBox(height: 16), SubLabel("N° de suivi", primary), const SizedBox(height: 8), Obx( () => AppDropdown( value: controller.selectedTrackingNumber.value, items: controller.trackingNumbers, hint: "Sélectionner un N° de suivi", labelBuilder: (t) => t.trackingNumberSeq ?? "", onChanged: (t) => controller.selectedTrackingNumber.value = t, ), ), SubLabel("Serial number ", primary), FancyTextField( icon: Icons.segment_rounded, controller: controller.serialNumberTxt, label: 'Serial number', hint: 'Serial number', ), SubLabel("Etat ", primary), const SizedBox(height: 8), AppDropdown( hint: 'Etat', value: _selectedState, items: const ['Ancien', 'Moyen', 'Neuf', 'Réformée'], onChanged: (val) { if (val != null) { HapticFeedback.selectionClick(); setState( () => controller.selectedState.value = val, ); } }, labelBuilder: (String t) => t, ), ], ), ), const SizedBox(height: 16), // Product image Obx(() { final product = controller.product.value; if (product?.picture != null) { return AppImage( future: controller.fetchImageBytes( product!.picture!.id!, ), ); } return const SizedBox.shrink(); }), const SizedBox(height: 16), // Quantity SectionHeader( icon: Icons.exposure_plus_1_rounded, title: "Quantité", accent: primary, ), GlassCard( child: AppTextField( controller: controller.qtyController, label: "Quantité", type: TextInputType.number, icon: Icons.onetwothree, ), ), const SizedBox(height: 16), // Depot Image Obx(() { final depot = controller.selectedDepot.value; if (depot?.picture != null) { return AppImage( future: controller.fetchImageBytes(depot!.picture!.id!), ); } return const SizedBox.shrink(); }), const SizedBox(height: 24), _saveButton(primary), ], ), ), ), ], ), ); } // ---------------- SAVE BUTTON ------------------- Widget _saveButton(MaterialColor primary) { return ElevatedButton.icon( icon: const Icon(Icons.save, color: Colors.white), label: const Text("Enregistrer", style: TextStyle(color: Colors.white)), style: ElevatedButton.styleFrom( backgroundColor: primary, minimumSize: const Size.fromHeight(50), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), onPressed: () async { if (controller.selectedDepot.value == null) { QuickAlert.show( context: context, type: QuickAlertType.error, title: "Veuillez selectionnez la zone d'abord", text: "En cours...", ); return; } print('************************'); print(controller.selectedTrackingNumber.value); QuickAlert.show( context: context, type: QuickAlertType.loading, title: "Enregistrement", text: "En cours...", ); HapticFeedback.mediumImpact(); await controller.save(); Navigator.pop(context); QuickAlert.show( context: context, type: QuickAlertType.success, title: "Succès", text: "Transaction enregistrée", ); }, ); } // ---------------- SCANNER SHEET ------------------- void _showScannerSheet(BuildContext context, MaterialColor primary) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (_) { return GlassBottomSheet( child: Column( mainAxisSize: MainAxisSize.min, children: [ SheetHandle(primary), ListTile( leading: const Icon(Icons.qr_code_scanner), title: const Text("Scanner Article"), onTap: () async { Navigator.pop(context); final r = await Get.to(() => const QRViewExample()); if (r is String) { controller.handleScannedCode(r); } else Get.to(() => const CreateProductPage()); }, ), ListTile( leading: const Icon(Icons.location_searching), title: const Text("Scanner Bureau"), onTap: () async { Navigator.pop(context); final r = await Get.to(() => const QRViewExample()); if (r is String) controller.handleLocationScan(r); }, ), ListTile( leading: const Icon(Icons.business_center_rounded, color: Colors.teal), title: const Text("Voir Articles d'un Bureau"), subtitle: const Text("Scanner le QR du bureau pour afficher ses articles"), onTap: () async { Navigator.pop(context); await _scanAndShowBureauInventory(); }, ), ], ), ); }, ); } // ---------------- BUREAU INVENTORY SCAN ------------------- Future _scanAndShowBureauInventory() async { final r = await Get.to(() => const QRViewExample()); if (r is! String) return; Get.dialog( const Center(child: CircularProgressIndicator()), barrierDismissible: false, ); final locationJson = await controller.client.fetchStockLocationByName(r); Get.back(); // close loading spinner if (locationJson == null) { Get.snackbar( 'Erreur', 'Bureau introuvable. Vérifiez le QR code.', backgroundColor: Colors.red, colorText: Colors.white, ); return; } final depot = Depot.fromJson(locationJson); Get.to( () => BureauInventoryPage( locationId: depot.id!, locationName: depot.name ?? r, ), ); } } // ------------------------------------------------------------- // GLASS APP BAR // ------------------------------------------------------------- class GlassAppBar extends StatelessWidget { final String title; final Color primary; final VoidCallback onLogout; final VoidCallback? onCamera; const GlassAppBar({ super.key, required this.title, required this.primary, required this.onLogout, this.onCamera, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(12, 6, 12, 12), child: GlassCard( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ Icon(Icons.inventory_2_rounded, color: primary), const SizedBox(width: 10), Text( title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: Colors.blue.shade700, ), ), const Spacer(), if (onCamera != null) IconButton( icon: const Icon(Icons.camera_alt_rounded), onPressed: onCamera, ), IconButton( icon: const Icon(Icons.menu_rounded), onPressed: () => Scaffold.of(context).openDrawer(), ), IconButton( icon: const Icon(Icons.logout_rounded), onPressed: onLogout, ), ], ), ), ); } } // ------------------------------------------------------------- // FANCY DRAWER (modern glass drawer) // ------------------------------------------------------------- class FancyDrawer extends StatelessWidget { final Color primary; final VoidCallback onLogout; final VoidCallback? onScanBureau; const FancyDrawer({ super.key, required this.primary, required this.onLogout, this.onScanBureau, }); @override Widget build(BuildContext context) { final dark = Theme.of(context).brightness == Brightness.dark; final userController = Get.find(); final user = userController.user.value; return Drawer( backgroundColor: dark ? Colors.black.withOpacity(.9) : Colors.white.withOpacity(.9), child: Column( children: [ DrawerHeader( margin: EdgeInsets.zero, decoration: BoxDecoration( gradient: LinearGradient( colors: [primary.withOpacity(.9), primary.withOpacity(.6)], ), ), child: Row( children: [ const CircleAvatar( radius: 34, backgroundImage: NetworkImage( 'https://i.pravatar.cc/150?img=3', ), ), const SizedBox(width: 12), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user?["fullName"] ?? "Utilisateur", style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), SizedBox(height: 4), Text( user?["email"] ?? "email inconnu", style: TextStyle(color: Colors.white70), ), ], ), ], ), ), // ---- MENU ---- _drawerTile( icon: Icons.home_rounded, label: "Accueil", onTap: () => Navigator.pop(context), ), user?["group"]?["code"] == 'INV_OFF' ? SizedBox() : _drawerTile( icon: Icons.add_box_rounded, label: "Créer un produit", onTap: () => Get.to(() => const CreateProductPage()), ), _drawerTile( icon: Icons.list_alt, label: "Mes scans", onTap: () => Get.to(() => MyScans()), ), _drawerTile( icon: Icons.business_center_rounded, label: "Articles par Bureau", onTap: () { Navigator.pop(context); onScanBureau?.call(); }, ), _drawerTile( icon: Icons.report_problem_outlined, label: "Signaler un problème", onTap: () async => _launchTicketUrl(), ), const Spacer(), // ---- LOGOUT ---- _drawerTile( icon: Icons.logout_rounded, label: "Déconnexion", onTap: onLogout, ), const SizedBox(height: 12), ], ), ); } Widget _drawerTile({ required IconData icon, required String label, required VoidCallback onTap, }) { return ListTile(leading: Icon(icon), title: Text(label), onTap: onTap); } Future _launchTicketUrl() async { final url = Uri.parse(Utils.ticketUrl); if (!await launchUrl(url)) { throw Exception("Cannot open ticket system"); } } }