Update: 2026-05-08 01:15:44
This commit is contained in:
133
app/modules_app/invoices/export.php
Normal file
133
app/modules_app/invoices/export.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Export Invoices as CSV (Excel-compatible)
|
||||
* GET /v1/invoices/export
|
||||
* Downloads a CSV file with invoice data + line items
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
$companyId = $_GET['company_id'] ?? null;
|
||||
$dateFrom = $_GET['date_from'] ?? null;
|
||||
$dateTo = $_GET['date_to'] ?? null;
|
||||
$status = $_GET['status'] ?? null;
|
||||
|
||||
// Build query with filters
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
$where[] = 'i.tenant_id = ?';
|
||||
$params[] = $tenantId;
|
||||
}
|
||||
|
||||
if ($companyId) {
|
||||
$where[] = 'i.company_id = ?';
|
||||
$params[] = $companyId;
|
||||
}
|
||||
|
||||
if ($dateFrom) {
|
||||
$where[] = 'i.invoice_date >= ?';
|
||||
$params[] = $dateFrom;
|
||||
}
|
||||
|
||||
if ($dateTo) {
|
||||
$where[] = 'i.invoice_date <= ?';
|
||||
$params[] = $dateTo;
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
$where[] = 'i.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT i.*, c.name as company_name_raw
|
||||
FROM invoices i
|
||||
JOIN companies c ON i.company_id = c.id
|
||||
$whereClause
|
||||
ORDER BY i.invoice_date DESC
|
||||
LIMIT 5000
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$invoices = $stmt->fetchAll();
|
||||
|
||||
// Decrypt helper
|
||||
$dec = function($val) {
|
||||
if (empty($val)) return '';
|
||||
$result = Encryption::decrypt((string)$val);
|
||||
return ($result !== false && $result !== null) ? $result : (string)$val;
|
||||
};
|
||||
|
||||
// UTF-8 BOM for Excel compatibility
|
||||
$output = "\xEF\xBB\xBF";
|
||||
|
||||
// CSV headers
|
||||
$output .= implode(',', [
|
||||
'رقم الفاتورة',
|
||||
'تاريخ الفاتورة',
|
||||
'الشركة',
|
||||
'اسم المورّد',
|
||||
'الرقم الضريبي للمورّد',
|
||||
'عنوان المورّد',
|
||||
'اسم العميل',
|
||||
'الرقم الضريبي للعميل',
|
||||
'نوع الفاتورة',
|
||||
'المبلغ قبل الضريبة',
|
||||
'قيمة الخصم',
|
||||
'قيمة الضريبة',
|
||||
'الإجمالي',
|
||||
'العملة',
|
||||
'الحالة',
|
||||
'JoFotara UUID',
|
||||
'تاريخ الإنشاء',
|
||||
]) . "\n";
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$statusAr = match($inv['status']) {
|
||||
'extracted' => 'مستخرجة',
|
||||
'approved' => 'معتمدة',
|
||||
'submitted' => 'مقدمة لجوفتورة',
|
||||
'rejected' => 'مرفوضة',
|
||||
default => $inv['status']
|
||||
};
|
||||
|
||||
$row = [
|
||||
'"' . str_replace('"', '""', $inv['invoice_number'] ?? '') . '"',
|
||||
$inv['invoice_date'] ?? '',
|
||||
'"' . str_replace('"', '""', $dec($inv['company_name_raw'] ?? '')) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['supplier_name'])) . '"',
|
||||
'"' . $dec($inv['supplier_tin']) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['supplier_address'])) . '"',
|
||||
'"' . str_replace('"', '""', $dec($inv['buyer_name'])) . '"',
|
||||
'"' . $dec($inv['buyer_tin']) . '"',
|
||||
$inv['invoice_type'] ?? 'cash',
|
||||
$inv['subtotal'] ?? '0',
|
||||
$inv['discount_total'] ?? '0',
|
||||
$inv['tax_amount'] ?? '0',
|
||||
$inv['grand_total'] ?? '0',
|
||||
$inv['currency_code'] ?? 'JOD',
|
||||
$statusAr,
|
||||
$inv['jofotara_uuid'] ?? '',
|
||||
$inv['created_at'] ?? '',
|
||||
];
|
||||
|
||||
$output .= implode(',', $row) . "\n";
|
||||
}
|
||||
|
||||
// Send as download
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="musadaq_invoices_' . date('Y-m-d') . '.csv"');
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
echo $output;
|
||||
exit;
|
||||
116
app/modules_app/invoices/update.php
Normal file
116
app/modules_app/invoices/update.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Update Invoice (Before Approval Only)
|
||||
* POST /v1/invoices/update
|
||||
* Allows editing extracted data before final approval.
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Core\AuditLogger;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$data = input();
|
||||
|
||||
$id = $data['id'] ?? null;
|
||||
if (!$id) json_error('معرّف الفاتورة مطلوب', 422);
|
||||
|
||||
$db = Database::getInstance();
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
|
||||
// 1. Fetch & verify access
|
||||
$query = $role === 'super_admin'
|
||||
? "SELECT * FROM invoices WHERE id = ?"
|
||||
: "SELECT * FROM invoices WHERE id = ? AND tenant_id = ?";
|
||||
$params = $role === 'super_admin' ? [$id] : [$id, $tenantId];
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$invoice = $stmt->fetch();
|
||||
|
||||
if (!$invoice) json_error('الفاتورة غير موجودة', 404);
|
||||
|
||||
// 2. Only allow editing extracted (not yet approved) invoices
|
||||
if (!in_array($invoice['status'], ['extracted', 'pending'])) {
|
||||
json_error('لا يمكن تعديل الفاتورة بعد اعتمادها', 403);
|
||||
}
|
||||
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
// 3. Update main invoice fields
|
||||
$fields = [];
|
||||
$values = [];
|
||||
|
||||
$plainFields = ['invoice_number', 'invoice_date', 'invoice_type', 'invoice_category',
|
||||
'subtotal', 'tax_amount', 'discount_total', 'grand_total', 'currency_code'];
|
||||
|
||||
foreach ($plainFields as $f) {
|
||||
if (isset($data[$f])) {
|
||||
$fields[] = "$f = ?";
|
||||
$values[] = $data[$f];
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypted fields
|
||||
$encryptedFields = [
|
||||
'supplier_name' => 'supplier_name',
|
||||
'supplier_tin' => 'supplier_tin',
|
||||
'supplier_address' => 'supplier_address',
|
||||
'buyer_name' => 'buyer_name',
|
||||
'buyer_tin' => 'buyer_tin',
|
||||
'buyer_national_id' => 'buyer_national_id',
|
||||
];
|
||||
|
||||
foreach ($encryptedFields as $key => $column) {
|
||||
if (isset($data[$key])) {
|
||||
$fields[] = "$column = ?";
|
||||
$values[] = !empty($data[$key]) ? Encryption::encrypt($data[$key]) : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fields)) {
|
||||
$fields[] = 'updated_at = NOW()';
|
||||
$values[] = $id;
|
||||
$sql = "UPDATE invoices SET " . implode(', ', $fields) . " WHERE id = ?";
|
||||
$db->prepare($sql)->execute($values);
|
||||
}
|
||||
|
||||
// 4. Update line items (if provided)
|
||||
if (isset($data['items']) && is_array($data['items'])) {
|
||||
// Delete old lines
|
||||
$db->prepare("DELETE FROM invoice_lines WHERE invoice_id = ?")->execute([$id]);
|
||||
|
||||
// Insert new lines
|
||||
$lineStmt = $db->prepare(
|
||||
"INSERT INTO invoice_lines (id, invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
foreach ($data['items'] as $idx => $item) {
|
||||
$lineStmt->execute([
|
||||
Database::generateUuid(),
|
||||
$id,
|
||||
$item['line_number'] ?? ($idx + 1),
|
||||
$item['description'] ?? '',
|
||||
$item['quantity'] ?? 1,
|
||||
$item['unit_price'] ?? 0,
|
||||
$item['tax_rate'] ?? 0,
|
||||
$item['line_total'] ?? 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
AuditLogger::log('invoice.updated', 'invoice', $id, null, [
|
||||
'fields_updated' => array_keys($data),
|
||||
], $decoded);
|
||||
|
||||
json_success(null, 'تم تحديث بيانات الفاتورة بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
error_log("Invoice Update Error: " . $e->getMessage());
|
||||
json_error('فشل تحديث الفاتورة: ' . $e->getMessage(), 500);
|
||||
}
|
||||
154
app/modules_app/reports/tax_summary.php
Normal file
154
app/modules_app/reports/tax_summary.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* Monthly Tax Report API
|
||||
* GET /v1/reports/tax-summary
|
||||
* Returns monthly summary of tax, revenue, and invoice statistics
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$role = $decoded['role'];
|
||||
$companyId = $_GET['company_id'] ?? null;
|
||||
$month = $_GET['month'] ?? date('m');
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
|
||||
$where = ["MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?"];
|
||||
$params = [$month, $year];
|
||||
|
||||
if ($role !== 'super_admin') {
|
||||
$where[] = 'i.tenant_id = ?';
|
||||
$params[] = $tenantId;
|
||||
}
|
||||
|
||||
if ($companyId) {
|
||||
$where[] = 'i.company_id = ?';
|
||||
$params[] = $companyId;
|
||||
}
|
||||
|
||||
$whereClause = 'WHERE ' . implode(' AND ', $where);
|
||||
|
||||
// 1. Main aggregation
|
||||
$stmt = $db->prepare("
|
||||
SELECT
|
||||
COUNT(*) as total_invoices,
|
||||
SUM(CASE WHEN status = 'approved' OR status = 'submitted' THEN 1 ELSE 0 END) as approved_count,
|
||||
SUM(CASE WHEN status = 'extracted' THEN 1 ELSE 0 END) as pending_count,
|
||||
SUM(CASE WHEN status = 'submitted' THEN 1 ELSE 0 END) as submitted_count,
|
||||
COALESCE(SUM(subtotal), 0) as total_subtotal,
|
||||
COALESCE(SUM(tax_amount), 0) as total_tax,
|
||||
COALESCE(SUM(discount_total), 0) as total_discount,
|
||||
COALESCE(SUM(grand_total), 0) as total_grand,
|
||||
COALESCE(AVG(grand_total), 0) as avg_invoice_amount,
|
||||
COALESCE(MAX(grand_total), 0) as max_invoice_amount,
|
||||
COALESCE(MIN(grand_total), 0) as min_invoice_amount
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$summary = $stmt->fetch();
|
||||
|
||||
// 2. Daily breakdown for chart
|
||||
$stmtDaily = $db->prepare("
|
||||
SELECT
|
||||
DAY(i.invoice_date) as day_num,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(grand_total), 0) as daily_total,
|
||||
COALESCE(SUM(tax_amount), 0) as daily_tax
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY DAY(i.invoice_date)
|
||||
ORDER BY day_num
|
||||
");
|
||||
$stmtDaily->execute($params);
|
||||
$dailyBreakdown = $stmtDaily->fetchAll();
|
||||
|
||||
// 3. Invoice type breakdown
|
||||
$stmtType = $db->prepare("
|
||||
SELECT
|
||||
invoice_type,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(grand_total), 0) as total
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY invoice_type
|
||||
");
|
||||
$stmtType->execute($params);
|
||||
$typeBreakdown = $stmtType->fetchAll();
|
||||
|
||||
// 4. Top 5 suppliers
|
||||
$stmtSuppliers = $db->prepare("
|
||||
SELECT
|
||||
supplier_name,
|
||||
COUNT(*) as invoice_count,
|
||||
COALESCE(SUM(grand_total), 0) as total_amount
|
||||
FROM invoices i
|
||||
$whereClause
|
||||
GROUP BY supplier_name
|
||||
ORDER BY total_amount DESC
|
||||
LIMIT 5
|
||||
");
|
||||
$stmtSuppliers->execute($params);
|
||||
$topSuppliers = $stmtSuppliers->fetchAll();
|
||||
|
||||
// Decrypt supplier names
|
||||
foreach ($topSuppliers as &$s) {
|
||||
$decrypted = \App\Core\Encryption::decrypt($s['supplier_name']);
|
||||
$s['supplier_name'] = ($decrypted !== false && $decrypted !== null) ? $decrypted : $s['supplier_name'];
|
||||
}
|
||||
unset($s);
|
||||
|
||||
// 5. Comparison with previous month
|
||||
$prevMonth = $month == 1 ? 12 : $month - 1;
|
||||
$prevYear = $month == 1 ? $year - 1 : $year;
|
||||
|
||||
$prevWhere = str_replace(
|
||||
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
|
||||
"MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?",
|
||||
implode(' AND ', $where)
|
||||
);
|
||||
|
||||
$prevParams = [$prevMonth, $prevYear];
|
||||
if ($role !== 'super_admin') $prevParams[] = $tenantId;
|
||||
if ($companyId) $prevParams[] = $companyId;
|
||||
|
||||
$stmtPrev = $db->prepare("
|
||||
SELECT
|
||||
COUNT(*) as total_invoices,
|
||||
COALESCE(SUM(grand_total), 0) as total_grand,
|
||||
COALESCE(SUM(tax_amount), 0) as total_tax
|
||||
FROM invoices i
|
||||
WHERE MONTH(i.invoice_date) = ? AND YEAR(i.invoice_date) = ?
|
||||
" . ($role !== 'super_admin' ? " AND i.tenant_id = ?" : "")
|
||||
. ($companyId ? " AND i.company_id = ?" : "")
|
||||
);
|
||||
$stmtPrev->execute($prevParams);
|
||||
$previous = $stmtPrev->fetch();
|
||||
|
||||
// Calculate growth
|
||||
$growth = [
|
||||
'invoices' => $previous['total_invoices'] > 0
|
||||
? round((($summary['total_invoices'] - $previous['total_invoices']) / $previous['total_invoices']) * 100, 1)
|
||||
: 0,
|
||||
'revenue' => $previous['total_grand'] > 0
|
||||
? round((($summary['total_grand'] - $previous['total_grand']) / $previous['total_grand']) * 100, 1)
|
||||
: 0,
|
||||
'tax' => $previous['total_tax'] > 0
|
||||
? round((($summary['total_tax'] - $previous['total_tax']) / $previous['total_tax']) * 100, 1)
|
||||
: 0,
|
||||
];
|
||||
|
||||
json_success([
|
||||
'month' => (int)$month,
|
||||
'year' => (int)$year,
|
||||
'summary' => $summary,
|
||||
'daily_breakdown' => $dailyBreakdown,
|
||||
'type_breakdown' => $typeBreakdown,
|
||||
'top_suppliers' => $topSuppliers,
|
||||
'previous_month' => $previous,
|
||||
'growth' => $growth,
|
||||
], 'تقرير ضريبة المبيعات الشهري');
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,9 @@ $routes = [
|
||||
'v1/invoices/upload' => ['POST', 'invoices/upload.php'],
|
||||
'v1/invoices/download_xml' => ['GET', 'invoices/download_xml.php'],
|
||||
'v1/invoices/submit-jofotara' => ['POST', 'invoices/submit_jofotara.php'],
|
||||
'v1/invoices/update' => ['POST', 'invoices/update.php'],
|
||||
'v1/invoices/export' => ['GET', 'invoices/export.php'],
|
||||
'v1/reports/tax-summary' => ['GET', 'reports/tax_summary.php'],
|
||||
'v1/companies/stats' => ['GET', 'companies/stats.php'],
|
||||
'v1/companies/connect' => ['POST', 'companies/connect_jofotara.php'],
|
||||
'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'],
|
||||
|
||||
Reference in New Issue
Block a user