372 lines
12 KiB
Dart
372 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
import '../../core/auth_store.dart';
|
|
import '../../core/calendar_controller.dart';
|
|
import '../../core/api_client.dart';
|
|
import '../../models/transfer_document.dart';
|
|
import '../../services/transfer_service.dart';
|
|
import '../../widgets/data_table/data_table_widget.dart';
|
|
import '../../widgets/data_table/data_table_config.dart';
|
|
import '../../core/date_utils.dart' show HesabixDateUtils;
|
|
import '../../utils/number_formatters.dart' show formatWithThousands;
|
|
import '../../widgets/date_input_field.dart';
|
|
import '../../widgets/transfer/transfer_form_dialog.dart';
|
|
import '../../widgets/transfer/transfer_details_dialog.dart';
|
|
|
|
class TransfersPage extends StatefulWidget {
|
|
final int businessId;
|
|
final AuthStore authStore;
|
|
final CalendarController calendarController;
|
|
final ApiClient apiClient;
|
|
|
|
const TransfersPage({
|
|
super.key,
|
|
required this.businessId,
|
|
required this.authStore,
|
|
required this.calendarController,
|
|
required this.apiClient,
|
|
});
|
|
|
|
@override
|
|
State<TransfersPage> createState() => _TransfersPageState();
|
|
}
|
|
|
|
class _TransfersPageState extends State<TransfersPage> {
|
|
// کنترل جدول برای دسترسی به refresh
|
|
final GlobalKey _tableKey = GlobalKey();
|
|
DateTime? _fromDate;
|
|
DateTime? _toDate;
|
|
|
|
void _refreshData() {
|
|
final state = _tableKey.currentState;
|
|
if (state != null) {
|
|
try {
|
|
// ignore: avoid_dynamic_calls
|
|
(state as dynamic).refresh();
|
|
return;
|
|
} catch (_) {}
|
|
}
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = AppLocalizations.of(context);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
body: SafeArea(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildHeader(t),
|
|
_buildFilters(t),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: DataTableWidget<TransferDocument>(
|
|
key: _tableKey,
|
|
config: _buildTableConfig(t),
|
|
fromJson: (json) => TransferDocument.fromJson(json),
|
|
calendarController: widget.calendarController,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(AppLocalizations t) {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
t.transfers,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'مدیریت اسناد انتقال وجه',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Tooltip(
|
|
message: 'افزودن انتقال جدید',
|
|
child: FilledButton.icon(
|
|
onPressed: _onAddNew,
|
|
icon: const Icon(Icons.add),
|
|
label: Text(t.add),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilters(AppLocalizations t) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: DateInputField(
|
|
value: _fromDate,
|
|
calendarController: widget.calendarController,
|
|
onChanged: (date) {
|
|
setState(() => _fromDate = date);
|
|
_refreshData();
|
|
},
|
|
labelText: 'از تاریخ',
|
|
hintText: 'انتخاب تاریخ شروع',
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: DateInputField(
|
|
value: _toDate,
|
|
calendarController: widget.calendarController,
|
|
onChanged: (date) {
|
|
setState(() => _toDate = date);
|
|
_refreshData();
|
|
},
|
|
labelText: 'تا تاریخ',
|
|
hintText: 'انتخاب تاریخ پایان',
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_fromDate = null;
|
|
_toDate = null;
|
|
});
|
|
_refreshData();
|
|
},
|
|
icon: const Icon(Icons.clear),
|
|
tooltip: 'پاک کردن فیلتر تاریخ',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
DataTableConfig<TransferDocument> _buildTableConfig(AppLocalizations t) {
|
|
return DataTableConfig<TransferDocument>(
|
|
endpoint: '/businesses/${widget.businessId}/transfers',
|
|
title: t.transfers,
|
|
excelEndpoint: '/businesses/${widget.businessId}/transfers/export/excel',
|
|
pdfEndpoint: '/businesses/${widget.businessId}/transfers/export/pdf',
|
|
businessId: widget.businessId,
|
|
reportModuleKey: 'transfers',
|
|
reportSubtype: 'list',
|
|
getExportParams: () => {
|
|
if (_fromDate != null) 'from_date': _fromDate!.toUtc().toIso8601String(),
|
|
if (_toDate != null) 'to_date': _toDate!.toUtc().toIso8601String(),
|
|
},
|
|
columns: [
|
|
TextColumn(
|
|
'code',
|
|
'کد سند',
|
|
width: ColumnWidth.medium,
|
|
formatter: (it) => it.code,
|
|
),
|
|
TextColumn(
|
|
'description',
|
|
'توضیحات',
|
|
width: ColumnWidth.large,
|
|
formatter: (it) => (it.description ?? '').isNotEmpty ? it.description! : _composeDesc(it),
|
|
),
|
|
TextColumn(
|
|
'source',
|
|
'مبدا',
|
|
width: ColumnWidth.large,
|
|
formatter: (it) => _composeSource(it),
|
|
),
|
|
TextColumn(
|
|
'destination',
|
|
'مقصد',
|
|
width: ColumnWidth.large,
|
|
formatter: (it) => _composeDestination(it),
|
|
),
|
|
DateColumn(
|
|
'document_date',
|
|
'تاریخ سند',
|
|
width: ColumnWidth.medium,
|
|
formatter: (it) => HesabixDateUtils.formatForDisplay(it.documentDate, widget.calendarController.isJalali),
|
|
),
|
|
NumberColumn(
|
|
'total_amount',
|
|
'مبلغ کل',
|
|
width: ColumnWidth.large,
|
|
formatter: (it) => formatWithThousands(it.totalAmount),
|
|
suffix: ' ریال',
|
|
),
|
|
TextColumn(
|
|
'created_by_name',
|
|
'ایجادکننده',
|
|
width: ColumnWidth.medium,
|
|
formatter: (it) => it.createdByName ?? 'نامشخص',
|
|
),
|
|
DateColumn(
|
|
'registered_at',
|
|
'تاریخ ثبت',
|
|
width: ColumnWidth.medium,
|
|
formatter: (it) => HesabixDateUtils.formatForDisplay(it.registeredAt, widget.calendarController.isJalali),
|
|
),
|
|
ActionColumn(
|
|
'actions',
|
|
'عملیات',
|
|
width: ColumnWidth.medium,
|
|
actions: [
|
|
DataTableAction(icon: Icons.visibility, label: 'مشاهده', onTap: (it) => _onView(it as TransferDocument)),
|
|
DataTableAction(icon: Icons.edit, label: 'ویرایش', onTap: (it) => _onEdit(it as TransferDocument)),
|
|
DataTableAction(icon: Icons.delete, label: 'حذف', onTap: (it) => _onDelete(it as TransferDocument), isDestructive: true),
|
|
],
|
|
),
|
|
],
|
|
searchFields: ['code', 'created_by_name', 'source', 'destination'],
|
|
dateRangeField: 'document_date',
|
|
showSearch: true,
|
|
showFilters: true,
|
|
showPagination: true,
|
|
showColumnSearch: true,
|
|
showRefreshButton: true,
|
|
showClearFiltersButton: true,
|
|
enableRowSelection: true,
|
|
enableMultiRowSelection: true,
|
|
showExportButtons: true,
|
|
showExcelExport: true,
|
|
showPdfExport: true,
|
|
defaultPageSize: 20,
|
|
pageSizeOptions: [10, 20, 50, 100],
|
|
// انتخاب سطرها در این صفحه استفاده خاصی ندارد
|
|
additionalParams: {
|
|
if (_fromDate != null) 'from_date': _fromDate!.toUtc().toIso8601String(),
|
|
if (_toDate != null) 'to_date': _toDate!.toUtc().toIso8601String(),
|
|
},
|
|
onRowTap: (item) => _onView(item as TransferDocument),
|
|
onRowDoubleTap: (item) => _onEdit(item as TransferDocument),
|
|
emptyStateMessage: 'هیچ سند انتقالی یافت نشد',
|
|
loadingMessage: 'در حال بارگذاری اسناد انتقال...',
|
|
errorMessage: 'خطا در بارگذاری اسناد انتقال',
|
|
);
|
|
}
|
|
|
|
String _typeFa(String? t) {
|
|
switch (t) {
|
|
case 'bank':
|
|
return 'بانک';
|
|
case 'cash_register':
|
|
return 'صندوق';
|
|
case 'petty_cash':
|
|
return 'تنخواه';
|
|
default:
|
|
return t ?? '';
|
|
}
|
|
}
|
|
|
|
String _composeSource(TransferDocument it) {
|
|
return '${_typeFa(it.sourceType)} ${it.sourceName ?? ''}'.trim();
|
|
}
|
|
|
|
String _composeDestination(TransferDocument it) {
|
|
return '${_typeFa(it.destinationType)} ${it.destinationName ?? ''}'.trim();
|
|
}
|
|
|
|
String _composeDesc(TransferDocument it) {
|
|
final src = _composeSource(it);
|
|
final dst = _composeDestination(it);
|
|
if (src.isEmpty && dst.isEmpty) return '';
|
|
return 'انتقال $src → $dst';
|
|
}
|
|
|
|
void _onAddNew() async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => TransferFormDialog(
|
|
businessId: widget.businessId,
|
|
calendarController: widget.calendarController,
|
|
authStore: widget.authStore,
|
|
apiClient: widget.apiClient,
|
|
onSuccess: () {},
|
|
),
|
|
);
|
|
if (result == true) _refreshData();
|
|
}
|
|
|
|
void _onView(TransferDocument item) async {
|
|
final svc = TransferService(widget.apiClient);
|
|
final full = await svc.getById(item.id);
|
|
if (!mounted) return;
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => TransferDetailsDialog(
|
|
document: full,
|
|
calendarController: widget.calendarController,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onEdit(TransferDocument item) async {
|
|
final svc = TransferService(widget.apiClient);
|
|
final full = await svc.getById(item.id);
|
|
if (!mounted) return;
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => TransferFormDialog(
|
|
businessId: widget.businessId,
|
|
calendarController: widget.calendarController,
|
|
authStore: widget.authStore,
|
|
apiClient: widget.apiClient,
|
|
initial: full,
|
|
onSuccess: () {},
|
|
),
|
|
);
|
|
if (result == true) _refreshData();
|
|
}
|
|
|
|
void _onDelete(TransferDocument item) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('حذف انتقال'),
|
|
content: Text('آیا از حذف سند ${item.code} مطمئن هستید؟'),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('انصراف')),
|
|
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('حذف')),
|
|
],
|
|
),
|
|
);
|
|
if (confirm == true) {
|
|
try {
|
|
final svc = TransferService(widget.apiClient);
|
|
await svc.deleteById(item.id);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('حذف شد'), backgroundColor: Colors.green));
|
|
}
|
|
_refreshData();
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('خطا: $e'), backgroundColor: Colors.red));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|