import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../models/inventory.dart'; import '../../models/warehouse.dart'; import '../../providers/inventory_provider.dart'; import '../../providers/warehouse_provider.dart'; class InventoryCheckScreen extends ConsumerStatefulWidget { const InventoryCheckScreen({super.key}); @override ConsumerState createState() => _InventoryCheckScreenState(); } class _InventoryCheckScreenState extends ConsumerState { Warehouse? _selectedWarehouse; String _checkType = '全盘'; bool _submitting = false; bool _loadingItems = false; // Editable check items built from real inventory final List<_CheckItem> _checkItems = []; String get _checkNo { final now = DateTime.now(); final date = '${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}'; return 'PD$date${_selectedWarehouse?.id.toString().padLeft(3, '0') ?? '001'}'; } @override void dispose() { for (final item in _checkItems) { item.dispose(); } super.dispose(); } Future _loadInventory(Warehouse wh) async { setState(() { _loadingItems = true; for (final item in _checkItems) { item.dispose(); } _checkItems.clear(); }); try { final result = await ref .read(inventoryRepositoryProvider) .listInventory(warehouseId: wh.id, pageSize: 200); final items = result.data .where((inv) => inv.productId != null) .map((inv) => _CheckItem(inventory: inv)) .toList(); if (mounted) { setState(() { _checkItems.addAll(items); _loadingItems = false; }); } } catch (e) { if (mounted) { setState(() => _loadingItems = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('加载库存失败:$e'), backgroundColor: AppTheme.danger), ); } } } Future _submit() async { if (_selectedWarehouse == null) { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('请先选择仓库'))); return; } setState(() => _submitting = true); try { final now = DateTime.now(); final dateStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; final items = _checkItems.map((item) { final actual = double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity; return { 'product_id': item.inventory.productId!, 'actual_qty': actual, 'remark': item.remarkCtrl.text.trim(), }; }).toList(); await ref.read(inventoryRepositoryProvider).createCheck({ 'check_no': _checkNo, 'warehouse_id': _selectedWarehouse!.id, 'check_date': dateStr, 'items': items, }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('盘点单已提交'), backgroundColor: AppTheme.success, ), ); context.go('/inventory'); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('提交失败:$e'), backgroundColor: AppTheme.danger), ); } } finally { if (mounted) setState(() => _submitting = false); } } int _getDiff(_CheckItem item) { final actual = double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity; return (actual - item.inventory.quantity).round(); } @override Widget build(BuildContext context) { final asyncWarehouses = ref.watch(warehouseListProvider); return Scaffold( backgroundColor: AppTheme.background, body: Column( children: [ // Header Container( height: 52, color: AppTheme.surface, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, size: 20), onPressed: () => context.go('/inventory'), ), const SizedBox(width: 8), const Text('库存盘点', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600)), const Spacer(), OutlinedButton( onPressed: () => context.go('/inventory'), child: const Text('取消'), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: (_submitting || _checkItems.isEmpty) ? null : _submit, icon: _submitting ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Icon(Icons.check_circle_outline, size: 16), label: const Text('提交盘点'), ), ], ), ), const Divider(height: 1), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ // Basic info Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('盘点基本信息', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)), const SizedBox(height: 16), Wrap( spacing: 16, runSpacing: 16, children: [ _InfoField( label: '盘点单号', child: InputDecorator( decoration: const InputDecoration(), child: Text( _checkNo, style: const TextStyle( fontFamily: 'monospace', fontSize: 13), ), ), ), _InfoField( label: '盘点仓库', child: asyncWarehouses.when( loading: () => const LinearProgressIndicator(), error: (e, _) => Text('$e', style: const TextStyle( color: AppTheme.danger, fontSize: 12)), data: (warehouses) => DropdownButtonFormField( value: _selectedWarehouse, hint: const Text('请选择仓库'), items: warehouses .map((w) => DropdownMenuItem( value: w, child: Text(w.name, style: const TextStyle( fontSize: 13)))) .toList(), onChanged: (w) { setState( () => _selectedWarehouse = w); if (w != null) _loadInventory(w); }, decoration: const InputDecoration(), ), ), ), _InfoField( label: '盘点类型', child: DropdownButtonFormField( value: _checkType, items: ['全盘', '抽盘', '循环盘点'] .map((s) => DropdownMenuItem( value: s, child: Text(s, style: const TextStyle( fontSize: 13)))) .toList(), onChanged: (v) => setState(() => _checkType = v!), decoration: const InputDecoration(), ), ), _InfoField( label: '盘点日期', child: InputDecorator( decoration: const InputDecoration(), child: Builder(builder: (ctx) { final now = DateTime.now(); return Text( '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 13), ); }), ), ), ], ), ], ), ), ), const SizedBox(height: 12), // Check items table Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('盘点明细', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)), const SizedBox(width: 12), if (_checkItems.isNotEmpty) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppTheme.accent.withOpacity(0.1), borderRadius: BorderRadius.circular(3), ), child: Text( '差异 ${_checkItems.where((i) => _getDiff(i) != 0).length} 项', style: const TextStyle( fontSize: 12, color: AppTheme.accent), ), ), ], ), const SizedBox(height: 12), if (_selectedWarehouse == null) const Center( child: Padding( padding: EdgeInsets.all(32), child: Text('请先选择盘点仓库', style: TextStyle( color: AppTheme.textSecondary)), ), ) else if (_loadingItems) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) else if (_checkItems.isEmpty) const Center( child: Padding( padding: EdgeInsets.all(32), child: Text('该仓库暂无库存记录', style: TextStyle( color: AppTheme.textSecondary)), ), ) else Table( columnWidths: const { 0: FixedColumnWidth(36), 1: FixedColumnWidth(80), 2: FlexColumnWidth(3), 3: FixedColumnWidth(50), 4: FixedColumnWidth(80), 5: FixedColumnWidth(120), 6: FixedColumnWidth(80), 7: FlexColumnWidth(2), }, children: [ TableRow( decoration: const BoxDecoration( color: Color(0xFFF0F4FF)), children: [ '序号', '商品编码', '商品名称', '单位', '账面数量', '实际数量', '差异', '备注', ] .map((h) => Padding( padding: const EdgeInsets .symmetric( horizontal: 8, vertical: 10), child: Text(h, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme .primaryDark)), )) .toList(), ), ...List.generate( _checkItems.length, (i) => _buildCheckRow(i)), ], ), if (_checkItems.isNotEmpty) ...[ const Divider(height: 1), Padding( padding: const EdgeInsets.only(top: 12), child: Row( children: [ _SummaryItem( label: '盘点商品', value: '${_checkItems.length}种', color: AppTheme.primary, ), const SizedBox(width: 24), _SummaryItem( label: '盘盈', value: '${_checkItems.where((i) => _getDiff(i) > 0).length}种', color: AppTheme.success, ), const SizedBox(width: 24), _SummaryItem( label: '盘亏', value: '${_checkItems.where((i) => _getDiff(i) < 0).length}种', color: AppTheme.danger, ), const SizedBox(width: 24), _SummaryItem( label: '相符', value: '${_checkItems.where((i) => _getDiff(i) == 0).length}种', color: AppTheme.textSecondary, ), ], ), ), ], ], ), ), ), ], ), ), ), ], ), ); } TableRow _buildCheckRow(int index) { final item = _checkItems[index]; final diff = _getDiff(item); final inv = item.inventory; return TableRow( decoration: BoxDecoration( color: diff != 0 ? (diff > 0 ? AppTheme.success.withOpacity(0.04) : AppTheme.danger.withOpacity(0.04)) : (index.isEven ? Colors.white : const Color(0xFFFAFAFA)), ), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text('${index + 1}', style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text(inv.productCode.isEmpty ? '-' : inv.productCode, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text(inv.productName.isEmpty ? '-' : inv.productName, style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text(inv.unit.isEmpty ? '-' : inv.unit, style: const TextStyle(fontSize: 13)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text(inv.quantity.toStringAsFixed(0), style: const TextStyle(fontSize: 13)), ), Padding( padding: const EdgeInsets.all(4), child: TextFormField( controller: item.actualQtyCtrl, decoration: const InputDecoration(), style: const TextStyle(fontSize: 13), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => setState(() {}), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text( diff == 0 ? '0' : (diff > 0 ? '+$diff' : '$diff'), style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: diff == 0 ? AppTheme.textSecondary : (diff > 0 ? AppTheme.success : AppTheme.danger), ), ), ), Padding( padding: const EdgeInsets.all(4), child: TextFormField( controller: item.remarkCtrl, decoration: const InputDecoration(hintText: '备注'), style: const TextStyle(fontSize: 13), ), ), ], ); } } class _CheckItem { final Inventory inventory; final TextEditingController actualQtyCtrl; final TextEditingController remarkCtrl; _CheckItem({required this.inventory}) : actualQtyCtrl = TextEditingController( text: inventory.quantity.toStringAsFixed(0)), remarkCtrl = TextEditingController(); void dispose() { actualQtyCtrl.dispose(); remarkCtrl.dispose(); } } class _InfoField extends StatelessWidget { final String label; final Widget child; const _InfoField({required this.label, required this.child}); @override Widget build(BuildContext context) { return SizedBox( width: 220, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary)), const SizedBox(height: 6), child, ], ), ); } } class _SummaryItem extends StatelessWidget { final String label; final String value; final Color color; const _SummaryItem( {required this.label, required this.value, required this.color}); @override Widget build(BuildContext context) { return Row( children: [ Text(label, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary)), const SizedBox(width: 4), Text(value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: color)), ], ); } }