hesabixArc/hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart

406 lines
12 KiB
Dart
Raw Normal View History

2025-09-25 22:36:08 +03:30
import 'package:flutter/material.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import '../../widgets/data_table/data_table_widget.dart';
import '../../widgets/data_table/data_table_config.dart';
import '../../widgets/person/person_form_dialog.dart';
2025-09-28 14:29:09 +03:30
import '../../widgets/person/person_import_dialog.dart';
2025-09-25 22:36:08 +03:30
import '../../widgets/permission/permission_widgets.dart';
import '../../models/person_model.dart';
import '../../services/person_service.dart';
import '../../core/auth_store.dart';
class PersonsPage extends StatefulWidget {
final int businessId;
final AuthStore authStore;
const PersonsPage({
super.key,
required this.businessId,
required this.authStore,
});
@override
State<PersonsPage> createState() => _PersonsPageState();
}
class _PersonsPageState extends State<PersonsPage> {
final _personService = PersonService();
2025-09-26 23:05:20 +03:30
final GlobalKey _personsTableKey = GlobalKey();
2025-09-25 22:36:08 +03:30
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
// بررسی دسترسی خواندن
if (!widget.authStore.canReadSection('people')) {
return AccessDeniedPage(
message: 'شما دسترسی لازم برای مشاهده لیست اشخاص را ندارید',
);
}
return Scaffold(
body: DataTableWidget<Person>(
2025-09-26 23:05:20 +03:30
key: _personsTableKey,
2025-09-25 22:36:08 +03:30
config: _buildDataTableConfig(t),
fromJson: Person.fromJson,
),
);
}
DataTableConfig<Person> _buildDataTableConfig(AppLocalizations t) {
return DataTableConfig<Person>(
endpoint: '/api/v1/persons/businesses/${widget.businessId}/persons',
title: t.personsList,
2025-09-27 21:19:00 +03:30
excelEndpoint: '/api/v1/persons/businesses/${widget.businessId}/persons/export/excel',
pdfEndpoint: '/api/v1/persons/businesses/${widget.businessId}/persons/export/pdf',
getExportParams: () => {
'business_id': widget.businessId,
},
showBackButton: true,
onBack: () => Navigator.of(context).maybePop(),
showTableIcon: false,
2025-09-26 23:05:20 +03:30
showRowNumbers: true,
enableRowSelection: true,
2025-09-28 12:28:41 +03:30
enableMultiRowSelection: true,
2025-09-25 22:36:08 +03:30
columns: [
2025-09-26 23:05:20 +03:30
NumberColumn(
'code',
2025-09-29 19:19:24 +03:30
t.personCode,
2025-09-26 23:05:20 +03:30
width: ColumnWidth.small,
formatter: (person) => (person.code?.toString() ?? '-'),
textAlign: TextAlign.center,
),
2025-09-25 22:36:08 +03:30
TextColumn(
'alias_name',
t.personAliasName,
width: ColumnWidth.large,
formatter: (person) => person.aliasName,
),
TextColumn(
'first_name',
t.personFirstName,
width: ColumnWidth.medium,
formatter: (person) => person.firstName ?? '-',
),
TextColumn(
'last_name',
t.personLastName,
width: ColumnWidth.medium,
formatter: (person) => person.lastName ?? '-',
),
TextColumn(
'person_type',
t.personType,
width: ColumnWidth.medium,
2025-09-29 19:19:24 +03:30
filterType: ColumnFilterType.multiSelect,
filterOptions: [
FilterOption(value: 'مشتری', label: t.personTypeCustomer),
FilterOption(value: 'بازاریاب', label: t.personTypeMarketer),
FilterOption(value: 'کارمند', label: t.personTypeEmployee),
FilterOption(value: 'تامین‌کننده', label: t.personTypeSupplier),
FilterOption(value: 'همکار', label: t.personTypePartner),
FilterOption(value: 'فروشنده', label: t.personTypeSeller),
FilterOption(value: 'سهامدار', label: 'سهامدار'),
],
2025-09-26 23:05:20 +03:30
formatter: (person) => (person.personTypes.isNotEmpty
? person.personTypes.map((e) => e.persianName).join('، ')
: person.personType.persianName),
2025-09-25 22:36:08 +03:30
),
TextColumn(
'company_name',
t.personCompanyName,
width: ColumnWidth.medium,
formatter: (person) => person.companyName ?? '-',
),
TextColumn(
'mobile',
t.personMobile,
width: ColumnWidth.medium,
formatter: (person) => person.mobile ?? '-',
),
TextColumn(
'email',
t.personEmail,
width: ColumnWidth.large,
formatter: (person) => person.email ?? '-',
),
TextColumn(
2025-09-26 23:05:20 +03:30
'national_id',
t.personNationalId,
width: ColumnWidth.medium,
formatter: (person) => person.nationalId ?? '-',
2025-09-25 22:36:08 +03:30
),
2025-09-27 21:19:00 +03:30
NumberColumn(
'share_count',
2025-09-29 19:19:24 +03:30
t.shareCount,
2025-09-27 21:19:00 +03:30
width: ColumnWidth.small,
textAlign: TextAlign.center,
decimalPlaces: 0,
),
NumberColumn(
'commission_sale_percent',
2025-09-29 19:19:24 +03:30
t.commissionSalePercentLabel,
2025-09-27 21:19:00 +03:30
width: ColumnWidth.medium,
decimalPlaces: 2,
suffix: '٪',
),
NumberColumn(
'commission_sales_return_percent',
2025-09-29 19:19:24 +03:30
t.commissionSalesReturnPercentLabel,
2025-09-27 21:19:00 +03:30
width: ColumnWidth.medium,
decimalPlaces: 2,
suffix: '٪',
),
NumberColumn(
'commission_sales_amount',
2025-09-29 19:19:24 +03:30
t.commissionSalesAmountLabel,
2025-09-27 21:19:00 +03:30
width: ColumnWidth.large,
decimalPlaces: 0,
),
NumberColumn(
'commission_sales_return_amount',
2025-09-29 19:19:24 +03:30
t.commissionSalesReturnAmountLabel,
2025-09-27 21:19:00 +03:30
width: ColumnWidth.large,
decimalPlaces: 0,
),
TextColumn(
'payment_id',
t.personPaymentId,
width: ColumnWidth.medium,
formatter: (person) => person.paymentId ?? '-',
),
TextColumn(
'registration_number',
t.personRegistrationNumber,
width: ColumnWidth.medium,
formatter: (person) => person.registrationNumber ?? '-',
),
TextColumn(
'economic_id',
t.personEconomicId,
width: ColumnWidth.medium,
formatter: (person) => person.economicId ?? '-',
),
TextColumn(
'country',
t.personCountry,
width: ColumnWidth.medium,
formatter: (person) => person.country ?? '-',
),
TextColumn(
'province',
t.personProvince,
width: ColumnWidth.medium,
formatter: (person) => person.province ?? '-',
),
TextColumn(
'city',
t.personCity,
width: ColumnWidth.medium,
formatter: (person) => person.city ?? '-',
),
TextColumn(
'address',
t.personAddress,
width: ColumnWidth.extraLarge,
formatter: (person) => person.address ?? '-',
),
TextColumn(
'postal_code',
t.personPostalCode,
width: ColumnWidth.medium,
formatter: (person) => person.postalCode ?? '-',
),
TextColumn(
'phone',
t.personPhone,
width: ColumnWidth.medium,
formatter: (person) => person.phone ?? '-',
),
TextColumn(
'fax',
t.personFax,
width: ColumnWidth.medium,
formatter: (person) => person.fax ?? '-',
),
TextColumn(
'website',
t.personWebsite,
width: ColumnWidth.large,
formatter: (person) => person.website ?? '-',
),
2025-09-25 22:36:08 +03:30
ActionColumn(
'actions',
2025-09-29 19:19:24 +03:30
t.actions,
2025-09-25 22:36:08 +03:30
actions: [
DataTableAction(
icon: Icons.edit,
label: t.edit,
onTap: (person) => _editPerson(person),
),
DataTableAction(
icon: Icons.delete,
label: t.delete,
color: Colors.red,
onTap: (person) => _deletePerson(person),
),
],
),
],
searchFields: [
2025-09-26 23:05:20 +03:30
'code',
2025-09-25 22:36:08 +03:30
'alias_name',
'first_name',
'last_name',
'company_name',
'mobile',
'email',
'national_id',
],
filterFields: [
'person_type',
2025-09-26 23:05:20 +03:30
'person_types',
2025-09-25 22:36:08 +03:30
'country',
'province',
],
defaultPageSize: 20,
2025-09-27 21:19:00 +03:30
// انتقال دکمه افزودن به اکشن‌های هدر جدول با کنترل دسترسی
customHeaderActions: [
PermissionButton(
section: 'people',
action: 'add',
authStore: widget.authStore,
child: Tooltip(
message: t.addPerson,
child: IconButton(
onPressed: _addPerson,
icon: const Icon(Icons.add),
),
),
),
2025-09-29 19:19:24 +03:30
Builder(builder: (context) {
final theme = Theme.of(context);
return Tooltip(
message: t.importFromExcel,
child: GestureDetector(
onTap: () async {
final ok = await showDialog<bool>(
context: context,
builder: (context) => PersonImportDialog(businessId: widget.businessId),
);
if (ok == true) {
final state = _personsTableKey.currentState;
try {
// ignore: avoid_dynamic_calls
(state as dynamic)?.refresh();
} catch (_) {}
}
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Icon(
Icons.upload_file,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
);
}),
2025-09-27 21:19:00 +03:30
],
2025-09-25 22:36:08 +03:30
);
}
void _addPerson() {
showDialog(
context: context,
builder: (context) => PersonFormDialog(
businessId: widget.businessId,
onSuccess: () {
2025-09-26 23:05:20 +03:30
final state = _personsTableKey.currentState;
try {
// Call public refresh() via dynamic to avoid private state typing
// ignore: avoid_dynamic_calls
(state as dynamic)?.refresh();
} catch (_) {}
2025-09-25 22:36:08 +03:30
},
),
);
}
void _editPerson(Person person) {
showDialog(
context: context,
builder: (context) => PersonFormDialog(
businessId: widget.businessId,
person: person,
onSuccess: () {
2025-09-26 23:05:20 +03:30
final state = _personsTableKey.currentState;
try {
// ignore: avoid_dynamic_calls
(state as dynamic)?.refresh();
} catch (_) {}
2025-09-25 22:36:08 +03:30
},
),
);
}
void _deletePerson(Person person) {
final t = AppLocalizations.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(t.deletePerson),
content: Text('آیا از حذف شخص "${person.displayName}" مطمئن هستید؟'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(t.cancel),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await _performDelete(person);
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text(t.delete),
),
],
),
);
}
Future<void> _performDelete(Person person) async {
try {
await _personService.deletePerson(person.id!);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).personDeletedSuccessfully),
backgroundColor: Colors.green,
),
);
// DataTableWidget will automatically refresh
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('خطا در حذف شخص: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}