290 lines
12 KiB
Dart
290 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/ai_usage_controller.dart';
|
|
|
|
class AiUsageView extends StatelessWidget {
|
|
const AiUsageView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final controller = Get.put(AiUsageController());
|
|
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(fontWeight: FontWeight.bold)),
|
|
centerTitle: true,
|
|
backgroundColor: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
body: Obx(() {
|
|
if (controller.isLoading.value) {
|
|
return const Center(child: CircularProgressIndicator(color: Color(0xFF0F4C81)));
|
|
}
|
|
|
|
final overall = controller.overall;
|
|
final today = controller.today;
|
|
final month = controller.thisMonth;
|
|
final daily = controller.dailyBreakdown;
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: () async => controller.fetchUsage(),
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Hero Stats
|
|
_buildHeroCard(overall, isDark),
|
|
const SizedBox(height: 16),
|
|
|
|
// Today vs This Month
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildPeriodCard('اليوم', today, Icons.today, const Color(0xFF3B82F6), isDark)),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: _buildPeriodCard('هذا الشهر', month, Icons.calendar_month, const Color(0xFF6366F1), isDark)),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Cost per invoice
|
|
_buildCostPerInvoiceCard(overall, isDark),
|
|
const SizedBox(height: 20),
|
|
|
|
// Daily breakdown
|
|
if (daily.isNotEmpty) ...[
|
|
Text('التفصيل اليومي', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87)),
|
|
const SizedBox(height: 12),
|
|
...daily.take(15).map((d) => _buildDailyRow(d, isDark)),
|
|
],
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeroCard(Map<String, dynamic> overall, bool isDark) {
|
|
final totalRequests = overall['total_requests'] ?? 0;
|
|
final totalTokens = overall['total_tokens'] ?? 0;
|
|
final totalCostJod = double.tryParse(overall['total_cost_jod']?.toString() ?? '0') ?? 0.0;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF0F4C81), Color(0xFF1A6BB5), Color(0xFF6366F1)],
|
|
begin: Alignment.topLeft, end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(color: const Color(0xFF0F4C81).withValues(alpha: 0.3), blurRadius: 15, offset: const Offset(0, 6)),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Icon(Icons.psychology, size: 40, color: Color(0xFFD4AF37)),
|
|
const SizedBox(height: 8),
|
|
const Text('إجمالي استخدام AI', style: TextStyle(fontSize: 14, color: Colors.white70)),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_heroStat('الطلبات', '$totalRequests', Icons.receipt_long),
|
|
Container(width: 1, height: 40, color: Colors.white24),
|
|
_heroStat('التوكنات', _formatNumber(totalTokens), Icons.token),
|
|
Container(width: 1, height: 40, color: Colors.white24),
|
|
_heroStat('التكلفة', '${totalCostJod.toStringAsFixed(3)} د.أ', Icons.attach_money),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _heroStat(String label, String value, IconData icon) {
|
|
return Column(
|
|
children: [
|
|
Icon(icon, size: 20, color: const Color(0xFFD4AF37)),
|
|
const SizedBox(height: 6),
|
|
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w900, color: Colors.white, fontFamily: 'monospace')),
|
|
const SizedBox(height: 2),
|
|
Text(label, style: const TextStyle(fontSize: 11, color: Colors.white60)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPeriodCard(String title, Map<String, dynamic> data, IconData icon, Color color, bool isDark) {
|
|
final requests = data['requests'] ?? 0;
|
|
final tokens = data['tokens'] ?? 0;
|
|
final costJod = double.tryParse(data['cost_jod']?.toString() ?? '0') ?? 0.0;
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: color.withValues(alpha: 0.2)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, size: 18, color: color),
|
|
const SizedBox(width: 6),
|
|
Text(title, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color)),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text('$requests', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: isDark ? Colors.white : Colors.black87, fontFamily: 'monospace')),
|
|
Text('فاتورة', style: TextStyle(fontSize: 11, color: isDark ? Colors.white38 : Colors.grey)),
|
|
const SizedBox(height: 8),
|
|
Text('${_formatNumber(tokens)} توكن', style: TextStyle(fontSize: 12, color: isDark ? Colors.white54 : Colors.black54)),
|
|
Text('${costJod.toStringAsFixed(4)} د.أ', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: color)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCostPerInvoiceCard(Map<String, dynamic> overall, bool isDark) {
|
|
final avgCost = double.tryParse(overall['avg_cost_per_invoice_jod']?.toString() ?? '0') ?? 0.0;
|
|
final avgTokens = double.tryParse(overall['avg_tokens_per_invoice']?.toString() ?? '0') ?? 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: const Color(0xFF10B981).withValues(alpha: 0.3)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.analytics, size: 20, color: Color(0xFF10B981)),
|
|
SizedBox(width: 8),
|
|
Text('تحليل التكلفة لكل فاتورة', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF10B981).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text('${avgCost.toStringAsFixed(5)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: Color(0xFF10B981), fontFamily: 'monospace')),
|
|
const Text('د.أ / فاتورة', style: TextStyle(fontSize: 11, color: Colors.grey)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF3B82F6).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text('${avgTokens.toStringAsFixed(0)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: Color(0xFF3B82F6), fontFamily: 'monospace')),
|
|
const Text('توكن / فاتورة', style: TextStyle(fontSize: 11, color: Colors.grey)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFD4AF37).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Row(
|
|
children: [
|
|
Icon(Icons.lightbulb, size: 16, color: Color(0xFFD4AF37)),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'تكلفة AI أقل من ربع قرش لكل فاتورة — هامش ربح 99%+',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFFD4AF37), fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDailyRow(dynamic d, bool isDark) {
|
|
final date = d['date'] ?? '';
|
|
final requests = d['requests'] ?? 0;
|
|
final tokens = d['tokens'] ?? 0;
|
|
final costJod = double.tryParse(d['cost_jod']?.toString() ?? '0') ?? 0.0;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade200),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(date, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: isDark ? Colors.white70 : Colors.black87, fontFamily: 'monospace')),
|
|
const Spacer(),
|
|
_dailyChip('$requests', Icons.receipt, const Color(0xFF3B82F6)),
|
|
const SizedBox(width: 8),
|
|
_dailyChip(_formatNumber(tokens), Icons.token, const Color(0xFF6366F1)),
|
|
const SizedBox(width: 8),
|
|
_dailyChip('${costJod.toStringAsFixed(3)}', Icons.attach_money, const Color(0xFF10B981)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _dailyChip(String value, IconData icon, Color color) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.08),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 12, color: color),
|
|
const SizedBox(width: 3),
|
|
Text(value, style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color, fontFamily: 'monospace')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatNumber(dynamic num) {
|
|
final n = (num is int) ? num : int.tryParse(num.toString()) ?? 0;
|
|
if (n >= 1000000) return '${(n / 1000000).toStringAsFixed(1)}M';
|
|
if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}K';
|
|
return '$n';
|
|
}
|
|
}
|