Update: 2026-05-08 14:05:50

This commit is contained in:
Hamza-Ayed
2026-05-08 14:05:50 +03:00
parent cfc330e291
commit 155c2d0fc0
13 changed files with 709 additions and 22 deletions

View File

@@ -23,10 +23,22 @@ class AuthController extends GetxController {
return;
}
isLoading.value = true;
phone.value = phoneNumber;
// Normalize phone number
String normalizedPhone = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
if (normalizedPhone.startsWith('+')) {
normalizedPhone = normalizedPhone.substring(1);
}
if (normalizedPhone.startsWith('07')) {
normalizedPhone = '962' + normalizedPhone.substring(1);
} else if (normalizedPhone.startsWith('7')) {
normalizedPhone = '962' + normalizedPhone;
}
phone.value = normalizedPhone;
final response = await _dio.post('auth/mobile/request-otp', data: {
'phone': phoneNumber,
'phone': normalizedPhone,
});
if (response.statusCode == 200) {

View File

@@ -57,4 +57,27 @@ class CompaniesManagementController extends GetxController {
AppSnackbar.showError('خطأ', 'تعذر حذف الشركة');
}
}
Future<void> connectJoFotara(String id, String clientId, String secretKey, String sequence) async {
try {
isLoading.value = true;
final response = await _dio.post('companies/connect_jofotara', data: {
'id': id,
'client_id': clientId,
'secret_key': secretKey,
'income_source_sequence': sequence,
});
if (response.data['success'] == true) {
await fetchCompanies();
AppSnackbar.showSuccess('نجاح', 'تم ربط الشركة بجوفوترا بنجاح');
} else {
AppSnackbar.showError('خطأ', response.data['message'] ?? 'فشل الربط');
}
} catch (e) {
AppLogger.error('Failed to connect jofotara', e);
AppSnackbar.showError('خطأ', 'تعذر ربط جوفوترا');
} finally {
isLoading.value = false;
}
}
}

View File

@@ -137,6 +137,9 @@ class CompaniesManagementView extends StatelessWidget {
'company_name': company['name'],
});
break;
case 'link_jofotara':
_showLinkJoFotaraDialog(context, company, controller);
break;
case 'delete':
_confirmDelete(context, controller, company['id'], company['name'] ?? '');
break;
@@ -145,6 +148,7 @@ class CompaniesManagementView extends StatelessWidget {
itemBuilder: (context) => [
const PopupMenuItem(value: 'edit', child: Row(children: [Icon(Icons.edit, size: 18), SizedBox(width: 8), Text('تعديل البيانات')])),
const PopupMenuItem(value: 'stats', child: Row(children: [Icon(Icons.bar_chart, size: 18), SizedBox(width: 8), Text('الإحصائيات')])),
const PopupMenuItem(value: 'link_jofotara', child: Row(children: [Icon(Icons.link, size: 18, color: Color(0xFF6366F1)), SizedBox(width: 8), Text('ربط جوفوترا', style: TextStyle(color: Color(0xFF6366F1)))])),
const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, size: 18, color: Colors.red), SizedBox(width: 8), Text('حذف', style: TextStyle(color: Colors.red))])),
],
),
@@ -162,7 +166,7 @@ class CompaniesManagementView extends StatelessWidget {
if (company['address'] != null && company['address'].toString().isNotEmpty)
_chip(Icons.location_on, company['address'], Colors.orange),
if (company['is_jofotara_linked'] == 1)
_chip(Icons.verified, 'جوفتورة', const Color(0xFF6366F1)),
_chip(Icons.verified, 'جوفوترا', const Color(0xFF6366F1)),
],
),
@@ -298,4 +302,43 @@ class CompaniesManagementView extends StatelessWidget {
},
);
}
void _showLinkJoFotaraDialog(BuildContext context, Map<String, dynamic> company, CompaniesManagementController controller) {
final clientIdC = TextEditingController();
final secretKeyC = TextEditingController();
final sequenceC = TextEditingController();
Get.dialog(
AlertDialog(
title: const Text('ربط منصة جوفوترا', textAlign: TextAlign.center, style: TextStyle(color: Color(0xFF6366F1))),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('أدخل بيانات الربط الخاصة بالشركة:', style: TextStyle(fontSize: 13, color: Colors.grey), textAlign: TextAlign.center),
const SizedBox(height: 16),
_editField('Client ID', clientIdC, Icons.vpn_key),
_editField('Secret Key', secretKeyC, Icons.lock),
_editField('Income Source Sequence (اختياري)', sequenceC, Icons.format_list_numbered),
],
),
),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('إلغاء')),
ElevatedButton(
onPressed: () {
if (clientIdC.text.isEmpty || secretKeyC.text.isEmpty) {
Get.snackbar('تنبيه', 'يجب إدخال الـ Client ID و Secret Key');
return;
}
Get.back();
controller.connectJoFotara(company['id'], clientIdC.text, secretKeyC.text, sequenceC.text);
},
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF6366F1)),
child: const Text('ربط الآن', style: TextStyle(color: Colors.white)),
),
],
),
);
}
}

View File

@@ -25,6 +25,7 @@ class DashboardController extends GetxController {
var isLoading = true.obs;
var stats = {}.obs;
var gamification = {}.obs;
var recentActivities = [].obs;
var userRole = ''.obs;
@@ -67,6 +68,12 @@ class DashboardController extends GetxController {
}
}
// Fetch Gamification
final gamificationResponse = await _dio.get('gamification/profile');
if (gamificationResponse.data['success'] == true) {
gamification.value = gamificationResponse.data['data'];
}
// Fetch Recent Activity
final activityResponse = await _dio.get('dashboard/recent-activity');
if (activityResponse.data['success'] == true) {

View File

@@ -74,6 +74,10 @@ class DashboardView extends GetView<DashboardController> {
const SizedBox(height: 24),
_buildAdminQuickActions(role, isDark),
],
if (controller.gamification.isNotEmpty) ...[
const SizedBox(height: 24),
_buildGamificationCard(controller.gamification, isDark),
],
const SizedBox(height: 32),
const Text('إحصائيات الفواتير',
style: TextStyle(
@@ -386,6 +390,96 @@ class DashboardView extends GetView<DashboardController> {
);
}
Widget _buildGamificationCard(Map gamification, bool isDark) {
final points = gamification['points'] ?? 0;
final level = gamification['level'] ?? 1;
final levelName = gamification['level_name'] ?? 'مبتدئ';
final currentLevelThreshold = gamification['current_level_threshold'] ?? 0;
final nextLevelThreshold = gamification['next_level_threshold'] ?? 1000;
final progress = (points - currentLevelThreshold) /
((nextLevelThreshold - currentLevelThreshold) > 0
? (nextLevelThreshold - currentLevelThreshold)
: 1);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF6366F1).withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.stars_rounded, color: Colors.amber, size: 32),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'المستوى $level: $levelName',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'$points نقطة مكتسبة',
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'المكافآت',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0).toDouble(),
backgroundColor: Colors.white24,
color: Colors.amber,
minHeight: 8,
),
),
const SizedBox(height: 8),
Text(
'باقي ${nextLevelThreshold - points} نقطة للمستوى القادم',
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
);
}
Widget _buildQuotaMeter(Map subscription, bool isDark) {
int limit = subscription['limit'] ?? 100;
int used = subscription['used'] ?? 0;

View File

@@ -226,7 +226,7 @@ class InvoiceDetailController extends GetxController {
AlertDialog(
title: const Text('تأكيد الإرسال'),
content: const Text(
'هل أنت متأكد من إرسال هذه الفاتورة لمنظومة جوفتورة؟\nلا يمكن التراجع عن هذا الإجراء.'),
'هل أنت متأكد من إرسال هذه الفاتورة لمنظومة جوفوترا؟\nلا يمكن التراجع عن هذا الإجراء.'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
@@ -246,21 +246,21 @@ class InvoiceDetailController extends GetxController {
if (confirmed != true) return;
try {
AppSnackbar.showInfo('جاري الإرسال', 'يتم إرسال الفاتورة لمنظومة جوفتورة...');
AppSnackbar.showInfo('جاري الإرسال', 'يتم إرسال الفاتورة لمنظومة جوفوترا...');
final res = await DioClient().client.post(
'invoices/submit-jofotara',
data: {'invoice_id': invoiceId},
);
if (res.data['success'] == true) {
AppSnackbar.showSuccess('تم الإرسال', 'تم تقديم الفاتورة لجوفتورة بنجاح');
AppSnackbar.showSuccess('تم الإرسال', 'تم تقديم الفاتورة لجوفوترا بنجاح');
fetchInvoiceDetails();
} else {
AppSnackbar.showError('خطأ', res.data['message'] ?? 'فشل الإرسال');
}
} catch (e) {
AppLogger.error('Failed to submit to JoFotara', e);
AppSnackbar.showError('خطأ', 'فشل إرسال الفاتورة لجوفتورة');
AppSnackbar.showError('خطأ', 'فشل إرسال الفاتورة لجوفوترا');
}
}
}

View File

@@ -135,7 +135,7 @@ class InvoiceDetailView extends StatelessWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
icon: const Icon(Icons.send_rounded),
label: const Text('إرسال لجوفتورة', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
label: const Text('إرسال لجوفوترا', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
),
],
@@ -268,7 +268,7 @@ class InvoiceDetailView extends StatelessWidget {
const Icon(Icons.verified, color: Color(0xFF10B981), size: 20),
const SizedBox(width: 8),
const Text(
'مُقدَّمة لجوفتورة',
'مُقدَّمة لجوفوترا',
style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF10B981)),
),
],
@@ -489,7 +489,7 @@ class InvoiceDetailView extends StatelessWidget {
break;
case 'submitted':
color = const Color(0xFF6366F1);
text = 'مُقدَّمة لجوفتورة';
text = 'مُقدَّمة لجوفوترا';
break;
default:
color = const Color(0xFFF59E0B);