Update: 2026-05-08 00:26:39

This commit is contained in:
Hamza-Ayed
2026-05-08 00:26:40 +03:00
parent 51d1d42f75
commit 08e2a87c10
24 changed files with 1743 additions and 210 deletions

View File

@@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:dio/dio.dart';
import '../../../core/network/dio_client.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/app_snackbar.dart';
class CompanyStatsController extends GetxController {
final Dio _dio = DioClient().client;
var isLoading = false.obs;
var company = <String, dynamic>{}.obs;
var totals = <String, dynamic>{}.obs;
var monthly = <Map<String, dynamic>>[].obs;
final String companyId;
final String companyName;
CompanyStatsController({required this.companyId, required this.companyName});
@override
void onInit() {
super.onInit();
fetchStats();
}
Future<void> fetchStats() async {
try {
isLoading.value = true;
final response = await _dio.get('companies/stats?company_id=$companyId');
if (response.data['success'] == true) {
final data = response.data['data'];
company.value = Map<String, dynamic>.from(data['company'] ?? {});
totals.value = Map<String, dynamic>.from(data['totals'] ?? {});
monthly.value = List<Map<String, dynamic>>.from(data['monthly'] ?? []);
}
} catch (e) {
AppLogger.error('Company stats error', e);
AppSnackbar.showError('خطأ', 'فشل تحميل الإحصائيات');
} finally {
isLoading.value = false;
}
}
}

View File

@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/company_stats_controller.dart';
class CompanyStatsView extends StatelessWidget {
const CompanyStatsView({super.key});
@override
Widget build(BuildContext context) {
final args = Get.arguments as Map<String, dynamic>;
final controller = Get.put(
CompanyStatsController(
companyId: args['company_id'],
companyName: args['company_name'],
),
tag: args['company_id'],
);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(
title: Text(
'إحصائيات ${args['company_name']}',
style: const TextStyle(fontFamily: 'El Messiri', fontSize: 16),
overflow: TextOverflow.ellipsis,
),
centerTitle: true,
backgroundColor: const Color(0xFF0F4C81),
foregroundColor: Colors.white,
elevation: 0,
actions: [
Obx(() => controller.isLoading.value
? const Padding(
padding: EdgeInsets.all(14),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
),
)
: IconButton(
icon: const Icon(Icons.refresh),
onPressed: controller.fetchStats,
)),
],
),
body: Obx(() {
if (controller.isLoading.value && controller.totals.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (controller.totals.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bar_chart_outlined,
size: 80, color: Colors.grey.shade400),
const SizedBox(height: 16),
const Text('لا توجد بيانات بعد',
style: TextStyle(fontSize: 18, color: Colors.grey)),
],
),
);
}
final totals = controller.totals;
final monthly = controller.monthly;
return RefreshIndicator(
onRefresh: controller.fetchStats,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// ── Lifetime Totals ──────────────────────────────
_sectionTitle('الإحصائيات الإجمالية', isDark),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.6,
children: [
_statCard(
icon: Icons.receipt_long_rounded,
label: 'إجمالي الفواتير',
value: _fmt(totals['total_invoices']),
color: const Color(0xFF0F4C81),
isDark: isDark,
),
_statCard(
icon: Icons.check_circle_rounded,
label: 'فواتير معتمدة',
value: _fmt(totals['approved_count']),
color: const Color(0xFF10B981),
isDark: isDark,
),
_statCard(
icon: Icons.attach_money_rounded,
label: 'إجمالي المبيعات',
value: '${_fmtAmount(totals['total_amount'])} د.أ',
color: const Color(0xFFD4AF37),
isDark: isDark,
),
_statCard(
icon: Icons.percent_rounded,
label: 'إجمالي الضريبة',
value: '${_fmtAmount(totals['total_tax'])} د.أ',
color: const Color(0xFF8B5CF6),
isDark: isDark,
),
],
),
// Approval Rate
const SizedBox(height: 20),
_buildApprovalRateCard(totals, isDark),
// ── Monthly Breakdown ────────────────────────────
if (monthly.isNotEmpty) ...[
const SizedBox(height: 24),
_sectionTitle('التفاصيل الشهرية (آخر 12 شهر)', isDark),
const SizedBox(height: 12),
...monthly.map((m) => _buildMonthRow(m, isDark)),
],
],
),
);
}),
);
}
Widget _sectionTitle(String title, bool isDark) {
return Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFF0F4C81),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
);
}
Widget _statCard({
required IconData icon,
required String label,
required String value,
required Color color,
required bool isDark,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
borderRadius: BorderRadius.circular(14),
border:
Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
boxShadow: isDark
? []
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(width: 8),
Expanded(
child: Text(label,
style: TextStyle(
fontSize: 11,
color: isDark ? Colors.white54 : Colors.grey.shade600),
overflow: TextOverflow.ellipsis),
),
],
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
Widget _buildApprovalRateCard(Map<String, dynamic> totals, bool isDark) {
final total =
int.tryParse(totals['total_invoices']?.toString() ?? '0') ?? 0;
final approved =
int.tryParse(totals['approved_count']?.toString() ?? '0') ?? 0;
final rate = total > 0 ? (approved / total) : 0.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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('نسبة القبول',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
Text(
'${(rate * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: rate > 0.8
? const Color(0xFF10B981)
: rate > 0.5
? const Color(0xFFD4AF37)
: const Color(0xFFDC2626),
),
),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: rate,
minHeight: 8,
color: rate > 0.8
? const Color(0xFF10B981)
: rate > 0.5
? const Color(0xFFD4AF37)
: const Color(0xFFDC2626),
backgroundColor:
isDark ? Colors.white10 : const Color(0xFFE2E8F0),
),
),
const SizedBox(height: 8),
Text(
'$approved معتمدة من أصل $total فاتورة',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white54 : Colors.grey.shade600),
),
],
),
);
}
Widget _buildMonthRow(Map<String, dynamic> m, bool isDark) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
),
child: Row(
children: [
// Month label
Container(
width: 56,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
decoration: BoxDecoration(
color: const Color(0xFF0F4C81).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_formatMonth(m['month']?.toString() ?? ''),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Color(0xFF0F4C81),
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 12),
// Stats
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.receipt_long_rounded,
size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text('${_fmt(m['total_invoices'])} فاتورة',
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600)),
const SizedBox(width: 12),
const Icon(Icons.check_circle_outline,
size: 14, color: Color(0xFF10B981)),
const SizedBox(width: 4),
Text('${_fmt(m['approved_count'])} معتمدة',
style: const TextStyle(
fontSize: 12, color: Color(0xFF10B981))),
],
),
const SizedBox(height: 4),
Row(
children: [
Text(
'${_fmtAmount(m['total_amount'])} د.أ مبيعات',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white70 : Colors.black87),
),
const SizedBox(width: 12),
Text(
'${_fmtAmount(m['total_tax'])} د.أ ضريبة',
style: const TextStyle(
fontSize: 12, color: Color(0xFF8B5CF6)),
),
],
),
],
),
),
],
),
);
}
String _fmt(dynamic v) {
if (v == null) return '0';
return int.tryParse(v.toString())?.toString() ?? v.toString();
}
String _fmtAmount(dynamic v) {
if (v == null) return '0.000';
final d = double.tryParse(v.toString()) ?? 0;
return d.toStringAsFixed(3);
}
String _formatMonth(String m) {
if (m.isEmpty) return m;
final parts = m.split('-');
if (parts.length < 2) return m;
final months = [
'',
'يناير',
'فبراير',
'مارس',
'أبريل',
'مايو',
'يونيو',
'يوليو',
'أغسطس',
'سبتمبر',
'أكتوبر',
'نوفمبر',
'ديسمبر'
];
final monthIndex = int.tryParse(parts[1]) ?? 0;
final monthName = monthIndex > 0 && monthIndex < months.length
? months[monthIndex]
: parts[1];
return '$monthName\n${parts[0]}';
}
}