2025-10-04 17:17:53 +03:30
import ' package:flutter/material.dart ' ;
import ' package:hesabix_ui/l10n/app_localizations.dart ' ;
import ' ../../core/auth_store.dart ' ;
import ' ../../widgets/permission/access_denied_page.dart ' ;
2025-11-09 08:46:37 +03:30
// import '../../core/api_client.dart'; // duplicate removed
import ' ../../services/wallet_service.dart ' ;
import ' ../../widgets/invoice/bank_account_combobox_widget.dart ' ;
import ' package:hesabix_ui/utils/number_formatters.dart ' show formatWithThousands ;
import ' package:go_router/go_router.dart ' ;
import ' ../../core/api_client.dart ' ;
import ' ../../services/payment_gateway_service.dart ' ;
import ' package:url_launcher/url_launcher.dart ' ;
2025-11-09 22:07:27 +03:30
import ' package:url_launcher/url_launcher_string.dart ' ;
import ' package:url_launcher/link.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:flutter/foundation.dart ' show kIsWeb ;
import ' package:dio/dio.dart ' ;
import ' package:hesabix_ui/widgets/data_table/data_table_widget.dart ' ;
import ' package:hesabix_ui/widgets/data_table/data_table_config.dart ' ;
import ' ../../core/calendar_controller.dart ' ;
2025-10-04 17:17:53 +03:30
class WalletPage extends StatefulWidget {
final int businessId ;
final AuthStore authStore ;
const WalletPage ( {
super . key ,
required this . businessId ,
required this . authStore ,
} ) ;
@ override
State < WalletPage > createState ( ) = > _WalletPageState ( ) ;
}
class _WalletPageState extends State < WalletPage > {
2025-11-09 08:46:37 +03:30
late final WalletService _service ;
bool _loading = true ;
Map < String , dynamic > ? _overview ;
String ? _error ;
Map < String , dynamic > ? _metrics ;
DateTime ? _fromDate ;
DateTime ? _toDate ;
2025-11-09 22:07:27 +03:30
CalendarController ? _calendarCtrl ;
String _typeLabel ( String ? t ) {
switch ( ( t ? ? ' ' ) . toLowerCase ( ) ) {
case ' top_up ' :
return ' افزایش اعتبار ' ;
case ' customer_payment ' :
return ' پرداخت مشتری ' ;
case ' payout_request ' :
return ' درخواست تسویه ' ;
case ' payout_settlement ' :
return ' تسویه ' ;
case ' refund ' :
return ' استرداد ' ;
case ' fee ' :
return ' کارمزد ' ;
default :
return t ? ? ' نامشخص ' ;
}
}
String _statusLabel ( String ? s ) {
switch ( ( s ? ? ' ' ) . toLowerCase ( ) ) {
case ' pending ' :
return ' در انتظار ' ;
case ' approved ' :
return ' تایید شده ' ;
case ' processing ' :
return ' در حال پردازش ' ;
case ' succeeded ' :
return ' موفق ' ;
case ' failed ' :
return ' ناموفق ' ;
case ' canceled ' :
return ' لغو شده ' ;
default :
return s ? ? ' نامشخص ' ;
}
}
// آیکون نوع تراکنش دیگر استفاده نمیشود (نمایش در جدول)
2025-11-09 08:46:37 +03:30
@ override
void initState ( ) {
super . initState ( ) ;
_service = WalletService ( ApiClient ( ) ) ;
2025-11-09 22:07:27 +03:30
// بارگذاری کنترلر تقویم برای پشتیبانی از جلالی/میلادی در فیلترهای جدول
CalendarController . load ( ) . then ( ( c ) {
if ( mounted ) setState ( ( ) = > _calendarCtrl = c ) ;
} ) ;
2025-11-09 08:46:37 +03:30
_load ( ) ;
}
Future < void > _load ( ) async {
setState ( ( ) {
_loading = true ;
_error = null ;
} ) ;
try {
final res = await _service . getOverview ( businessId: widget . businessId ) ;
final now = DateTime . now ( ) ;
_toDate = now ;
_fromDate = now . subtract ( const Duration ( days: 30 ) ) ;
final m = await _service . getMetrics ( businessId: widget . businessId , fromDate: _fromDate , toDate: _toDate ) ;
setState ( ( ) {
_overview = res ;
_metrics = m ;
} ) ;
} catch ( e ) {
setState ( ( ) {
_error = ' $ e ' ;
} ) ;
} finally {
setState ( ( ) {
_loading = false ;
} ) ;
}
}
Future < void > _openPayoutDialog ( ) async {
final t = AppLocalizations . of ( context ) ;
final formKey = GlobalKey < FormState > ( ) ;
int ? bankId ;
final amountCtrl = TextEditingController ( ) ;
final descCtrl = TextEditingController ( ) ;
final result = await showDialog < bool > (
context: context ,
builder: ( ctx ) {
return AlertDialog (
title: Text ( ' درخواست تسویه ' ) ,
content: Form (
key: formKey ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
BankAccountComboboxWidget (
businessId: widget . businessId ,
selectedAccountId: bankId ? . toString ( ) ,
onChanged: ( opt ) = > bankId = int . tryParse ( opt ? . id ? ? ' ' ) ,
hintText: ' انتخاب حساب بانکی ' ,
) ,
const SizedBox ( height: 12 ) ,
TextFormField (
controller: amountCtrl ,
decoration: const InputDecoration ( labelText: ' مبلغ ' ) ,
keyboardType: TextInputType . number ,
validator: ( v ) = > ( v = = null | | v . isEmpty ) ? ' الزامی ' : null ,
) ,
const SizedBox ( height: 8 ) ,
TextFormField (
controller: descCtrl ,
decoration: const InputDecoration ( labelText: ' توضیحات (اختیاری) ' ) ,
) ,
] ,
) ,
) ,
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( ctx , false ) , child: Text ( t . cancel ) ) ,
FilledButton (
onPressed: ( ) {
if ( formKey . currentState ? . validate ( ) = = true & & bankId ! = null ) {
Navigator . pop ( ctx , true ) ;
}
} ,
child: Text ( t . confirm ) ,
) ,
] ,
) ;
} ,
) ;
if ( result = = true & & bankId ! = null ) {
try {
final amount = double . tryParse ( amountCtrl . text . replaceAll ( ' , ' , ' ' ) ) ? ? 0 ;
await _service . requestPayout (
businessId: widget . businessId ,
bankAccountId: bankId ! ,
amount: amount ,
description: descCtrl . text ,
) ;
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' درخواست تسویه ثبت شد ' ) ) ) ;
}
await _load ( ) ;
} catch ( e ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( ' خطا: $ e ' ) ) ) ;
}
}
}
}
Future < void > _openTopUpDialog ( ) async {
final t = AppLocalizations . of ( context ) ;
final formKey = GlobalKey < FormState > ( ) ;
final amountCtrl = TextEditingController ( ) ;
final descCtrl = TextEditingController ( ) ;
final pgService = PaymentGatewayService ( ApiClient ( ) ) ;
List < Map < String , dynamic > > gateways = const < Map < String , dynamic > > [ ] ;
int ? gatewayId ;
try {
gateways = await pgService . listBusinessGateways ( widget . businessId ) ;
if ( gateways . isNotEmpty ) {
gatewayId = int . tryParse ( ' ${ gateways . first [ ' id ' ] } ' ) ;
}
} catch ( _ ) { }
final result = await showDialog < bool > (
context: context ,
builder: ( ctx ) {
return AlertDialog (
title: const Text ( ' افزایش اعتبار ' ) ,
content: Form (
key: formKey ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
TextFormField (
controller: amountCtrl ,
decoration: const InputDecoration ( labelText: ' مبلغ ' ) ,
keyboardType: TextInputType . number ,
validator: ( v ) = > ( v = = null | | v . isEmpty ) ? ' الزامی ' : null ,
) ,
const SizedBox ( height: 8 ) ,
TextFormField (
controller: descCtrl ,
decoration: const InputDecoration ( labelText: ' توضیحات (اختیاری) ' ) ,
) ,
const SizedBox ( height: 8 ) ,
if ( gateways . isNotEmpty )
DropdownButtonFormField < int > (
value: gatewayId ,
decoration: const InputDecoration ( labelText: ' درگاه پرداخت ' ) ,
items: gateways
. map ( ( g ) = > DropdownMenuItem < int > (
value: int . tryParse ( ' ${ g [ ' id ' ] } ' ) ,
child: Text ( ' ${ g [ ' display_name ' ] } ( ${ g [ ' provider ' ] } ) ' ) ,
) )
. toList ( ) ,
onChanged: ( v ) = > gatewayId = v ,
) ,
] ,
) ,
) ,
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( ctx , false ) , child: Text ( t . cancel ) ) ,
FilledButton (
onPressed: ( ) {
if ( formKey . currentState ? . validate ( ) = = true & & ( gateways . isEmpty | | gatewayId ! = null ) ) {
Navigator . pop ( ctx , true ) ;
}
} ,
child: Text ( t . confirm ) ,
) ,
] ,
) ;
} ,
) ;
if ( result = = true ) {
try {
final amount = double . tryParse ( amountCtrl . text . replaceAll ( ' , ' , ' ' ) ) ? ? 0 ;
2025-11-09 22:07:27 +03:30
_showLoadingDialog ( ' در حال ثبت درخواست و آمادهسازی درگاه... ' ) ;
2025-11-09 08:46:37 +03:30
final data = await _service . topUp (
businessId: widget . businessId ,
amount: amount ,
description: descCtrl . text ,
gatewayId: gatewayId ,
) ;
2025-11-09 22:07:27 +03:30
if ( mounted ) Navigator . of ( context ) . pop ( ) ; // close loading
2025-11-09 08:46:37 +03:30
final paymentUrl = ( data [ ' payment_url ' ] ? ? ' ' ) . toString ( ) ;
if ( paymentUrl . isNotEmpty ) {
2025-11-09 22:07:27 +03:30
_showLoadingDialog ( ' در حال انتقال به درگاه پرداخت... ' ) ;
await _openPaymentUrlWithFallback ( paymentUrl ) ;
if ( mounted ) Navigator . of ( context ) . pop ( ) ; // close loading
2025-11-09 08:46:37 +03:30
} else {
if ( mounted ) {
2025-11-09 22:07:27 +03:30
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' درخواست افزایش اعتبار ثبت شد، اما لینک پرداخت دریافت نشد. لطفاً بعداً دوباره تلاش کنید یا تنظیمات درگاه را بررسی کنید. ' ) ) ) ;
2025-11-09 08:46:37 +03:30
}
}
await _load ( ) ;
} catch ( e ) {
if ( mounted ) {
2025-11-09 22:07:27 +03:30
// ensure loading dialog is closed if open
Navigator . of ( context , rootNavigator: true ) . maybePop ( ) ;
}
String friendly = ' خطا در ثبت درخواست افزایش اعتبار ' ;
if ( e is DioException ) {
final status = e . response ? . statusCode ;
final body = e . response ? . data ;
final serverMsg = ( body is Map & & body [ ' message ' ] is String ) ? ( body [ ' message ' ] as String ) : null ;
final errorCode = ( body is Map & & body [ ' error_code ' ] is String ) ? ( body [ ' error_code ' ] as String ) : null ;
if ( errorCode = = ' GATEWAY_INIT_FAILED ' ) {
friendly = ' خطا در اتصال به درگاه. لطفاً تنظیمات درگاه را بررسی کنید یا بعداً تلاش کنید. ' ;
} else if ( errorCode = = ' INVALID_CONFIG ' ) {
friendly = ' پیکربندی درگاه ناقص است. لطفاً مرچنت آیدی و آدرس بازگشت را بررسی کنید. ' ;
} else if ( errorCode = = ' GATEWAY_DISABLED ' ) {
friendly = ' این درگاه غیرفعال است. ' ;
} else if ( errorCode = = ' GATEWAY_NOT_FOUND ' ) {
friendly = ' درگاه پرداخت یافت نشد. ' ;
} else if ( status ! = null & & status > = 500 ) {
friendly = ' خطای سرور هنگام اتصال به درگاه. لطفاً بعداً تلاش کنید. ' ;
} else if ( serverMsg ! = null & & serverMsg . isNotEmpty ) {
friendly = serverMsg ;
}
}
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar ( content: Text ( friendly ) ) ) ;
2025-11-09 08:46:37 +03:30
}
}
}
}
2025-11-09 22:07:27 +03:30
Future < void > _openPaymentUrlWithFallback ( String url ) async {
// وب: تلاش برای باز کردن مستقیم؛ در صورت عدم موفقیت، دیالوگ جایگزین
if ( kIsWeb ) {
try {
final launched = await launchUrl ( Uri . parse ( url ) , mode: LaunchMode . externalApplication ) ;
if ( ! launched ) {
await _showOpenLinkDialog ( url ) ;
}
} catch ( _ ) {
await _showOpenLinkDialog ( url ) ;
}
return ;
}
// دسکتاپ/موبایل: باز کردن در مرورگر پیشفرض با Fallback
try {
final launched = await launchUrl ( Uri . parse ( url ) , mode: LaunchMode . externalApplication ) ;
if ( ! launched ) {
await _showOpenLinkDialog ( url ) ;
}
} catch ( _ ) {
await _showOpenLinkDialog ( url ) ;
2025-11-09 08:46:37 +03:30
}
}
2025-11-09 22:07:27 +03:30
void _showLoadingDialog ( String message ) {
showDialog < void > (
2025-11-09 08:46:37 +03:30
context: context ,
2025-11-09 22:07:27 +03:30
barrierDismissible: false ,
builder: ( _ ) = > WillPopScope (
onWillPop: ( ) async = > false ,
child: AlertDialog (
content: Row (
children: [
const SizedBox ( width: 24 , height: 24 , child: CircularProgressIndicator ( strokeWidth: 2 ) ) ,
const SizedBox ( width: 12 ) ,
Expanded ( child: Text ( message ) ) ,
] ,
) ,
) ,
) ,
2025-11-09 08:46:37 +03:30
) ;
}
2025-11-09 22:07:27 +03:30
Future < void > _showOpenLinkDialog ( String url ) async {
if ( ! mounted ) return ;
await showDialog < void > (
context: context ,
builder: ( ctx ) = > AlertDialog (
title: const Text ( ' انتقال به درگاه پرداخت ' ) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const Text ( ' برای ادامه پرداخت، لینک زیر را باز کنید: ' ) ,
const SizedBox ( height: 8 ) ,
SelectableText ( url ) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) async {
await Clipboard . setData ( ClipboardData ( text: url ) ) ;
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( ' لینک کپی شد ' ) ) ) ;
}
} ,
child: const Text ( ' کپی لینک ' ) ,
) ,
if ( kIsWeb )
Link (
uri: Uri . parse ( url ) ,
target: LinkTarget . blank ,
builder: ( context , followLink ) = > FilledButton (
onPressed: ( ) {
followLink ? . call ( ) ;
Navigator . of ( ctx ) . pop ( ) ;
} ,
child: const Text ( ' باز کردن ' ) ,
) ,
)
else
FilledButton (
onPressed: ( ) async {
try {
await launchUrl ( Uri . parse ( url ) , mode: LaunchMode . externalApplication ) ;
} finally {
if ( mounted ) Navigator . of ( ctx ) . pop ( ) ;
}
} ,
child: const Text ( ' باز کردن ' ) ,
) ,
] ,
) ,
) ;
2025-11-09 08:46:37 +03:30
}
2025-11-09 22:07:27 +03:30
// فیلتر بازه تاریخ اکنون توسط DataTableWidget و Dialog داخلی آن انجام میشود
// بارگذاری بیشتر جایگزین با جدول سراسری شده است
2025-10-04 17:17:53 +03:30
@ override
Widget build ( BuildContext context ) {
final t = AppLocalizations . of ( context ) ;
if ( ! widget . authStore . canReadSection ( ' wallet ' ) ) {
return AccessDeniedPage ( message: t . accessDenied ) ;
}
2025-11-09 08:46:37 +03:30
final theme = Theme . of ( context ) ;
final overview = _overview ;
final currency = overview ? [ ' base_currency_code ' ] ? ? ' IRR ' ;
2025-10-04 17:17:53 +03:30
return Scaffold (
2025-11-09 08:46:37 +03:30
appBar: AppBar ( title: Text ( t . wallet ) ) ,
body: _loading
? const Center ( child: CircularProgressIndicator ( ) )
: _error ! = null
? Center ( child: Text ( _error ! ) )
2025-11-09 22:07:27 +03:30
: LayoutBuilder (
builder: ( context , constraints ) {
final double tableHeight = ( constraints . maxHeight - 360 ) . clamp ( 280.0 , constraints . maxHeight ) ;
return SingleChildScrollView (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
2025-11-09 08:46:37 +03:30
children: [
2025-11-09 22:07:27 +03:30
Row (
children: [
Icon ( Icons . wallet , size: 32 , color: theme . colorScheme . primary ) ,
const SizedBox ( width: 12 ) ,
Text ( ' کیفپول کسبوکار ' , style: theme . textTheme . titleLarge ) ,
const Spacer ( ) ,
Chip ( label: Text ( currency ) ) ,
] ,
) ,
const SizedBox ( height: 12 ) ,
Row (
children: [
Expanded (
child: Card (
child: Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( ' مانده قابل برداشت ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
Text ( ' ${ formatWithThousands ( ( overview ? [ ' available_balance ' ] ? ? 0 ) is num ? ( overview ? [ ' available_balance ' ] ? ? 0 ) : double . tryParse ( ' $ {overview?[ ' available_balance ' ] } ' ) ? ? 0 ) } ' , style: theme.textTheme.headlineSmall),
] ,
) ,
) ,
2025-11-09 08:46:37 +03:30
) ,
) ,
2025-11-09 22:07:27 +03:30
const SizedBox ( width: 12 ) ,
Expanded (
child: Card (
child: Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text ( ' مانده در انتظار تایید ' , style: theme . textTheme . labelLarge ) ,
const SizedBox ( height: 8 ) ,
Text ( ' ${ formatWithThousands ( ( overview ? [ ' pending_balance ' ] ? ? 0 ) is num ? ( overview ? [ ' pending_balance ' ] ? ? 0 ) : double . tryParse ( ' $ {overview?[ ' pending_balance ' ] } ' ) ? ? 0 ) } ' , style: theme.textTheme.headlineSmall),
] ,
) ,
) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 16 ) ,
Row (
children: [
const Spacer ( ) ,
FilledButton . icon (
onPressed: _openPayoutDialog ,
icon: const Icon ( Icons . account_balance ) ,
label: const Text ( ' درخواست تسویه ' ) ,
) ,
const SizedBox ( width: 8 ) ,
OutlinedButton . icon (
onPressed: _openTopUpDialog ,
icon: const Icon ( Icons . add ) ,
label: const Text ( ' افزایش اعتبار ' ) ,
) ,
] ,
2025-11-09 08:46:37 +03:30
) ,
2025-11-09 22:07:27 +03:30
const SizedBox ( height: 16 ) ,
if ( _metrics ! = null )
Card (
2025-11-09 08:46:37 +03:30
child: Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
2025-11-09 22:07:27 +03:30
Text ( ' گزارش ۳۰ روز اخیر ' , style: theme . textTheme . titleMedium ) ,
2025-11-09 08:46:37 +03:30
const SizedBox ( height: 8 ) ,
2025-11-09 22:07:27 +03:30
Wrap (
spacing: 12 ,
runSpacing: 8 ,
children: [
Chip ( label: Text ( ' ورودی ناخالص: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' gross_in ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' gross_in ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' gross_in ' ] } ' ) ? ? 0 ) } ' )),
Chip ( label: Text ( ' کارمزد ورودی: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' fees_in ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' fees_in ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' fees_in ' ] } ' ) ? ? 0 ) } ' )),
Chip ( label: Text ( ' ورودی خالص: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' net_in ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' net_in ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' net_in ' ] } ' ) ? ? 0 ) } ' )),
Chip ( label: Text ( ' خروجی ناخالص: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' gross_out ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' gross_out ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' gross_out ' ] } ' ) ? ? 0 ) } ' )),
Chip ( label: Text ( ' کارمزد خروجی: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' fees_out ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' fees_out ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' fees_out ' ] } ' ) ? ? 0 ) } ' )),
Chip ( label: Text ( ' خروجی خالص: ${ formatWithThousands ( ( _metrics ? [ ' totals ' ] ? [ ' net_out ' ] ? ? 0 ) is num ? ( _metrics ? [ ' totals ' ] ? [ ' net_out ' ] ? ? 0 ) : double . tryParse ( ' $ {_metrics?[ ' totals ' ]?[ ' net_out ' ] } ' ) ? ? 0 ) } ' )),
] ,
) ,
2025-11-09 08:46:37 +03:30
] ,
) ,
) ,
) ,
2025-11-09 22:07:27 +03:30
const SizedBox ( height: 16 ) ,
// دکمههای خروجی CSV در پایین جدول موجود هستند؛ این بخش حذف شد تا فیلتر تاریخ از طریق جدول انجام شود
const SizedBox ( height: 16 ) ,
Text ( ' تراکنشهای اخیر ' , style: theme . textTheme . titleMedium ) ,
const SizedBox ( height: 8 ) ,
SizedBox (
height: tableHeight ,
child: DataTableWidget < Map < String , dynamic > > (
config: DataTableConfig < Map < String , dynamic > > (
endpoint: ' /businesses/ ${ widget . businessId } /wallet/transactions/table ' ,
title: ' تراکنشها ' ,
showTableIcon: true ,
showSearch: false ,
showActiveFilters: false ,
showPagination: true ,
defaultPageSize: 20 ,
pageSizeOptions: const [ 10 , 20 , 50 , 100 ] ,
enableColumnSettings: true ,
columns: [
DateColumn ( ' created_at ' , ' تاریخ ' , filterType: ColumnFilterType . dateRange , formatter: ( it ) {
final v = ( it [ ' created_at ' ] ? ? ' ' ) . toString ( ) ;
return v . split ( ' T ' ) . first ;
} ) ,
TextColumn ( ' type ' , ' نوع ' , formatter: ( it ) = > _typeLabel ( ( it [ ' type ' ] ? ? ' ' ) . toString ( ) ) ) ,
TextColumn ( ' status ' , ' وضعیت ' , formatter: ( it ) = > _statusLabel ( ( it [ ' status ' ] ? ? ' ' ) . toString ( ) ) ) ,
TextColumn ( ' description ' , ' توضیحات ' , searchable: false , overflow: true , maxLines: 1 ) ,
NumberColumn ( ' amount ' , ' مبلغ ' , formatter: ( it ) {
final amount = it [ ' amount ' ] ;
final n = ( amount is num ) ? amount . toDouble ( ) : double . tryParse ( ' $ amount ' ) ? ? 0 ;
return formatWithThousands ( n ) ;
} ) ,
NumberColumn ( ' fee_amount ' , ' کارمزد ' , formatter: ( it ) {
final fee = it [ ' fee_amount ' ] ;
final n = ( fee is num ) ? fee . toDouble ( ) : double . tryParse ( ' $ fee ' ) ? ? 0 ;
return formatWithThousands ( n ) ;
} ) ,
TextColumn ( ' document_id ' , ' سند ' , formatter: ( it ) {
final d = it [ ' document_id ' ] ;
return ( d = = null | | ' $ d ' = = ' null ' ) ? ' - ' : ' $ d ' ;
} ) ,
] ,
onRowTap: ( row ) async {
final docId = row [ ' document_id ' ] ;
if ( docId = = null ) return ;
2025-11-09 08:46:37 +03:30
if ( ! mounted ) return ;
await context . pushNamed (
' business_documents ' ,
pathParameters: { ' business_id ' : widget . businessId . toString ( ) } ,
extra: { ' focus_document_id ' : docId } ,
) ;
} ,
2025-11-09 22:07:27 +03:30
showExportButtons: true ,
excelEndpoint: ' /businesses/ ${ widget . businessId } /wallet/transactions/export '
' ${ _fromDate ! = null ? ' ?from_date= $ {_fromDate!.toIso8601String() } ' : ' ' } '
' ${ _toDate ! = null ? ( _fromDate ! = null ? ' & ' : ' ? ' ) + ' to_date= $ {_toDate!.toIso8601String() } ' : ' ' } ' ,
) ,
fromJson: ( json ) = > Map < String , dynamic > . from ( json ) ,
calendarController: _calendarCtrl ,
) ,
2025-11-09 08:46:37 +03:30
) ,
2025-11-09 22:07:27 +03:30
] ,
2025-11-09 08:46:37 +03:30
) ,
2025-11-09 22:07:27 +03:30
) ;
} ,
2025-11-09 08:46:37 +03:30
) ,
2025-10-04 17:17:53 +03:30
) ;
}
}