Files
musadaq-saas/musadaq-app/lib/features/invoices/controllers/invoice_detail_controller.dart
2026-05-08 14:05:50 +03:00

267 lines
10 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../core/network/dio_client.dart';
import '../../../core/utils/app_snackbar.dart';
import '../../../core/utils/logger.dart';
class InvoiceDetailController extends GetxController {
var invoice = {}.obs;
var isLoading = true.obs;
var isSaving = false.obs;
String? invoiceId;
@override
void onInit() {
super.onInit();
if (Get.arguments != null) {
invoiceId = Get.arguments['id'];
if (invoiceId != null) {
fetchInvoiceDetails();
}
}
}
Future<void> fetchInvoiceDetails() async {
try {
isLoading.value = true;
final res = await DioClient()
.client
.get('invoices/view', queryParameters: {'id': invoiceId});
if (res.data['success'] == true && res.data['data'] != null) {
invoice.value = res.data['data'];
} else {
AppSnackbar.showError('خطأ', 'لم يتم العثور على الفاتورة');
Get.back();
}
} catch (e) {
AppLogger.error('Failed to fetch invoice details', e);
AppSnackbar.showError('خطأ', 'فشل تحميل بيانات الفاتورة');
Get.back();
} finally {
isLoading.value = false;
}
}
Future<void> updateInvoice(Map<String, dynamic> data) async {
try {
isSaving.value = true;
data['id'] = invoiceId;
final res = await DioClient().client.post('invoices/update', data: data);
if (res.data['success'] == true) {
AppSnackbar.showSuccess('تم الحفظ', 'تم تحديث بيانات الفاتورة');
await fetchInvoiceDetails();
} else {
AppSnackbar.showError('خطأ', res.data['message'] ?? 'فشل التحديث');
}
} catch (e) {
AppLogger.error('Failed to update invoice', e);
AppSnackbar.showError('خطأ', 'حدث خطأ أثناء التحديث');
} finally {
isSaving.value = false;
}
}
Future<void> approveInvoice() async {
// First check for duplicates
try {
final dupRes = await DioClient().client.post('invoices/check-duplicate', data: {
'invoice_number': invoice['invoice_number'],
'supplier_tin': invoice['supplier_tin'],
'grand_total': invoice['grand_total'],
'invoice_date': invoice['invoice_date'],
'exclude_id': invoiceId,
});
if (dupRes.data['success'] == true) {
final matches = dupRes.data['data']?['matches'] as List? ?? [];
if (matches.isNotEmpty) {
// Show duplicate warning
final proceed = await Get.dialog<bool>(
AlertDialog(
title: const Row(
children: [
Icon(Icons.warning_amber, color: Colors.orange, size: 28),
SizedBox(width: 8),
Text('تحذير — فاتورة مكررة!', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('تم العثور على ${matches.length} فاتورة مشابهة:', style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
...matches.take(3).map((m) => Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('رقم: ${m['invoice_number'] ?? ''}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
Text('المورد: ${m['supplier_name'] ?? ''}', style: const TextStyle(fontSize: 12)),
Text('المبلغ: ${m['grand_total'] ?? ''} | نوع التطابق: ${m['match_type'] ?? ''}', style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
)),
const SizedBox(height: 8),
const Text('هل تريد الاستمرار بالاعتماد؟', style: TextStyle(fontWeight: FontWeight.w600)),
],
),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('إلغاء'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
child: const Text('اعتماد رغم التكرار', style: TextStyle(color: Colors.white)),
),
],
),
);
if (proceed != true) return;
}
}
} catch (e) {
// If duplicate check fails, proceed with approval anyway
AppLogger.error('Duplicate check failed (proceeding)', e);
}
// Proceed with approval
try {
final res = await DioClient()
.client
.post('invoices/approve', data: {'id': invoiceId});
if (res.data['success'] == true) {
AppSnackbar.showSuccess('تم الاعتماد', 'تم اعتماد الفاتورة بنجاح');
fetchInvoiceDetails();
} else {
AppSnackbar.showError('خطأ', 'فشل اعتماد الفاتورة');
}
} catch (e) {
AppLogger.error('Failed to approve invoice', e);
AppSnackbar.showError('خطأ', 'حدث خطأ غير متوقع');
}
}
void viewOriginalImage() {
final fileUrl = invoice['file_url'];
if (fileUrl != null && fileUrl.isNotEmpty) {
final fullUrl = 'https://musadaq.intaleqapp.com/api$fileUrl';
Get.to(() => Scaffold(
appBar: AppBar(
title: const Text('صورة الفاتورة'),
backgroundColor: const Color(0xFF0F4C81)),
body: Center(
child: InteractiveViewer(
child: Image.network(
fullUrl,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const CircularProgressIndicator();
},
errorBuilder: (context, error, stackTrace) {
return const Text(
'فشل تحميل الصورة. قد يكون الملف مفقوداً على الخادم.');
},
),
),
),
));
} else {
AppSnackbar.showWarning('عذراً', 'لا توجد صورة مرتبطة بهذه الفاتورة');
}
}
Future<void> exportInvoices({String? companyId}) async {
try {
final cId = companyId ?? invoice['company_id'];
AppSnackbar.showInfo('جاري التصدير', 'يتم تحميل ملف الفواتير...');
final res = await DioClient().client.get(
'invoices/export',
queryParameters: {'company_id': cId},
options: Options(responseType: ResponseType.bytes),
);
// Save to temp file
final dir = await getTemporaryDirectory();
final fileName = 'musadaq_invoices_${DateTime.now().millisecondsSinceEpoch}.csv';
final file = File('${dir.path}/$fileName');
final bytes = List<int>.from(res.data);
await file.writeAsBytes(bytes);
// Share via native sheet (share_plus v12 API)
try {
await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path, mimeType: 'text/csv', name: fileName)],
title: 'تصدير فواتير مُصادَق',
),
);
} catch (shareErr) {
AppLogger.error('Share fallback', shareErr);
AppSnackbar.showSuccess('تم الحفظ', 'تم حفظ الملف: ${file.path}');
}
} catch (e) {
AppLogger.error('Failed to export', e);
AppSnackbar.showError('خطأ', 'فشل تصدير الفواتير: $e');
}
}
Future<void> submitToJoFotara() async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('تأكيد الإرسال'),
content: const Text(
'هل أنت متأكد من إرسال هذه الفاتورة لمنظومة جوفوترا؟\nلا يمكن التراجع عن هذا الإجراء.'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('إلغاء'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
),
child: const Text('إرسال', style: TextStyle(color: Colors.white)),
),
],
),
);
if (confirmed != true) return;
try {
AppSnackbar.showInfo('جاري الإرسال', 'يتم إرسال الفاتورة لمنظومة جوفوترا...');
final res = await DioClient().client.post(
'invoices/submit-jofotara',
data: {'invoice_id': invoiceId},
);
if (res.data['success'] == true) {
AppSnackbar.showSuccess('تم الإرسال', 'تم تقديم الفاتورة لجوفوترا بنجاح');
fetchInvoiceDetails();
} else {
AppSnackbar.showError('خطأ', res.data['message'] ?? 'فشل الإرسال');
}
} catch (e) {
AppLogger.error('Failed to submit to JoFotara', e);
AppSnackbar.showError('خطأ', 'فشل إرسال الفاتورة لجوفوترا');
}
}
}