Update: 2026-05-07 03:06:15

This commit is contained in:
Hamza-Ayed
2026-05-07 03:06:15 +03:00
parent 272971fc5b
commit bfb6368ec8
28 changed files with 3292 additions and 188 deletions

View File

@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/payment_receipt_controller.dart';
class PaymentReceiptView extends StatelessWidget {
const PaymentReceiptView({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(PaymentReceiptController());
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)),
backgroundColor: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81),
foregroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Payment Info Card
Obx(() => 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(
children: [
const Text('تفاصيل التحويل المطلوب', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 16),
_buildInfoRow('الاسم المستعار (CliQ)', controller.payment['cliq_alias'] ?? '', isDark, isHighlight: true),
const Divider(height: 24),
_buildInfoRow('المبلغ المطلوب', '${controller.payment['amount_jod'] ?? 0} JOD', isDark),
],
),
)),
const SizedBox(height: 24),
Text('الخطوة التالية:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: isDark ? Colors.white : Colors.black87)),
const SizedBox(height: 8),
Text(
'قم بتحويل المبلغ عبر تطبيق البنك الخاص بك إلى الاسم المستعار المذكور أعلاه (CliQ). بعد إتمام الحوالة بنجاح، ستصلك رسالة أو إشعار من البنك يحتوي على "رقم المرجع" للعملية. انسخه والصقه هنا.',
style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.grey.shade700, height: 1.5),
),
const SizedBox(height: 24),
// Reference Number Input Area
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('رقم المرجع (Reference Number)', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
TextField(
controller: controller.referenceController,
decoration: InputDecoration(
hintText: 'مثال: 1234567890',
hintStyle: TextStyle(color: isDark ? Colors.white38 : Colors.grey),
filled: true,
fillColor: isDark ? const Color(0xFF1A1A2E) : const Color(0xFFF1F5F9),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
keyboardType: TextInputType.text,
style: TextStyle(
fontFamily: 'monospace',
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
),
const SizedBox(height: 32),
// Submit Button
SizedBox(
height: 52,
child: Obx(() => ElevatedButton.icon(
onPressed: controller.isUploading.value
? null
: () => controller.submitReference(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0F4C81),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
disabledBackgroundColor: isDark ? Colors.white10 : Colors.grey.shade300,
),
icon: controller.isUploading.value
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.check_circle_outline),
label: Text(
controller.isUploading.value ? 'جاري التحقق...' : 'تأكيد الدفع',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
)),
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value, bool isDark, {bool isHighlight = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.grey.shade600)),
Container(
padding: EdgeInsets.symmetric(horizontal: isHighlight ? 12 : 0, vertical: isHighlight ? 6 : 0),
decoration: isHighlight ? BoxDecoration(
color: const Color(0xFF0F4C81).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
) : null,
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: isHighlight ? const Color(0xFF0F4C81) : (isDark ? Colors.white : Colors.black87),
fontFamily: 'monospace',
),
),
),
],
);
}
}

View File

@@ -0,0 +1,370 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/subscription_controller.dart';
import '../../../core/utils/app_snackbar.dart';
class SubscriptionView extends StatelessWidget {
const SubscriptionView({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(SubscriptionController());
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)),
backgroundColor: isDark ? const Color(0xFF1E1E2E) : const Color(0xFF0F4C81),
foregroundColor: Colors.white,
elevation: 0,
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF0F4C81)));
}
return RefreshIndicator(
onRefresh: () async => controller.loadAll(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Current Subscription Status
if (controller.currentSubscription.value != null)
_buildCurrentPlan(controller.currentSubscription.value!, isDark),
const SizedBox(height: 24),
// Active Payment Request Banner
if (controller.activePaymentRequest.value != null)
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
// Plans Header
Row(
children: [
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22),
const SizedBox(width: 8),
Text(
'اختر باقتك',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
),
],
),
const SizedBox(height: 4),
Text(
'ادفع عبر CliQ — بدون عمولة!',
style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey),
),
const SizedBox(height: 16),
// Plans Grid
...controller.plans.map((plan) => _buildPlanCard(plan, controller, isDark)),
const SizedBox(height: 24),
// Payment History
if (controller.myPayments.isNotEmpty) ...[
const Text('سجل المدفوعات', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
...controller.myPayments.map((p) => _buildPaymentHistoryItem(p, isDark)),
],
const SizedBox(height: 40),
],
),
),
);
}),
);
}
Widget _buildCurrentPlan(Map<String, dynamic> sub, bool isDark) {
final planName = sub['plan_name'] ?? sub['plan_id'] ?? 'مجانية';
final daysLeft = sub['days_remaining'] ?? 0;
final used = sub['invoices_used'] ?? 0;
final limit = sub['invoices_limit'] ?? 0;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0F4C81), Color(0xFF1A6BB5)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.verified, color: Color(0xFFD4AF37), size: 24),
const SizedBox(width: 8),
Text(
'باقتك الحالية: $planName',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 12),
Row(
children: [
_buildSubInfoChip(Icons.timer_outlined, '$daysLeft يوم متبقي'),
const SizedBox(width: 12),
_buildSubInfoChip(Icons.receipt_long, '$used/$limit فاتورة'),
],
),
],
),
);
}
Widget _buildSubInfoChip(IconData icon, String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white70, size: 16),
const SizedBox(width: 6),
Text(text, style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
);
}
Widget _buildActivePaymentBanner(Map<String, dynamic> payment, bool isDark) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFFF7ED),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFFED7AA)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.pending_actions, color: Color(0xFFF59E0B), size: 20),
SizedBox(width: 8),
Text('لديك طلب دفع قائم', style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF92400E))),
],
),
const SizedBox(height: 8),
Text('رقم المرجع: ${payment['reference_number']}', style: const TextStyle(fontFamily: 'monospace', fontSize: 14, fontWeight: FontWeight.bold)),
Text('المبلغ: ${payment['amount_jod']} JOD', style: const TextStyle(fontSize: 13)),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.upload_file, size: 18),
label: const Text('رفع وصل الدفع'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF59E0B),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
// Navigate to receipt upload screen
Get.toNamed('/payment-receipt', arguments: payment);
},
),
),
],
),
);
}
Widget _buildPlanCard(Map<String, dynamic> plan, SubscriptionController ctrl, bool isDark) {
final isPopular = plan['is_popular'] == true;
final price = (plan['price_jod'] ?? 0).toString();
final features = (plan['features'] as List?)?.cast<String>() ?? [];
final nameAr = plan['name_ar'] ?? plan['name_en'] ?? 'باقة';
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isPopular ? const Color(0xFFD4AF37) : (isDark ? Colors.white10 : Colors.grey.shade200),
width: isPopular ? 2 : 1,
),
),
child: Column(
children: [
// Popular Badge
if (isPopular)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: const BoxDecoration(
color: Color(0xFFD4AF37),
borderRadius: BorderRadius.only(topLeft: Radius.circular(14), topRight: Radius.circular(14)),
),
child: const Center(
child: Text('⭐ الأكثر شعبية', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(nameAr, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF0F172A))),
RichText(
text: TextSpan(
children: [
TextSpan(text: price, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81))),
TextSpan(text: ' JOD', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
],
),
),
],
),
const SizedBox(height: 8),
Text(
plan['description_ar'] ?? '',
style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey),
),
const SizedBox(height: 12),
// Features
...features.map((f) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
const Icon(Icons.check_circle, color: Color(0xFF10B981), size: 16),
const SizedBox(width: 8),
Expanded(child: Text(f, style: TextStyle(fontSize: 13, color: isDark ? Colors.white70 : Colors.black87))),
],
),
)),
const SizedBox(height: 12),
// Stats Row
Row(
children: [
_buildPlanStat(Icons.business, '${plan['max_companies'] ?? 0} شركات'),
const SizedBox(width: 8),
_buildPlanStat(Icons.receipt_long, '${plan['max_invoices_month'] ?? 0} فاتورة/شهر'),
const SizedBox(width: 8),
_buildPlanStat(Icons.people, '${plan['max_users'] ?? 0} مستخدمين'),
],
),
const SizedBox(height: 16),
// Upgrade Button
SizedBox(
width: double.infinity,
height: 48,
child: Obx(() => ElevatedButton(
onPressed: ctrl.isCreatingPayment.value ? null : () async {
final result = await ctrl.createPaymentRequest(plan['id'].toString());
if (result != null) {
AppSnackbar.showSuccess('تم إنشاء طلب الدفع', 'قم بالتحويل عبر CliQ ثم ارفع وصل الدفع');
} else {
AppSnackbar.showError('خطأ', 'فشل إنشاء طلب الدفع');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: isPopular ? const Color(0xFFD4AF37) : const Color(0xFF0F4C81),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: ctrl.isCreatingPayment.value
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Text('ترقية الآن', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
)),
),
],
),
),
],
),
);
}
Widget _buildPlanStat(IconData icon, String text) {
return Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF0F4C81).withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(icon, size: 16, color: const Color(0xFF0F4C81)),
const SizedBox(height: 2),
Text(text, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
],
),
),
);
}
Widget _buildPaymentHistoryItem(Map<String, dynamic> payment, bool isDark) {
final status = payment['status'] ?? '';
Color statusColor;
String statusText;
switch (status) {
case 'approved': statusColor = const Color(0xFF10B981); statusText = 'تم الاعتماد'; break;
case 'pending': statusColor = const Color(0xFFF59E0B); statusText = 'قيد الانتظار'; break;
case 'uploaded': statusColor = const Color(0xFF3B82F6); statusText = 'تحت المراجعة'; break;
case 'rejected': statusColor = const Color(0xFFEF4444); statusText = 'مرفوض'; break;
default: statusColor = Colors.grey; statusText = status;
}
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 0,
color: isDark ? const Color(0xFF1E1E2E) : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: isDark ? Colors.white10 : Colors.grey.shade200),
),
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.payment_rounded, color: statusColor, size: 20),
),
title: Text(payment['plan_name'] ?? 'باقة', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
subtitle: Text('${payment['amount_jod']} JOD • ${payment['reference_number']}', style: const TextStyle(fontSize: 11, fontFamily: 'monospace')),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(statusText, style: TextStyle(color: statusColor, fontSize: 11, fontWeight: FontWeight.w600)),
),
),
);
}
}