Update: 2026-05-08 01:15:44

This commit is contained in:
Hamza-Ayed
2026-05-08 01:15:44 +03:00
parent 1a6ed52a52
commit 928e8e27e3
10 changed files with 991 additions and 4 deletions

View File

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

View 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);
}
}