import 'package:lucide_icons_flutter/lucide_icons.dart'; 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/context_tokens.dart'; import '../../models/inventory.dart'; import '../../models/warehouse.dart'; import '../../core/config/app_constants.dart'; import '../../providers/inventory_provider.dart'; import '../../providers/warehouse_provider.dart'; import '../../core/theme/app_fonts.dart'; import '../../widgets/ds/ds_atoms.dart'; import '../../widgets/ds/ds_toast.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: AppConstants.inventoryCheckPageSize); 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); showDsToast(context, '加载库存失败:$e', bg: context.tokens.danger); } } } Future _submit() async { if (_selectedWarehouse == null) { showDsToast(context, '请先选择仓库'); 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) { showDsToast(context, '盘点单已提交', bg: context.tokens.success); context.go('/inventory'); } } catch (e) { if (mounted) { showDsToast(context, '提交失败:$e', bg: context.tokens.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: context.tokens.bg, body: Column( children: [ // Header Container( height: 52, color: context.tokens.surface, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ IconButton( icon: const Icon(LucideIcons.arrowLeft, 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)) // ds-ignore: 盘点屏待重建批次统一 : const Icon(LucideIcons.circleCheck, 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: [ Text('盘点基本信息', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: context.tokens.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: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: 13), ), ), ), _InfoField( label: '盘点仓库', child: asyncWarehouses.when( loading: () => const LinearProgressIndicator(), error: (e, _) => Text('$e', style: TextStyle( color: context.tokens.danger, fontSize: 12)), data: (warehouses) => DsSelect( value: _selectedWarehouse, hint: '请选择仓库', options: [ for (final w in warehouses) (w, w.name), ], onChanged: (w) { setState(() => _selectedWarehouse = w); _loadInventory(w); }, ), ), ), _InfoField( label: '盘点类型', child: DsSelect( value: _checkType, options: const [ ('全盘', '全盘'), ('抽盘', '抽盘'), ('循环盘点', '循环盘点'), ], onChanged: (v) => setState(() => _checkType = v), ), ), _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: [ Text('盘点明细', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: context.tokens.primaryDark)), const SizedBox(width: 12), if (_checkItems.isNotEmpty) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: context.tokens.accent.withOpacity(0.1), borderRadius: BorderRadius.circular(3), ), child: Text( '差异 ${_checkItems.where((i) => _getDiff(i) != 0).length} 项', style: TextStyle( fontSize: 12, color: context.tokens.accent), ), ), ], ), const SizedBox(height: 12), if (_selectedWarehouse == null) Center( child: Padding( padding: const EdgeInsets.all(32), child: Text('请先选择盘点仓库', style: TextStyle(color: context.tokens.muted)), ), ) else if (_loadingItems) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) else if (_checkItems.isEmpty) Center( child: Padding( padding: const EdgeInsets.all(32), child: Text('该仓库暂无库存记录', style: TextStyle(color: context.tokens.muted)), ), ) 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: BoxDecoration(color: context.tokens.thBg), children: [ '序号', '商品编码', '商品名称', '单位', '账面数量', '实际数量', '差异', '备注', ] .map((h) => Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 10), child: Text(h, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: context .tokens.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: context.tokens.primary, ), const SizedBox(width: 24), _SummaryItem( label: '盘盈', value: '${_checkItems.where((i) => _getDiff(i) > 0).length}种', color: context.tokens.success, ), const SizedBox(width: 24), _SummaryItem( label: '盘亏', value: '${_checkItems.where((i) => _getDiff(i) < 0).length}种', color: context.tokens.danger, ), const SizedBox(width: 24), _SummaryItem( label: '相符', value: '${_checkItems.where((i) => _getDiff(i) == 0).length}种', color: context.tokens.muted, ), ], ), ), ], ], ), ), ), ], ), ), ), ], ), ); } 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 ? context.tokens.success.withOpacity(0.04) : context.tokens.danger.withOpacity(0.04)) : (index.isEven ? context.tokens.surface : context.tokens.bg), ), children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text('${index + 1}', style: TextStyle(fontSize: 13, color: context.tokens.muted)), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Text(inv.productCode.isEmpty ? '-' : inv.productCode, style: TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontSize: 12, color: context.tokens.muted)), ), 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 ? context.tokens.muted : (diff > 0 ? context.tokens.success : context.tokens.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: TextStyle(fontSize: 13, color: context.tokens.muted)), 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: TextStyle(fontSize: 13, color: context.tokens.muted)), const SizedBox(width: 4), Text(value, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: color)), ], ); } }