Update: 2026-05-08 01:15:44
This commit is contained in:
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