import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:inventory_app/constants.dart'; import 'package:inventory_app/models/inventory_line.dart'; import 'package:inventory_app/service/axelor_client.dart'; import 'package:inventory_app/widgets/glass_widgets.dart'; class BureauInventoryPage extends StatefulWidget { final int locationId; final String locationName; const BureauInventoryPage({ super.key, required this.locationId, required this.locationName, }); @override State createState() => _BureauInventoryPageState(); } class _BureauInventoryPageState extends State { Future?>? _future; List _allLines = []; final TextEditingController _searchController = TextEditingController(); String _query = ''; @override void initState() { super.initState(); _searchController.addListener(() { setState(() => _query = _searchController.text.trim().toLowerCase()); }); _load(); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _load() async { final client = await AxelorClient.create(); setState(() { _future = client.fetchInventoryLinesByLocation(widget.locationId).then(( lines, ) { _allLines = lines ?? []; return lines; }); }); } List get _filteredLines { if (_query.isEmpty) return _allLines; return _allLines.where((line) { return line.productName.toLowerCase().contains(_query) || line.ticketId.toLowerCase().contains(_query) || (line.observation?.toLowerCase().contains(_query) ?? false) || (line.description?.toLowerCase().contains(_query) ?? false); }).toList(); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( body: Stack( children: [ const FancyBackground(), SafeArea( child: Column( children: [ BackAppBar( title: widget.locationName, icon: Icons.business_rounded, ), Expanded( child: FutureBuilder?>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError || snapshot.data == null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, color: Colors.redAccent, size: 48, ), const SizedBox(height: 12), Text( 'Impossible de charger les articles.', style: TextStyle(color: Colors.redAccent), ), const SizedBox(height: 12), ElevatedButton.icon( icon: const Icon(Icons.refresh), label: const Text('Réessayer'), onPressed: _load, ), ], ), ); } if (_allLines.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.inventory_2_outlined, size: 64, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Aucun article trouvé dans ce bureau.', style: TextStyle( fontSize: 16, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), ], ), ); } final filtered = _filteredLines; return Column( children: [ _SearchBar( controller: _searchController, isDark: isDark, total: _allLines.length, shown: filtered.length, ), Expanded( child: filtered.isEmpty ? Center( child: Text( 'Aucun résultat pour "${_searchController.text}"', style: TextStyle( fontSize: 15, color: Colors.grey.shade500, ), textAlign: TextAlign.center, ), ) : RefreshIndicator( onRefresh: _load, child: ListView.builder( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB( 16, 4, 16, 100, ), itemCount: filtered.length, itemBuilder: (context, index) { return _LineCard( line: filtered[index], isDark: isDark, onTap: () => _showDetails( context, filtered[index], ), ); }, ), ), ), ], ); }, ), ), ], ), ), ], ), ); } void _showDetails(BuildContext context, InventoryLine line) { final isDark = Theme.of(context).brightness == Brightness.dark; final textColor = isDark ? Colors.white : Colors.blue.shade700; showModalBottomSheet( context: context, backgroundColor: Colors.transparent, barrierColor: Colors.black.withValues(alpha: 0.3), isScrollControlled: true, builder: (_) { return Padding( padding: const EdgeInsets.all(12), child: GlassCard( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SheetHandle(Colors.blue), Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( line.productName, style: kTextFormFieldStyle( fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), ), const SizedBox(height: 12), _detail('N° Ticket', line.ticketId, textColor), if (line.description != null && line.description!.isNotEmpty) _detail('État', line.description!, textColor), if (line.observation != null && line.observation!.isNotEmpty) _detail('N° Série', line.observation!, textColor), if (line.firstCounting != null && line.firstCounting! > 0) _detail( 'Comptage 1', line.firstCounting!.toStringAsFixed(0), textColor, ), if (line.secondCounting != null && line.secondCounting! > 0) _detail( 'Comptage 2', line.secondCounting!.toStringAsFixed(0), textColor, ), if (line.thirdCounting != null && line.thirdCounting! > 0) _detail( 'Comptage 3', line.thirdCounting!.toStringAsFixed(0), textColor, ), if (line.firstCountingDate != null) _detail( 'Date C1', _formatDate(line.firstCountingDate!), textColor, ), if (line.firstCountingByUser != null) _detail( 'Agent C1', line.firstCountingByUser!['fullName'] ?? '-', textColor, ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon( Icons.close_rounded, color: Colors.white, ), label: const Text( 'Fermer', style: TextStyle(color: Colors.white), ), onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ), ], ), ), ), ); }, ); } Widget _detail(String label, String value, Color textColor) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 110, child: Text( label, style: kTextFormFieldStyle( fontWeight: FontWeight.w600, color: textColor, ), ), ), Expanded( child: Text( value, style: kTextFormFieldStyle( fontWeight: FontWeight.bold, color: textColor, ), ), ), ], ), ); } String _formatDate(String iso) { try { final dt = DateTime.parse(iso).toLocal(); return '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; } catch (_) { return iso; } } } // ── Search bar ─────────────────────────────────────────────────────────────── class _SearchBar extends StatelessWidget { final TextEditingController controller; final bool isDark; final int total; final int shown; const _SearchBar({ required this.controller, required this.isDark, required this.total, required this.shown, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: TextField( controller: controller, style: TextStyle(color: isDark ? Colors.white : Colors.black87), decoration: InputDecoration( hintText: 'Rechercher par nom, ticket, état...', hintStyle: TextStyle( color: isDark ? Colors.white38 : Colors.black38, fontSize: 14, ), prefixIcon: Icon( Icons.search_rounded, color: isDark ? Colors.white54 : Colors.black45, ), suffixIcon: controller.text.isNotEmpty ? IconButton( icon: Icon( Icons.close_rounded, color: isDark ? Colors.white54 : Colors.black45, ), onPressed: () => controller.clear(), ) : Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( '$total articles', style: TextStyle( fontSize: 12, color: isDark ? Colors.white38 : Colors.black38, ), ), ), filled: true, fillColor: isDark ? Colors.white.withValues(alpha: 0.08) : Colors.white.withValues(alpha: 0.75), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(vertical: 12), ), ), ); } } // ── Card for each inventory line ───────────────────────────────────────────── class _LineCard extends StatelessWidget { final InventoryLine line; final bool isDark; final VoidCallback onTap; const _LineCard({ required this.line, required this.isDark, required this.onTap, }); @override Widget build(BuildContext context) { final textColor = isDark ? Colors.white : Colors.blue.shade700; final subColor = isDark ? Colors.white70 : Colors.blue.shade400; final qty = (line.thirdCounting ?? 0) > 0 ? line.thirdCounting! : (line.secondCounting ?? 0) > 0 ? line.secondCounting! : (line.firstCounting ?? 0); return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: GlassCard( child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () { HapticFeedback.selectionClick(); onTap(); }, child: Padding( padding: const EdgeInsets.all(5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: const Icon( Icons.inventory_2_rounded, color: Colors.blue, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( line.productName, style: kTextFormFieldStyle( fontWeight: FontWeight.bold, color: textColor, ), ), const SizedBox(height: 4), if (line.ticketId.isNotEmpty) Text( 'Ticket: ${line.ticketId}', style: kTextFormFieldStyle( fontSize: 12.0, color: subColor, ), ), if (line.description != null && line.description!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( 'État: ${line.description}', style: kTextFormFieldStyle( fontSize: 12.0, color: subColor, ), ), ), if (line.observation != null && line.observation!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( 'N° Série: ${line.observation}', style: kTextFormFieldStyle( fontSize: 12.0, color: subColor, ), ), ), const SizedBox(height: 6), Wrap( spacing: 6, children: [ if ((line.firstCounting ?? 0) > 0) _CountChip( label: 'C1: ${line.firstCounting!.toStringAsFixed(0)}', ), if ((line.secondCounting ?? 0) > 0) _CountChip( label: 'C2: ${line.secondCounting!.toStringAsFixed(0)}', color: Colors.orange, ), if ((line.thirdCounting ?? 0) > 0) _CountChip( label: 'C3: ${line.thirdCounting!.toStringAsFixed(0)}', color: Colors.green, ), ], ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( qty.toStringAsFixed(0), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ), ], ), ), ), ), ); } } class _CountChip extends StatelessWidget { final String label; final Color color; const _CountChip({required this.label, this.color = Colors.blue}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withValues(alpha: 0.3)), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: color, ), ), ); } }