Update: 2026-05-08 01:27:14
This commit is contained in:
@@ -234,6 +234,14 @@ class DashboardView extends GetView<DashboardController> {
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.SUBSCRIPTION),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildAdminActionCard(
|
||||
'التقارير',
|
||||
Icons.bar_chart_rounded,
|
||||
const Color(0xFFF59E0B),
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.TAX_REPORT),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -67,7 +67,7 @@ class InvoiceDetailController extends GetxController {
|
||||
try {
|
||||
final res = await DioClient()
|
||||
.client
|
||||
.post('invoices/approve', data: {'invoice_id': invoiceId});
|
||||
.post('invoices/approve', data: {'id': invoiceId});
|
||||
if (res.data['success'] == true) {
|
||||
AppSnackbar.showSuccess('تم الاعتماد', 'تم اعتماد الفاتورة بنجاح');
|
||||
fetchInvoiceDetails();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/utils/app_snackbar.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
|
||||
class InvoicesController extends GetxController {
|
||||
@@ -9,6 +10,10 @@ class InvoicesController extends GetxController {
|
||||
var searchQuery = ''.obs;
|
||||
var isSearching = false.obs;
|
||||
|
||||
// Multi-select
|
||||
var isSelecting = false.obs;
|
||||
var selectedIds = <String>{}.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
@@ -49,6 +54,53 @@ class InvoicesController extends GetxController {
|
||||
if (!isSearching.value) searchQuery.value = '';
|
||||
}
|
||||
|
||||
// ── Multi-select ──
|
||||
void toggleSelectMode() {
|
||||
isSelecting.value = !isSelecting.value;
|
||||
if (!isSelecting.value) selectedIds.clear();
|
||||
}
|
||||
|
||||
void toggleSelection(String id) {
|
||||
if (selectedIds.contains(id)) {
|
||||
selectedIds.remove(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
void selectAllExtracted() {
|
||||
selectedIds.clear();
|
||||
for (final inv in filteredInvoices) {
|
||||
if (inv['status'] == 'extracted') {
|
||||
selectedIds.add(inv['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> bulkApprove() async {
|
||||
if (selectedIds.isEmpty) {
|
||||
AppSnackbar.showWarning('تنبيه', 'يرجى اختيار فاتورة واحدة على الأقل');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final res = await DioClient().client.post('invoices/bulk-approve', data: {
|
||||
'ids': selectedIds.toList(),
|
||||
});
|
||||
|
||||
if (res.data['success'] == true) {
|
||||
final count = res.data['data']?['approved_count'] ?? 0;
|
||||
AppSnackbar.showSuccess('تم الاعتماد', 'تم اعتماد $count فاتورة بنجاح');
|
||||
selectedIds.clear();
|
||||
isSelecting.value = false;
|
||||
await loadInvoices();
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to bulk approve', e);
|
||||
AppSnackbar.showError('خطأ', 'فشل اعتماد الفواتير');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadInvoices() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../controllers/invoices_controller.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/utils/app_snackbar.dart';
|
||||
|
||||
class InvoicesListView extends GetView<InvoicesController> {
|
||||
const InvoicesListView({super.key});
|
||||
@@ -30,6 +33,31 @@ class InvoicesListView extends GetView<InvoicesController> {
|
||||
icon: const Icon(Icons.search_rounded, color: Colors.white),
|
||||
onPressed: () => controller.toggleSearch(),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||
onSelected: (value) {
|
||||
if (value == 'report') {
|
||||
Get.toNamed('/tax-report');
|
||||
} else if (value == 'export') {
|
||||
_exportInvoices(context);
|
||||
} else if (value == 'select') {
|
||||
controller.toggleSelectMode();
|
||||
} else if (value == 'select_all') {
|
||||
controller.selectAllExtracted();
|
||||
} else if (value == 'bulk_approve') {
|
||||
controller.bulkApprove();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'select', child: Row(children: [Icon(controller.isSelecting.value ? Icons.close : Icons.checklist, size: 18), const SizedBox(width: 8), Text(controller.isSelecting.value ? 'إلغاء التحديد' : 'تحديد متعدد')])),
|
||||
if (controller.isSelecting.value) ...[
|
||||
const PopupMenuItem(value: 'select_all', child: Row(children: [Icon(Icons.select_all, size: 18), SizedBox(width: 8), Text('تحديد الكل (المستخرجة)')])),
|
||||
const PopupMenuItem(value: 'bulk_approve', child: Row(children: [Icon(Icons.check_circle, size: 18, color: Color(0xFF10B981)), SizedBox(width: 8), Text('اعتماد المحدد')])),
|
||||
],
|
||||
const PopupMenuItem(value: 'report', child: Row(children: [Icon(Icons.bar_chart, size: 18), SizedBox(width: 8), Text('التقرير الضريبي')])),
|
||||
const PopupMenuItem(value: 'export', child: Row(children: [Icon(Icons.file_download, size: 18), SizedBox(width: 8), Text('تصدير Excel')])),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded, color: Colors.white),
|
||||
onPressed: () => controller.loadInvoices(),
|
||||
@@ -104,6 +132,42 @@ class InvoicesListView extends GetView<InvoicesController> {
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
// Bulk Approve Bar
|
||||
Obx(() {
|
||||
if (!controller.isSelecting.value || controller.selectedIds.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
color: const Color(0xFF10B981),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${controller.selectedIds.length} محدد',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () => controller.selectAllExtracted(),
|
||||
icon: const Icon(Icons.select_all, color: Colors.white, size: 18),
|
||||
label: const Text('الكل', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => controller.bulkApprove(),
|
||||
icon: const Icon(Icons.check_circle, size: 18),
|
||||
label: const Text('اعتماد', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: const Color(0xFF10B981),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -159,23 +223,44 @@ class InvoicesListView extends GetView<InvoicesController> {
|
||||
statusIcon = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Card(
|
||||
return Obx(() {
|
||||
final isSelected = controller.selectedIds.contains(inv['id']);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
color: isSelected ? statusColor.withValues(alpha: 0.05) : (isDark ? const Color(0xFF1E1E2E) : Colors.white),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
side: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
side: BorderSide(color: isSelected ? statusColor : (isDark ? Colors.white10 : Colors.grey.shade200), width: isSelected ? 2 : 1),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () {
|
||||
Get.toNamed('/invoice-detail', arguments: {'id': inv['id'].toString()});
|
||||
if (controller.isSelecting.value) {
|
||||
controller.toggleSelection(inv['id']);
|
||||
} else {
|
||||
Get.toNamed('/invoice-detail', arguments: {'id': inv['id'].toString()});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (!controller.isSelecting.value) {
|
||||
controller.isSelecting.value = true;
|
||||
}
|
||||
controller.toggleSelection(inv['id']);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
if (controller.isSelecting.value) ...[
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) => controller.toggleSelection(inv['id']),
|
||||
activeColor: statusColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
@@ -243,6 +328,7 @@ class InvoicesListView extends GetView<InvoicesController> {
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(bool isDark) {
|
||||
@@ -284,4 +370,17 @@ class InvoicesListView extends GetView<InvoicesController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _exportInvoices(BuildContext context) async {
|
||||
try {
|
||||
AppSnackbar.showInfo('جاري التصدير', 'يتم تحميل ملف الفواتير...');
|
||||
final res = await DioClient().client.get(
|
||||
'invoices/export',
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
AppSnackbar.showSuccess('تم التصدير', 'تم تحميل ملف CSV بنجاح (${(res.data as List).length} bytes)');
|
||||
} catch (e) {
|
||||
AppSnackbar.showError('خطأ', 'فشل تصدير الفواتير');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user