Update: 2026-05-08 02:22:45
This commit is contained in:
@@ -24,6 +24,7 @@ import '../../features/onboarding/views/onboarding_view.dart';
|
|||||||
import '../../features/reports/views/tax_report_view.dart';
|
import '../../features/reports/views/tax_report_view.dart';
|
||||||
import '../../features/audit/views/audit_log_view.dart';
|
import '../../features/audit/views/audit_log_view.dart';
|
||||||
import '../../features/referral/views/referral_view.dart';
|
import '../../features/referral/views/referral_view.dart';
|
||||||
|
import '../../features/ai_usage/views/ai_usage_view.dart';
|
||||||
|
|
||||||
part 'app_routes.dart';
|
part 'app_routes.dart';
|
||||||
|
|
||||||
@@ -166,5 +167,9 @@ class AppPages {
|
|||||||
name: AppRoutes.REFERRAL,
|
name: AppRoutes.REFERRAL,
|
||||||
page: () => const ReferralView(),
|
page: () => const ReferralView(),
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.AI_USAGE,
|
||||||
|
page: () => const AiUsageView(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ abstract class AppRoutes {
|
|||||||
static const TAX_REPORT = '/tax-report';
|
static const TAX_REPORT = '/tax-report';
|
||||||
static const AUDIT_LOG = '/audit-log';
|
static const AUDIT_LOG = '/audit-log';
|
||||||
static const REFERRAL = '/referral';
|
static const REFERRAL = '/referral';
|
||||||
|
static const AI_USAGE = '/ai-usage';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../core/network/dio_client.dart';
|
||||||
|
import '../../../core/utils/logger.dart';
|
||||||
|
|
||||||
|
class AiUsageController extends GetxController {
|
||||||
|
var isLoading = true.obs;
|
||||||
|
var data = Rxn<Map<String, dynamic>>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> get overall => Map<String, dynamic>.from(data.value?['overall'] ?? {});
|
||||||
|
Map<String, dynamic> get today => Map<String, dynamic>.from(data.value?['today'] ?? {});
|
||||||
|
Map<String, dynamic> get thisMonth => Map<String, dynamic>.from(data.value?['this_month'] ?? {});
|
||||||
|
List get dailyBreakdown => data.value?['daily_breakdown'] ?? [];
|
||||||
|
|
||||||
|
Future<void> fetchUsage() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
final res = await DioClient().client.get('dashboard/ai-usage');
|
||||||
|
if (res.data['success'] == true) {
|
||||||
|
data.value = Map<String, dynamic>.from(res.data['data']);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.error('Failed to fetch AI usage', e);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
289
musadaq-app/lib/features/ai_usage/views/ai_usage_view.dart
Normal file
289
musadaq-app/lib/features/ai_usage/views/ai_usage_view.dart
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
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 = (overall['total_cost_jod'] ?? 0).toDouble();
|
||||||
|
|
||||||
|
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 = (data['cost_jod'] ?? 0).toDouble();
|
||||||
|
|
||||||
|
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 = (overall['avg_cost_per_invoice_jod'] ?? 0).toDouble();
|
||||||
|
final avgTokens = (overall['avg_tokens_per_invoice'] ?? 0).toDouble();
|
||||||
|
|
||||||
|
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 = (d['cost_jod'] ?? 0).toDouble();
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -258,6 +258,14 @@ class DashboardView extends GetView<DashboardController> {
|
|||||||
isDark,
|
isDark,
|
||||||
() => Get.toNamed(AppRoutes.REFERRAL),
|
() => Get.toNamed(AppRoutes.REFERRAL),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildAdminActionCard(
|
||||||
|
'استهلاك AI',
|
||||||
|
Icons.psychology,
|
||||||
|
const Color(0xFF6366F1),
|
||||||
|
isDark,
|
||||||
|
() => Get.toNamed(AppRoutes.AI_USAGE),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../controllers/referral_controller.dart';
|
import '../controllers/referral_controller.dart';
|
||||||
import '../../../core/utils/app_snackbar.dart';
|
import '../../../core/utils/app_snackbar.dart';
|
||||||
|
|
||||||
@@ -29,23 +30,14 @@ class ReferralView extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Hero Banner
|
|
||||||
_buildHeroBanner(controller, isDark),
|
_buildHeroBanner(controller, isDark),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Stats Cards
|
|
||||||
_buildStatsRow(controller.stats, isDark),
|
_buildStatsRow(controller.stats, isDark),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Rewards Info
|
|
||||||
_buildRewardsCard(controller.rewardRules, isDark),
|
_buildRewardsCard(controller.rewardRules, isDark),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// How it works
|
|
||||||
_buildHowItWorks(isDark),
|
_buildHowItWorks(isDark),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Recent referrals
|
|
||||||
if (controller.recent.isNotEmpty) ...[
|
if (controller.recent.isNotEmpty) ...[
|
||||||
_buildRecentReferrals(controller.recent, isDark),
|
_buildRecentReferrals(controller.recent, isDark),
|
||||||
],
|
],
|
||||||
@@ -92,53 +84,109 @@ class ReferralView extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
controller.code,
|
child: Text(
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 3, fontFamily: 'monospace'),
|
controller.code,
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(width: 12),
|
style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 4, fontFamily: 'monospace'),
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: controller.code));
|
|
||||||
AppSnackbar.showSuccess('تم النسخ', 'تم نسخ رمز الإحالة');
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFD4AF37),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.copy, size: 18, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Share link button
|
// WhatsApp Share Button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () => _shareViaWhatsApp(controller.code, controller.link),
|
||||||
Clipboard.setData(ClipboardData(text: controller.link));
|
icon: const Icon(Icons.chat, size: 20),
|
||||||
AppSnackbar.showSuccess('تم النسخ', 'تم نسخ رابط الإحالة');
|
label: const Text('أرسل عبر واتساب', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
|
||||||
},
|
|
||||||
icon: const Icon(Icons.share, size: 18),
|
|
||||||
label: const Text('نسخ رابط الدعوة', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFD4AF37),
|
backgroundColor: const Color(0xFF25D366),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// Copy Code + Copy Link Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: controller.code));
|
||||||
|
AppSnackbar.showSuccess('تم النسخ', 'تم نسخ رمز الإحالة: ${controller.code}');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy, size: 16),
|
||||||
|
label: const Text('نسخ الرمز', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white.withValues(alpha: 0.2),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: controller.link));
|
||||||
|
AppSnackbar.showSuccess('تم النسخ', 'تم نسخ رابط الدعوة');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.link, size: 16),
|
||||||
|
label: const Text('نسخ الرابط', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFD4AF37),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _shareViaWhatsApp(String code, String link) async {
|
||||||
|
final message = Uri.encodeComponent(
|
||||||
|
'مرحباً 👋\n\n'
|
||||||
|
'أدعوك لتجربة تطبيق *مُصادَق* — أذكى نظام فوترة إلكتروني في الأردن 🇯🇴\n\n'
|
||||||
|
'✅ استخراج الفواتير بالذكاء الاصطناعي\n'
|
||||||
|
'✅ ربط مباشر مع جوفوترة\n'
|
||||||
|
'✅ تقارير ضريبية جاهزة\n\n'
|
||||||
|
'🎁 استخدم رمز الدعوة: *$code*\n'
|
||||||
|
'واحصل على شهر مجاني!\n\n'
|
||||||
|
'🔗 $link'
|
||||||
|
);
|
||||||
|
|
||||||
|
final whatsappUrl = Uri.parse('https://wa.me/?text=$message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await canLaunchUrl(whatsappUrl)) {
|
||||||
|
await launchUrl(whatsappUrl, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
// Fallback — copy to clipboard
|
||||||
|
Clipboard.setData(ClipboardData(text: Uri.decodeComponent(message)));
|
||||||
|
AppSnackbar.showInfo('واتساب غير متوفر', 'تم نسخ الرسالة — ألصقها يدوياً');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Clipboard.setData(ClipboardData(text: Uri.decodeComponent(message)));
|
||||||
|
AppSnackbar.showInfo('واتساب غير متوفر', 'تم نسخ الرسالة — ألصقها يدوياً');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildStatsRow(Map<String, dynamic> stats, bool isDark) {
|
Widget _buildStatsRow(Map<String, dynamic> stats, bool isDark) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -236,7 +284,7 @@ class ReferralView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text('كيف يعمل؟', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87)),
|
Text('كيف يعمل؟', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87)),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_stepItem('1', 'شارك رمز الإحالة مع زميلك', Icons.share, const Color(0xFF3B82F6), isDark),
|
_stepItem('1', 'أرسل الرمز لصديقك عبر واتساب', Icons.chat, const Color(0xFF25D366), isDark),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_stepItem('2', 'يسجّل في مُصادَق بالرمز', Icons.person_add, const Color(0xFF6366F1), isDark),
|
_stepItem('2', 'يسجّل في مُصادَق بالرمز', Icons.person_add, const Color(0xFF6366F1), isDark),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|||||||
@@ -194,24 +194,43 @@ class UsersManagementView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// Toggle active
|
// Only show actions for non-super_admin users
|
||||||
IconButton(
|
if (role != 'super_admin') ...[
|
||||||
icon: Icon(isActive ? Icons.toggle_on : Icons.toggle_off, color: isActive ? const Color(0xFF10B981) : Colors.grey, size: 28),
|
// Toggle active
|
||||||
onPressed: () => controller.toggleUserActive(user['id'], !isActive),
|
IconButton(
|
||||||
tooltip: isActive ? 'تعطيل' : 'تفعيل',
|
icon: Icon(isActive ? Icons.toggle_on : Icons.toggle_off, color: isActive ? const Color(0xFF10B981) : Colors.grey, size: 28),
|
||||||
),
|
onPressed: () => controller.toggleUserActive(user['id'], !isActive),
|
||||||
// Edit
|
tooltip: isActive ? 'تعطيل' : 'تفعيل',
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.edit, size: 20, color: Color(0xFF0F4C81)),
|
// Edit
|
||||||
onPressed: () => _showEditDialog(context, user, controller),
|
IconButton(
|
||||||
tooltip: 'تعديل',
|
icon: const Icon(Icons.edit, size: 20, color: Color(0xFF0F4C81)),
|
||||||
),
|
onPressed: () => _showEditDialog(context, user, controller),
|
||||||
// Delete
|
tooltip: 'تعديل',
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.delete_outline, size: 20, color: Colors.red),
|
// Delete
|
||||||
onPressed: () => _confirmDelete(context, controller, user['id'], user['name'] ?? ''),
|
IconButton(
|
||||||
tooltip: 'حذف',
|
icon: const Icon(Icons.delete_outline, size: 20, color: Colors.red),
|
||||||
),
|
onPressed: () => _confirmDelete(context, controller, user['id'], user['name'] ?? ''),
|
||||||
|
tooltip: 'حذف',
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF6366F1).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.shield, size: 14, color: Color(0xFF6366F1)),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text('محمي', style: TextStyle(fontSize: 11, color: Color(0xFF6366F1), fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -748,26 +748,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.0.2"
|
version: "10.0.9"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.10"
|
version: "3.0.9"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -836,18 +836,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
matrix2d:
|
matrix2d:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -860,10 +860,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.16.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1401,10 +1401,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.4"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1421,6 +1421,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.20"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.4"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1429,6 +1453,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.3"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1465,10 +1497,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.1.4"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1550,5 +1582,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0-0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.32.0"
|
flutter: ">=3.32.0"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ dependencies:
|
|||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
lottie: ^3.1.0
|
lottie: ^3.1.0
|
||||||
share_plus: ^12.0.2
|
share_plus: ^12.0.2
|
||||||
|
url_launcher: ^6.2.5
|
||||||
|
|
||||||
# ─── Utilities ──────────────────────────────────────
|
# ─── Utilities ──────────────────────────────────────
|
||||||
uuid: ^4.3.3
|
uuid: ^4.3.3
|
||||||
|
|||||||
Reference in New Issue
Block a user