Update: 2026-05-08 01:15:44
This commit is contained in:
@@ -21,6 +21,7 @@ import '../../features/onboarding/views/onboarding_view.dart';
|
||||
import '../../features/companies/views/company_stats_view.dart';
|
||||
import '../../features/users/views/users_management_view.dart';
|
||||
import '../../features/tenants/views/tenants_management_view.dart';
|
||||
import '../../features/reports/views/tax_report_view.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
@@ -151,5 +152,9 @@ class AppPages {
|
||||
name: AppRoutes.USERS_MANAGEMENT,
|
||||
page: () => const UsersManagementView(),
|
||||
),
|
||||
GetPage(
|
||||
name: AppRoutes.TAX_REPORT,
|
||||
page: () => const TaxReportView(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ abstract class AppRoutes {
|
||||
static const COMPANY_STATS = '/company-stats';
|
||||
static const TENANTS_MANAGEMENT = '/tenants-management';
|
||||
static const USERS_MANAGEMENT = '/users-management';
|
||||
static const TAX_REPORT = '/tax-report';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/utils/app_snackbar.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
@@ -7,6 +8,7 @@ import '../../../core/utils/logger.dart';
|
||||
class InvoiceDetailController extends GetxController {
|
||||
var invoice = {}.obs;
|
||||
var isLoading = true.obs;
|
||||
var isSaving = false.obs;
|
||||
String? invoiceId;
|
||||
|
||||
@override
|
||||
@@ -42,6 +44,25 @@ class InvoiceDetailController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
final res = await DioClient()
|
||||
@@ -49,7 +70,6 @@ class InvoiceDetailController extends GetxController {
|
||||
.post('invoices/approve', data: {'invoice_id': invoiceId});
|
||||
if (res.data['success'] == true) {
|
||||
AppSnackbar.showSuccess('تم الاعتماد', 'تم اعتماد الفاتورة بنجاح');
|
||||
// Refresh the detail view
|
||||
fetchInvoiceDetails();
|
||||
} else {
|
||||
AppSnackbar.showError('خطأ', 'فشل اعتماد الفاتورة');
|
||||
@@ -63,7 +83,6 @@ class InvoiceDetailController extends GetxController {
|
||||
void viewOriginalImage() {
|
||||
final fileUrl = invoice['file_url'];
|
||||
if (fileUrl != null && fileUrl.isNotEmpty) {
|
||||
// Navigate to a dedicated image viewer or show in a dialog
|
||||
final fullUrl = 'https://musadaq.intaleqapp.com/api$fileUrl';
|
||||
Get.to(() => Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -90,8 +109,24 @@ class InvoiceDetailController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
// For now, just confirm download was successful
|
||||
AppSnackbar.showSuccess('تم التصدير', 'تم تحميل ملف CSV بنجاح (${res.data.length} bytes)');
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to export', e);
|
||||
AppSnackbar.showError('خطأ', 'فشل تصدير الفواتير');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> submitToJoFotara() async {
|
||||
// Confirmation dialog
|
||||
final confirmed = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: const Text('تأكيد الإرسال'),
|
||||
@@ -124,7 +159,7 @@ class InvoiceDetailController extends GetxController {
|
||||
|
||||
if (res.data['success'] == true) {
|
||||
AppSnackbar.showSuccess('تم الإرسال', 'تم تقديم الفاتورة لجوفتورة بنجاح');
|
||||
fetchInvoiceDetails(); // Refresh to show JoFotara status
|
||||
fetchInvoiceDetails();
|
||||
} else {
|
||||
AppSnackbar.showError('خطأ', res.data['message'] ?? 'فشل الإرسال');
|
||||
}
|
||||
|
||||
@@ -79,6 +79,20 @@ class InvoiceDetailView extends StatelessWidget {
|
||||
|
||||
// ─── Action Buttons ───
|
||||
if (status == 'extracted') ...[
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showEditDialog(context, inv, controller),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF3B82F6),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
icon: const Icon(Icons.edit_note_rounded),
|
||||
label: const Text('تعديل بيانات الفاتورة', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
@@ -568,4 +582,128 @@ class InvoiceDetailView extends StatelessWidget {
|
||||
if (num == num.truncateToDouble()) return num.toStringAsFixed(0);
|
||||
return num.toStringAsFixed(3);
|
||||
}
|
||||
|
||||
void _showEditDialog(BuildContext context, Map inv, InvoiceDetailController controller) {
|
||||
final invNumC = TextEditingController(text: inv['invoice_number']?.toString() ?? '');
|
||||
final invDateC = TextEditingController(text: inv['invoice_date']?.toString() ?? '');
|
||||
final supplierNameC = TextEditingController(text: inv['supplier_name']?.toString() ?? '');
|
||||
final supplierTinC = TextEditingController(text: inv['supplier_tin']?.toString() ?? '');
|
||||
final supplierAddressC = TextEditingController(text: inv['supplier_address']?.toString() ?? '');
|
||||
final buyerNameC = TextEditingController(text: inv['buyer_name']?.toString() ?? '');
|
||||
final buyerTinC = TextEditingController(text: inv['buyer_tin']?.toString() ?? '');
|
||||
final subtotalC = TextEditingController(text: inv['subtotal']?.toString() ?? '0');
|
||||
final taxC = TextEditingController(text: inv['tax_amount']?.toString() ?? '0');
|
||||
final discountC = TextEditingController(text: inv['discount_total']?.toString() ?? '0');
|
||||
final grandC = TextEditingController(text: inv['grand_total']?.toString() ?? '0');
|
||||
|
||||
Get.to(() => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('تعديل الفاتورة', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: const Color(0xFF3B82F6),
|
||||
foregroundColor: Colors.white,
|
||||
actions: [
|
||||
Obx(() => controller.isSaving.value
|
||||
? const Padding(padding: EdgeInsets.all(16), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)))
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
onPressed: () {
|
||||
controller.updateInvoice({
|
||||
'invoice_number': invNumC.text,
|
||||
'invoice_date': invDateC.text,
|
||||
'supplier_name': supplierNameC.text,
|
||||
'supplier_tin': supplierTinC.text,
|
||||
'supplier_address': supplierAddressC.text,
|
||||
'buyer_name': buyerNameC.text,
|
||||
'buyer_tin': buyerTinC.text,
|
||||
'subtotal': subtotalC.text,
|
||||
'tax_amount': taxC.text,
|
||||
'discount_total': discountC.text,
|
||||
'grand_total': grandC.text,
|
||||
}).then((_) => Get.back());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('معلومات الفاتورة', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_editRow('رقم الفاتورة', invNumC, Icons.numbers),
|
||||
_editRow('تاريخ الفاتورة', invDateC, Icons.calendar_today),
|
||||
const Divider(height: 32),
|
||||
const Text('بيانات المورّد', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_editRow('اسم المورّد', supplierNameC, Icons.store),
|
||||
_editRow('الرقم الضريبي', supplierTinC, Icons.badge),
|
||||
_editRow('العنوان', supplierAddressC, Icons.location_on),
|
||||
const Divider(height: 32),
|
||||
const Text('بيانات العميل', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_editRow('اسم العميل', buyerNameC, Icons.person),
|
||||
_editRow('الرقم الضريبي للعميل', buyerTinC, Icons.badge),
|
||||
const Divider(height: 32),
|
||||
const Text('المبالغ', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_editRow('المبلغ قبل الضريبة', subtotalC, Icons.attach_money, isNumeric: true),
|
||||
_editRow('الخصم', discountC, Icons.discount, isNumeric: true),
|
||||
_editRow('الضريبة', taxC, Icons.percent, isNumeric: true),
|
||||
_editRow('الإجمالي', grandC, Icons.payments, isNumeric: true),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
controller.updateInvoice({
|
||||
'invoice_number': invNumC.text,
|
||||
'invoice_date': invDateC.text,
|
||||
'supplier_name': supplierNameC.text,
|
||||
'supplier_tin': supplierTinC.text,
|
||||
'supplier_address': supplierAddressC.text,
|
||||
'buyer_name': buyerNameC.text,
|
||||
'buyer_tin': buyerTinC.text,
|
||||
'subtotal': subtotalC.text,
|
||||
'tax_amount': taxC.text,
|
||||
'discount_total': discountC.text,
|
||||
'grand_total': grandC.text,
|
||||
}).then((_) => Get.back());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('حفظ التعديلات', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _editRow(String label, TextEditingController ctrl, IconData icon, {bool isNumeric = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
textDirection: TextDirection.rtl,
|
||||
keyboardType: isNumeric ? const TextInputType.numberWithOptions(decimal: true) : TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: Icon(icon, size: 20),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../../../core/utils/app_snackbar.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
|
||||
class TaxReportController extends GetxController {
|
||||
var isLoading = true.obs;
|
||||
var report = {}.obs;
|
||||
var selectedMonth = DateTime.now().month.obs;
|
||||
var selectedYear = DateTime.now().year.obs;
|
||||
String? companyId;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments != null) {
|
||||
companyId = Get.arguments['company_id'];
|
||||
}
|
||||
fetchReport();
|
||||
}
|
||||
|
||||
Future<void> fetchReport() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final params = <String, dynamic>{
|
||||
'month': selectedMonth.value,
|
||||
'year': selectedYear.value,
|
||||
};
|
||||
if (companyId != null) params['company_id'] = companyId;
|
||||
|
||||
final res = await DioClient().client.get('reports/tax-summary', queryParameters: params);
|
||||
if (res.data['success'] == true) {
|
||||
report.value = res.data['data'];
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to fetch tax report', e);
|
||||
AppSnackbar.showError('خطأ', 'تعذر تحميل التقرير');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void changeMonth(int delta) {
|
||||
var m = selectedMonth.value + delta;
|
||||
var y = selectedYear.value;
|
||||
if (m > 12) { m = 1; y++; }
|
||||
if (m < 1) { m = 12; y--; }
|
||||
selectedMonth.value = m;
|
||||
selectedYear.value = y;
|
||||
fetchReport();
|
||||
}
|
||||
|
||||
String get monthName {
|
||||
const months = ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو',
|
||||
'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'];
|
||||
return '${months[selectedMonth.value]} ${selectedYear.value}';
|
||||
}
|
||||
}
|
||||
344
musadaq-app/lib/features/reports/views/tax_report_view.dart
Normal file
344
musadaq-app/lib/features/reports/views/tax_report_view.dart
Normal file
@@ -0,0 +1,344 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controllers/tax_report_controller.dart';
|
||||
|
||||
class TaxReportView extends StatelessWidget {
|
||||
const TaxReportView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(TaxReportController());
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: isDark ? const Color(0xFF121212) : const Color(0xFFF5F7FA),
|
||||
appBar: AppBar(
|
||||
title: const Text('التقرير الضريبي الشهري', style: TextStyle(fontFamily: 'El Messiri', fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
backgroundColor: const Color(0xFF0F4C81),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator(color: Color(0xFF0F4C81)));
|
||||
}
|
||||
|
||||
final summary = controller.report['summary'] as Map? ?? {};
|
||||
final growth = controller.report['growth'] as Map? ?? {};
|
||||
final topSuppliers = (controller.report['top_suppliers'] as List?) ?? [];
|
||||
final daily = (controller.report['daily_breakdown'] as List?) ?? [];
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.fetchReport,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Month Selector
|
||||
_buildMonthSelector(controller, isDark),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Summary Cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildMetricCard('إجمالي الفواتير', '${summary['total_invoices'] ?? 0}', Icons.receipt_long, const Color(0xFF3B82F6), growth['invoices'], isDark)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildMetricCard('إجمالي المبيعات', '${_fmt(summary['total_grand'])} JOD', Icons.payments, const Color(0xFF10B981), growth['revenue'], isDark)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildMetricCard('ضريبة المبيعات', '${_fmt(summary['total_tax'])} JOD', Icons.percent, const Color(0xFFF59E0B), growth['tax'], isDark)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _buildMetricCard('مقدمة لجوفتورة', '${summary['submitted_count'] ?? 0}', Icons.send_rounded, const Color(0xFF6366F1), null, isDark)),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Status Breakdown
|
||||
_buildStatusBreakdown(summary, isDark),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Daily Chart (simple bar representation)
|
||||
if (daily.isNotEmpty) ...[
|
||||
_buildDailyChart(daily, isDark),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Top Suppliers
|
||||
if (topSuppliers.isNotEmpty) ...[
|
||||
_buildTopSuppliers(topSuppliers, isDark),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Amount Details
|
||||
_buildAmountDetails(summary, isDark),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthSelector(TaxReportController controller, bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
onPressed: () => controller.changeMonth(1),
|
||||
),
|
||||
Obx(() => Text(
|
||||
controller.monthName,
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : const Color(0xFF0F172A),
|
||||
),
|
||||
)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: () => controller.changeMonth(-1),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricCard(String label, String value, IconData icon, Color color, dynamic growthPct, bool isDark) {
|
||||
final growth = double.tryParse(growthPct?.toString() ?? '') ?? 0;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
const Spacer(),
|
||||
if (growthPct != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: (growth >= 0 ? const Color(0xFF10B981) : Colors.red).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${growth >= 0 ? "+" : ""}${growth.toStringAsFixed(1)}%',
|
||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: growth >= 0 ? const Color(0xFF10B981) : Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: isDark ? Colors.white : const Color(0xFF0F172A), fontFamily: 'monospace')),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBreakdown(Map summary, bool isDark) {
|
||||
final total = (int.tryParse(summary['total_invoices']?.toString() ?? '0') ?? 0);
|
||||
final approved = (int.tryParse(summary['approved_count']?.toString() ?? '0') ?? 0);
|
||||
final pending = (int.tryParse(summary['pending_count']?.toString() ?? '0') ?? 0);
|
||||
final submitted = (int.tryParse(summary['submitted_count']?.toString() ?? '0') ?? 0);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('توزيع الحالات', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_statusRow('معتمدة', approved, total, const Color(0xFF10B981), isDark),
|
||||
const SizedBox(height: 8),
|
||||
_statusRow('قيد المراجعة', pending, total, const Color(0xFFF59E0B), isDark),
|
||||
const SizedBox(height: 8),
|
||||
_statusRow('مقدمة لجوفتورة', submitted, total, const Color(0xFF6366F1), isDark),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statusRow(String label, int count, int total, Color color, bool isDark) {
|
||||
final pct = total > 0 ? count / total : 0.0;
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.black87)),
|
||||
Text('$count (${(pct * 100).toStringAsFixed(0)}%)', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: color)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(value: pct, backgroundColor: color.withValues(alpha: 0.1), color: color, minHeight: 6),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDailyChart(List daily, bool isDark) {
|
||||
final maxVal = daily.fold<double>(0, (max, d) => (double.tryParse(d['daily_total']?.toString() ?? '0') ?? 0) > max ? (double.tryParse(d['daily_total']?.toString() ?? '0') ?? 0) : max);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('المبيعات اليومية', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: daily.map((d) {
|
||||
final val = double.tryParse(d['daily_total']?.toString() ?? '0') ?? 0;
|
||||
final height = maxVal > 0 ? (val / maxVal) * 100 : 0.0;
|
||||
return Expanded(
|
||||
child: Tooltip(
|
||||
message: 'يوم ${d['day_num']}: ${val.toStringAsFixed(2)} JOD',
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF3B82F6).withValues(alpha: 0.7),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopSuppliers(List suppliers, bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('أكبر الموردين', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
...suppliers.asMap().entries.map((e) {
|
||||
final s = e.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16, backgroundColor: const Color(0xFF0F4C81).withValues(alpha: 0.1),
|
||||
child: Text('${e.key + 1}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF0F4C81))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s['supplier_name'] ?? '—', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
Text('${s['invoice_count']} فاتورة', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text('${_fmt(s['total_amount'])} JOD', style: const TextStyle(fontWeight: FontWeight.bold, fontFamily: 'monospace', fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmountDetails(Map summary, bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('تفاصيل المبالغ', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
_amountRow('إجمالي المبيعات (قبل الضريبة)', summary['total_subtotal'], isDark),
|
||||
const Divider(height: 20),
|
||||
_amountRow('إجمالي الخصومات', summary['total_discount'], isDark),
|
||||
const Divider(height: 20),
|
||||
_amountRow('إجمالي ضريبة المبيعات', summary['total_tax'], isDark, color: const Color(0xFFF59E0B)),
|
||||
const Divider(height: 20),
|
||||
_amountRow('صافي المبيعات', summary['total_grand'], isDark, isBold: true),
|
||||
const Divider(height: 20),
|
||||
_amountRow('متوسط قيمة الفاتورة', summary['avg_invoice_amount'], isDark),
|
||||
const Divider(height: 20),
|
||||
_amountRow('أعلى فاتورة', summary['max_invoice_amount'], isDark),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _amountRow(String label, dynamic value, bool isDark, {bool isBold = false, Color? color}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: TextStyle(fontSize: 13, color: color ?? (isDark ? Colors.white70 : Colors.grey.shade600))),
|
||||
Text(
|
||||
'${_fmt(value)} JOD',
|
||||
style: TextStyle(
|
||||
fontSize: isBold ? 17 : 14,
|
||||
fontWeight: isBold ? FontWeight.w900 : FontWeight.w600,
|
||||
color: color ?? (isDark ? Colors.white : Colors.black87),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _fmt(dynamic v) {
|
||||
final n = double.tryParse(v?.toString() ?? '0') ?? 0;
|
||||
return n.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user