Update: 2026-05-08 01:27:14

This commit is contained in:
Hamza-Ayed
2026-05-08 01:27:14 +03:00
parent 928e8e27e3
commit 23813fee95
6 changed files with 227 additions and 5 deletions

View File

@@ -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),
),
],
),
),

View File

@@ -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();

View File

@@ -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;

View File

@@ -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('خطأ', 'فشل تصدير الفواتير');
}
}
}