// widgets/finance_partner_drawer.dart — 财务屏往来流水抽屉(镜像原型 finance.html // openPartner):应收/应付/净额 3 行 + 近期流水(类型徽章 + ±金额)+ 登记收款/付款。 import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../core/theme/app_chrome.g.dart'; import '../core/theme/app_dims.g.dart'; import '../core/theme/context_tokens.dart'; import '../models/finance.dart'; import '../providers/finance_provider.dart'; import 'ds/ds_atoms.dart'; import 'finance_entry_dialog.dart'; import 'write_guard.dart'; import '../core/theme/app_fonts.dart'; /// 该往来单位近期财务流水(抽屉内展示,最多 8 条)。 final _partnerFlowsProvider = FutureProvider.family, int>((ref, partnerId) async { final page = await ref .read(financeRepositoryProvider) .listRecords(partnerId: partnerId, pageSize: 8); return page.data; }); String _yuan(double v) => '¥${NumberFormat.decimalPattern().format( v == v.roundToDouble() ? v.round() : v, )}'; /// 净额带符号(原型 net-pos 前缀 +、负数自带 -)。 String signedYuan(double net) { if (net > 0) return '+${_yuan(net)}'; if (net < 0) return '-${_yuan(net.abs())}'; return _yuan(0); } /// 财务流水类型徽章(b-收款=ok / b-付款=danger / 应收=warn / 应付=info)。 DsBadge financeTypeBadge(FinanceRecord r) => DsBadge(r.typeLabel, tone: switch (r.type) { 'receipt' => DsBadgeTone.ok, 'payment' => DsBadgeTone.danger, 'receivable' => DsBadgeTone.warn, _ => DsBadgeTone.info, }); Future showFinancePartnerDrawer( BuildContext context, { required PartnerFinanceRow row, }) { return showGeneralDialog( context: context, useRootNavigator: false, barrierDismissible: true, barrierLabel: '关闭', barrierColor: AppChrome.scrim, transitionDuration: const Duration(milliseconds: 250), pageBuilder: (ctx, _, __) { final w = math.min(480.0, MediaQuery.of(ctx).size.width * 0.94); return Align( alignment: Alignment.centerRight, child: Material( color: ctx.tokens.surface, elevation: 16, child: SelectionArea( child: SizedBox( width: w, height: double.infinity, child: _FinancePartnerDrawer(row: row), ), ), ), ); }, transitionBuilder: (ctx, anim, _, child) => SlideTransition( position: Tween(begin: const Offset(1, 0), end: Offset.zero) .animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)), child: child, ), ); } class _FinancePartnerDrawer extends ConsumerWidget { final PartnerFinanceRow row; const _FinancePartnerDrawer({required this.row}); @override Widget build(BuildContext context, WidgetRef ref) { final t = context.tokens; final flows = row.partnerId != null ? ref.watch(_partnerFlowsProvider(row.partnerId!)) : const AsyncValue>.data([]); final net = row.net; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // .drawer-head Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.border))), child: Row(children: [ Expanded( child: Text(row.name, style: TextStyle( fontSize: AppDims.fsH2, fontWeight: FontWeight.w700, color: t.heading)), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: Icon(LucideIcons.x, size: 18, color: t.muted), tooltip: '关闭', ), ]), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _drow(t, '应收账款', _yuan(row.recv)), _drow(t, '应付账款', _yuan(row.pay)), _drow(t, '净额', signedYuan(net), color: net > 0 ? t.success : (net < 0 ? t.danger : null)), Padding( padding: const EdgeInsets.only(top: 18, bottom: 8), child: Text('近期流水', style: TextStyle( fontSize: AppDims.fsSm, fontWeight: FontWeight.w600, color: t.muted)), ), flows.when( loading: () => const Padding( padding: EdgeInsets.all(20), child: Center( child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))), ), error: (e, _) => Text('流水加载失败', style: TextStyle(color: t.muted, fontSize: AppDims.fsSm)), data: (list) => list.isEmpty ? Text('本期暂无流水记录', style: TextStyle( color: t.faint, fontSize: AppDims.fsBody)) : Column(children: [ for (final r in list) _flowRow(t, r), ]), ), ], ), ), ), // 底部:登记收款 / 登记付款 Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration(border: Border(top: BorderSide(color: t.border))), child: WriteGuard( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ DsButton('登记收款', small: true, onPressed: () { Navigator.of(context).pop(); showFinanceEntryDialog(context, type: 'receipt', partnerId: row.partnerId); }), const SizedBox(width: 10), DsButton('登记付款', small: true, onPressed: () { Navigator.of(context).pop(); showFinanceEntryDialog(context, type: 'payment', partnerId: row.partnerId); }), ], ), ), ), ], ); } Widget _drow(dynamic t, String label, String value, {Color? color}) => Container( padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.borderSubtle))), child: Row(children: [ Text(label, style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)), const Spacer(), Text(value, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, color: color ?? t.text)), ]), ); Widget _flowRow(dynamic t, FinanceRecord r) { final isIn = r.type == 'receipt' || r.type == 'receivable'; return Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.borderSubtle))), child: Row(children: [ Text((r.recordDate ?? '').split('T').first, style: TextStyle( fontSize: AppDims.fsSm, color: t.muted, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), const SizedBox(width: 8), financeTypeBadge(r), const Spacer(), Text('${isIn ? '+' : '-'}${_yuan(r.amount.abs())}', style: TextStyle( fontSize: AppDims.fsSm, fontWeight: FontWeight.w600, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, color: t.heading)), ]), ); } }