Compare commits

...

7 Commits

Author SHA1 Message Date
Hamza-Ayed
5f62455113 Update: 2026-05-25 21:44:11 2026-05-25 21:44:11 +03:00
Hamza-Ayed
2f1a6f9c85 Update: 2026-05-17 18:47:51 2026-05-17 18:47:51 +03:00
Hamza-Ayed
9ad361e992 Update: 2026-05-16 01:40:56 2026-05-16 01:40:56 +03:00
Hamza-Ayed
aceb7d324f Update: 2026-05-16 01:36:22 2026-05-16 01:36:22 +03:00
Hamza-Ayed
24a9f064a1 Update: 2026-05-16 00:33:22 2026-05-16 00:33:22 +03:00
Hamza-Ayed
663896becb Update: 2026-05-16 00:27:57 2026-05-16 00:27:59 +03:00
Hamza-Ayed
e93f1d4f34 feat: implement annual subscription model across backend quota system and flutter UI 2026-05-16 00:15:38 +03:00
25 changed files with 793 additions and 447 deletions

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info // Fetch subscription with plan info
$stmt = $db->prepare(" $stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ? WHERE s.tenant_id = ?
@@ -57,10 +57,12 @@ final class QuotaMiddleware
json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403); json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403);
} }
// Auto-reset monthly counter if billing period has ended // Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) { if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s'); $newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+30 days')); $cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare(" $resetStmt = $db->prepare("
UPDATE subscriptions UPDATE subscriptions
@@ -76,15 +78,15 @@ final class QuotaMiddleware
$sub['current_period_start'] = $newStart; $sub['current_period_start'] = $newStart;
$sub['current_period_end'] = $newEnd; $sub['current_period_end'] = $newEnd;
error_log("QuotaMiddleware: Auto-reset monthly counter for tenant {$tenantId}"); error_log("QuotaMiddleware: Auto-reset annual counter for tenant {$tenantId}");
} }
// Check invoice quota // Check invoice quota
$used = (int)$sub['invoices_used_this_month']; $used = (int)$sub['invoices_used_this_month'];
$limit = (int)$sub['max_invoices_per_month']; $limit = (int)$sub['max_invoices_per_month']; // Keeping the DB column name the same for compatibility
if ($used >= $limit) { if ($used >= $limit) {
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [ json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
'quota_type' => 'invoices', 'quota_type' => 'invoices',
'used' => $used, 'used' => $used,
'limit' => $limit, 'limit' => $limit,

View File

@@ -9,12 +9,14 @@
return [ return [
'free' => [ 'free' => [
'id' => 'free', 'id' => 'free',
'name_ar' => 'مجانية', 'name_ar' => 'التجربة المجانية',
'name_en' => 'Free', 'name_en' => 'Free Trial',
'max_companies' => 1, 'max_companies' => 1,
'max_invoices_month' => 15, 'max_invoices_month' => 15,
'max_users' => 1, 'max_users' => 1,
'price_jod' => 0.00, 'price_jod' => 0.00,
'price_monthly_jod' => 0.00,
'price_annual_jod' => 0.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'gray', 'badge_color' => 'gray',
@@ -29,90 +31,50 @@ return [
], ],
'basic' => [ 'basic' => [
'id' => 'basic', 'id' => 'basic',
'name_ar' => 'أساسية', 'name_ar' => 'الباقة الأساسية',
'name_en' => 'Basic', 'name_en' => 'Basic Plan',
'max_companies' => 3, 'max_companies' => 3,
'max_invoices_month' => 100, 'max_invoices_month' => 500,
'max_users' => 3, 'max_users' => 2,
'price_jod' => 15.00, 'price_jod' => 15.00, // Default legacy price
'price_monthly_jod' => 15.00,
'price_annual_jod' => 120.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'blue', 'badge_color' => 'blue',
'description_ar' => 'للمحاسبين المستقلين — 3 شركات', 'description_ar' => 'للمحاسبين المستقلين والشركات الصغيرة — 3 شركات',
'features' => [ 'features' => [
'استخراج الفواتير بالذكاء الاصطناعي', 'استخراج الفواتير بالذكاء الاصطناعي',
'الربط المباشر مع جوفوترة', 'الربط المباشر مع جوفوترة',
'حتى 3 شركات', 'حتى 3 شركات (بدلاً من واحدة)',
'100 فاتورة شهرياً', '500 فاتورة شهرياً (سخية جداً)',
'3 مستخدمين', 'مستخدمين اثنين',
'تقارير شهرية', 'دعم فني عبر الواتساب',
],
],
'office' => [
'id' => 'office',
'name_ar' => 'مكتبية',
'name_en' => 'Office',
'max_companies' => 10,
'max_invoices_month' => 500,
'max_users' => 10,
'price_jod' => 45.00,
'ai_features' => true,
'jofotara_enabled' => true,
'badge_color' => 'teal',
'is_popular' => true,
'description_ar' => 'للمكاتب المحاسبية — ربط مباشر بجوفوترة',
'features' => [
'كل ميزات الأساسية',
'ربط مباشر بنظام JoFotara',
'حتى 10 شركات',
'500 فاتورة شهرياً',
'10 مستخدمين',
'تقارير متقدمة + تصدير',
'دعم فني بالأولوية',
], ],
], ],
'pro' => [ 'pro' => [
'id' => 'pro', 'id' => 'pro',
'name_ar' => 'احترافية', 'name_ar' => 'الباقة الاحترافية',
'name_en' => 'Pro', 'name_en' => 'Pro Plan',
'max_companies' => 25, 'max_companies' => 9999,
'max_invoices_month' => 2000, 'max_invoices_month' => 3000,
'max_users' => 25, 'max_users' => 5,
'price_jod' => 99.00, 'price_jod' => 35.00, // Default legacy price
'ai_features' => true, 'price_monthly_jod' => 35.00,
'jofotara_enabled' => true, 'price_annual_jod' => 290.00,
'badge_color' => 'navy',
'description_ar' => 'للمكاتب الكبيرة — حجم عمل ضخم بلا حدود عملية',
'features' => [
'كل ميزات المكتبية',
'حتى 25 شركة',
'2000 فاتورة شهرياً',
'25 مستخدم',
'API كامل لتطبيق الهاتف',
'تدقيق ذكي بالـ AI (Pre-Audit)',
'مدير حساب مخصص',
],
],
'enterprise' => [
'id' => 'enterprise',
'name_ar' => 'مؤسسية',
'name_en' => 'Enterprise',
'max_companies' => 999,
'max_invoices_month' => 99999,
'max_users' => 999,
'price_jod' => 249.00,
'ai_features' => true, 'ai_features' => true,
'jofotara_enabled' => true, 'jofotara_enabled' => true,
'badge_color' => 'gold', 'badge_color' => 'gold',
'description_ar' => 'للمؤسسات — بلا حدود مع دعم مخصص', 'is_popular' => true,
'description_ar' => 'للمكاتب الكبيرة والموزعين — حجم عمل ضخم',
'features' => [ 'features' => [
'كل ميزات الاحترافية', 'استخراج الفواتير بالذكاء الاصطناعي',
'شركات وفواتير بلا حدود عملية', 'الربط المباشر مع جوفوترة',
'مستخدمين بلا حدود', 'عدد شركات غير محدود',
'SLA مضمون 99.9%', '3,000 فاتورة شهرياً',
'ربط API مخصص', '5 مستخدمين',
'تدريب فريق المحاسبة', 'API كامل لتطبيق الهاتف',
'نسخ احتياطي مخصص', 'مدير حساب مخصص',
], ],
], ],
]; ];

View File

@@ -31,7 +31,7 @@ final class QuotaMiddleware
// Fetch subscription with plan info // Fetch subscription with plan info
$stmt = $db->prepare(" $stmt = $db->prepare("
SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled SELECT s.*, sp.name_ar as plan_name, sp.ai_features, sp.jofotara_enabled, sp.price_monthly_jod, sp.price_annual_jod
FROM subscriptions s FROM subscriptions s
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
WHERE s.tenant_id = ? WHERE s.tenant_id = ?
@@ -57,10 +57,12 @@ final class QuotaMiddleware
json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403); json_error('اشتراكك متأخر الدفع. يرجى تسوية المبلغ المستحق للمتابعة.', 403);
} }
// Auto-reset monthly counter if billing period has ended // Auto-reset period counter if billing period has ended
if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) { if (!empty($sub['current_period_end']) && strtotime($sub['current_period_end']) < time()) {
$newStart = date('Y-m-d H:i:s'); $newStart = date('Y-m-d H:i:s');
$newEnd = date('Y-m-d H:i:s', strtotime('+30 days')); $cycle = $sub['billing_cycle'] ?? 'annual';
$interval = ($cycle === 'monthly') ? '+1 month' : '+1 year';
$newEnd = date('Y-m-d H:i:s', strtotime($interval));
$resetStmt = $db->prepare(" $resetStmt = $db->prepare("
UPDATE subscriptions UPDATE subscriptions
@@ -76,15 +78,15 @@ final class QuotaMiddleware
$sub['current_period_start'] = $newStart; $sub['current_period_start'] = $newStart;
$sub['current_period_end'] = $newEnd; $sub['current_period_end'] = $newEnd;
error_log("QuotaMiddleware: Auto-reset monthly counter for tenant {$tenantId}"); error_log("QuotaMiddleware: Auto-reset annual counter for tenant {$tenantId}");
} }
// Check invoice quota // Check invoice quota
$used = (int)$sub['invoices_used_this_month']; $used = (int)$sub['invoices_used_this_month'];
$limit = (int)$sub['max_invoices_per_month']; $limit = (int)$sub['max_invoices_per_month']; // Keeping the DB column name the same for compatibility
if ($used >= $limit) { if ($used >= $limit) {
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [ json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
'quota_type' => 'invoices', 'quota_type' => 'invoices',
'used' => $used, 'used' => $used,
'limit' => $limit, 'limit' => $limit,

View File

@@ -39,8 +39,71 @@ if (!$user || !password_verify($password, $user['password_hash'])) {
json_error('بيانات الدخول غير صحيحة', 401); json_error('بيانات الدخول غير صحيحة', 401);
} }
// 3. Handle device registration if provided (for mobile app login)
$deviceId = $data['device_id'] ?? null; $deviceId = $data['device_id'] ?? null;
$isReviewer = (strtolower($email) === 'reviewer@musadaq.jo');
if ($deviceId && !$isReviewer) {
// Generate and send WhatsApp OTP
$phone = $user['phone'] ? (\App\Core\Encryption::decrypt($user['phone']) ?: $user['phone']) : null;
if (empty($phone)) {
json_error('رقم الهاتف غير مسجل لهذا المستخدم. يرجى التواصل مع المسؤول.', 403);
}
$phone = preg_replace('/[^0-9+]/', '', $phone);
$phone = ltrim($phone, '+');
if (str_starts_with($phone, '07')) {
$phone = '962' . substr($phone, 1);
} elseif (str_starts_with($phone, '7')) {
$phone = '962' . $phone;
}
$otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT);
$otpHash = password_hash($otp, PASSWORD_DEFAULT);
$phoneHash = hash('sha256', $phone);
$cacheDir = STORAGE_PATH . '/cache/otp';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$otpData = [
'hash' => $otpHash,
'user_id' => $user['id'],
'attempts' => 0,
'max_attempts' => 5,
'expires_at' => time() + 300,
'created_at' => time(),
];
$fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w');
if ($fp) {
flock($fp, LOCK_EX);
fwrite($fp, json_encode($otpData));
flock($fp, LOCK_UN);
fclose($fp);
}
$whatsappService = new \App\Services\WhatsAppProxyService();
$message = "رمز التحقق لتطبيق مُصادَق:\n*{$otp}*\n\nصالح لمدة 5 دقائق.";
$result = $whatsappService->sendMessage($phone, $message);
if (!$result['success']) {
error_log("ERROR: Failed to send OTP WhatsApp to phone: {$phone}");
json_error('عذراً، فشل في إرسال رمز التحقق. يرجى المحاولة مرة أخرى.', 500);
}
if (env('APP_DEBUG', 'false') === 'true') {
error_log("DEV OTP for {$phone}: {$otp}");
}
json_success([
'otp_required' => true,
'phone' => $phone,
], 'تم إرسال رمز التحقق إلى رقم هاتفك المسجل عبر واتساب');
exit;
}
// 3. Handle device registration if provided (for mobile app login)
$deviceName = $data['device_name'] ?? 'Web Browser'; $deviceName = $data['device_name'] ?? 'Web Browser';
$deviceSecret = null; $deviceSecret = null;

View File

@@ -32,8 +32,13 @@ if ($errors) {
$db = Database::getInstance(); $db = Database::getInstance();
$tenantId = $decoded['tenant_id']; $tenantId = $decoded['tenant_id'];
$userId = $decoded['user_id']; $userId = $decoded['user_id'];
$planId = $data['plan_id']; $planId = $data['plan_id'];
$cycle = $data['billing_cycle'] ?? 'annual'; // Default to annual
if (!in_array($cycle, ['monthly', 'annual'])) {
json_error('دورة الفوترة غير صالحة.', 422);
}
try { try {
// 1. Get plan details // 1. Get plan details
@@ -45,6 +50,9 @@ try {
json_error('الباقة المختارة غير صالحة أو غير نشطة.', 422); json_error('الباقة المختارة غير صالحة أو غير نشطة.', 422);
} }
// Determine amount based on cycle
$amount = ($cycle === 'monthly') ? ($plan['price_monthly_jod'] ?? $plan['price_jod']) : ($plan['price_annual_jod'] ?? ($plan['price_jod'] * 10));
// 2. Check for existing pending payment for this tenant // 2. Check for existing pending payment for this tenant
$stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1"); $stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1");
$stmt->execute([$tenantId]); $stmt->execute([$tenantId]);
@@ -68,15 +76,16 @@ try {
// 6. Create payment request // 6. Create payment request
$paymentId = Database::generateUuid(); $paymentId = Database::generateUuid();
$stmt = $db->prepare(" $stmt = $db->prepare("
INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at) INSERT INTO payment_requests (id, tenant_id, user_id, plan_id, billing_cycle, amount_jod, internal_reference, cliq_alias, payer_name, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())
"); ");
$stmt->execute([ $stmt->execute([
$paymentId, $paymentId,
$tenantId, $tenantId,
$userId, $userId,
$planId, $planId,
$plan['price_jod'], $cycle,
$amount,
$referenceNumber, $referenceNumber,
$cliqAlias, $cliqAlias,
$user['name'] ?? '' $user['name'] ?? ''
@@ -88,17 +97,17 @@ try {
$tenantId, $tenantId,
$userId, $userId,
$paymentId, $paymentId,
json_encode(['plan_id' => $planId, 'amount' => $plan['price_jod'], 'ref' => $referenceNumber]) json_encode(['plan_id' => $planId, 'cycle' => $cycle, 'amount' => $amount, 'ref' => $referenceNumber])
]); ]);
json_success([ json_success([
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'reference_number' => $referenceNumber, 'reference_number' => $referenceNumber,
'cliq_alias' => $cliqAlias, 'cliq_alias' => $cliqAlias,
'amount_jod' => (float)$plan['price_jod'], 'amount_jod' => (float)$amount,
'plan_name' => $plan['name_ar'] ?? $plan['name_en'], 'plan_name' => ($plan['name_ar'] ?? $plan['name_en']) . " (" . ($cycle === 'monthly' ? 'شهري' : 'سنوي') . ")",
'payer_name' => $user['name'] ?? '', 'payer_name' => $user['name'] ?? '',
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$plan['price_jod']} دينار أردني.", 'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$amount} دينار أردني.",
], 'تم إنشاء طلب الدفع بنجاح'); ], 'تم إنشاء طلب الدفع بنجاح');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -47,18 +47,33 @@ try {
$plan = $stmt->fetch(); $plan = $stmt->fetch();
if ($plan) { if ($plan) {
$cycle = $payment['billing_cycle'] ?? 'annual';
$startDate = date('Y-m-d H:i:s'); $startDate = date('Y-m-d H:i:s');
$endDate = date('Y-m-d H:i:s', strtotime('+30 days'));
if ($cycle === 'monthly') {
$endDate = date('Y-m-d H:i:s', strtotime('+30 days'));
$maxInvoices = (int)$plan['max_invoices_month'];
$price = (float)($plan['price_monthly_jod'] ?? $plan['price_jod']);
} else {
$endDate = date('Y-m-d H:i:s', strtotime('+1 year'));
// Annual gets 12x the monthly quota
$maxInvoices = (int)($plan['max_invoices_month'] * 12);
$price = (float)($plan['price_annual_jod'] ?? ($plan['price_jod'] * 10));
}
$stmt = $db->prepare(" $stmt = $db->prepare("
INSERT INTO subscriptions (tenant_id, plan_id, max_companies, max_invoices_per_month, max_users, price_jod, status, current_period_start, current_period_end, updated_at) INSERT INTO subscriptions (
VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, 'active', :start, :end, NOW()) tenant_id, plan_id, max_companies, max_invoices_per_month, max_users,
price_jod, billing_cycle, status, current_period_start, current_period_end, updated_at
)
VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, :cycle, 'active', :start, :end, NOW())
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
plan_id = VALUES(plan_id), plan_id = VALUES(plan_id),
max_companies = VALUES(max_companies), max_companies = VALUES(max_companies),
max_invoices_per_month = VALUES(max_invoices_per_month), max_invoices_per_month = VALUES(max_invoices_per_month),
max_users = VALUES(max_users), max_users = VALUES(max_users),
price_jod = VALUES(price_jod), price_jod = VALUES(price_jod),
billing_cycle = VALUES(billing_cycle),
status = 'active', status = 'active',
current_period_start = VALUES(current_period_start), current_period_start = VALUES(current_period_start),
current_period_end = VALUES(current_period_end), current_period_end = VALUES(current_period_end),
@@ -68,9 +83,10 @@ try {
't_id' => $payment['tenant_id'], 't_id' => $payment['tenant_id'],
'p_id' => $plan['id'], 'p_id' => $plan['id'],
'max_c' => $plan['max_companies'], 'max_c' => $plan['max_companies'],
'max_i' => $plan['max_invoices_month'], 'max_i' => $maxInvoices,
'max_u' => $plan['max_users'], 'max_u' => $plan['max_users'],
'price' => $plan['price_jod'], 'price' => $price,
'cycle' => $cycle,
'start' => $startDate, 'start' => $startDate,
'end' => $endDate 'end' => $endDate
]); ]);

View File

@@ -13,7 +13,7 @@ $db = Database::getInstance();
try { try {
$stmt = $db->query(" $stmt = $db->query("
SELECT id, name_ar, name_en, max_companies, max_invoices_month, max_users, SELECT id, name_ar, name_en, max_companies, max_invoices_month, max_users,
price_jod, ai_features, jofotara_enabled, sort_order price_jod, price_annual_jod, price_monthly_jod, ai_features, jofotara_enabled, sort_order
FROM subscription_plans FROM subscription_plans
WHERE is_active = 1 WHERE is_active = 1
ORDER BY sort_order ASC ORDER BY sort_order ASC
@@ -36,6 +36,8 @@ try {
$plan['max_invoices_month'] = (int)$plan['max_invoices_month']; $plan['max_invoices_month'] = (int)$plan['max_invoices_month'];
$plan['max_users'] = (int)$plan['max_users']; $plan['max_users'] = (int)$plan['max_users'];
$plan['price_jod'] = (float)$plan['price_jod']; $plan['price_jod'] = (float)$plan['price_jod'];
$plan['price_annual_jod'] = (float)$plan['price_annual_jod'];
$plan['price_monthly_jod'] = (float)$plan['price_monthly_jod'];
$plan['ai_features'] = (bool)$plan['ai_features']; $plan['ai_features'] = (bool)$plan['ai_features'];
$plan['jofotara_enabled'] = (bool)$plan['jofotara_enabled']; $plan['jofotara_enabled'] = (bool)$plan['jofotara_enabled'];
} }

View File

@@ -18,8 +18,8 @@ $data = input();
$errors = Validator::validate($data, [ $errors = Validator::validate($data, [
'name' => 'required', 'name' => 'required',
'email' => 'required|email', 'email' => 'required|email',
'phone' => 'required',
'manager_name' => 'required', 'manager_name' => 'required',
'manager_email' => 'required|email',
'manager_password' => 'required' 'manager_password' => 'required'
]); ]);
@@ -43,12 +43,23 @@ try {
$encryptedTenantName = \App\Core\Encryption::encrypt($data['name']); $encryptedTenantName = \App\Core\Encryption::encrypt($data['name']);
$encryptedTenantEmail = \App\Core\Encryption::encrypt($data['email']); $encryptedTenantEmail = \App\Core\Encryption::encrypt($data['email']);
$phone = preg_replace('/[^0-9+]/', '', $data['phone']);
$phone = ltrim($phone, '+');
if (str_starts_with($phone, '07')) {
$phone = '962' . substr($phone, 1);
} elseif (str_starts_with($phone, '7')) {
$phone = '962' . $phone;
}
$encryptedPhone = \App\Core\Encryption::encrypt($phone);
$phoneHash = hash('sha256', $phone);
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())"); $stmt = $db->prepare("INSERT INTO tenants (id, name, email, phone, status, created_at) VALUES (?, ?, ?, ?, 'active', NOW())");
$stmt->execute([ $stmt->execute([
$tenantId, $tenantId,
$encryptedTenantName, $encryptedTenantName,
$encryptedTenantEmail, $encryptedTenantEmail,
$data['phone'] ?? null $phone
]); ]);
// Generate User UUID // Generate User UUID
@@ -60,17 +71,19 @@ try {
// Encrypt sensitive user data // Encrypt sensitive user data
$encryptedName = \App\Core\Encryption::encrypt($data['manager_name']); $encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
$encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']); $encryptedEmail = \App\Core\Encryption::encrypt($data['email']);
$emailHash = hash('sha256', strtolower($data['manager_email'])); $emailHash = hash('sha256', strtolower($data['email']));
// 2. Create Initial Manager (Admin) for this Tenant // 2. Create Initial Manager (Admin) for this Tenant
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())"); $stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, phone, phone_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'admin', NOW())");
$stmtUser->execute([ $stmtUser->execute([
$userId, $userId,
$tenantId, $tenantId,
$encryptedName, $encryptedName,
$encryptedEmail, $encryptedEmail,
$emailHash, $emailHash,
$encryptedPhone,
$phoneHash,
password_hash($data['manager_password'], PASSWORD_DEFAULT) password_hash($data['manager_password'], PASSWORD_DEFAULT)
]); ]);

View File

@@ -6,7 +6,6 @@
<!-- Camera & Storage (Invoice Scanning & Picking) --> <!-- Camera & Storage (Invoice Scanning & Picking) -->
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- Audio (Voice Assistant) --> <!-- Audio (Voice Assistant) -->

View File

@@ -30,9 +30,9 @@ class AuthController extends GetxController {
normalizedPhone = normalizedPhone.substring(1); normalizedPhone = normalizedPhone.substring(1);
} }
if (normalizedPhone.startsWith('07')) { if (normalizedPhone.startsWith('07')) {
normalizedPhone = '962' + normalizedPhone.substring(1); normalizedPhone = '962${normalizedPhone.substring(1)}';
} else if (normalizedPhone.startsWith('7')) { } else if (normalizedPhone.startsWith('7')) {
normalizedPhone = '962' + normalizedPhone; normalizedPhone = '962$normalizedPhone';
} }
phone.value = normalizedPhone; phone.value = normalizedPhone;
@@ -111,7 +111,8 @@ class AuthController extends GetxController {
} }
} on DioException catch (e, stackTrace) { } on DioException catch (e, stackTrace) {
AppLogger.error('OTP Verify Failed', e.response?.data, stackTrace); AppLogger.error('OTP Verify Failed', e.response?.data, stackTrace);
AppSnackbar.showError('خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح'); AppSnackbar.showError(
'خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -120,7 +121,8 @@ class AuthController extends GetxController {
Future<void> loginWithEmail(String email, String password) async { Future<void> loginWithEmail(String email, String password) async {
try { try {
if (email.trim().isEmpty || password.trim().isEmpty) { if (email.trim().isEmpty || password.trim().isEmpty) {
AppSnackbar.showError('خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور'); AppSnackbar.showError(
'خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور');
return; return;
} }
isLoading.value = true; isLoading.value = true;
@@ -150,9 +152,18 @@ class AuthController extends GetxController {
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
AppLogger.print('Email Login Success. Tokens received.');
final data = response.data['data']; final data = response.data['data'];
if (data['otp_required'] == true) {
AppLogger.print('Email Login verification required via OTP.');
phone.value = data['phone'] ?? '';
AppSnackbar.showSuccess('نجاح', 'تم إرسال رمز التحقق إلى رقم هاتفك المسجل');
Get.toNamed(AppRoutes.OTP_VERIFY);
return;
}
AppLogger.print('Email Login Success. Tokens received.');
// Save secure data // Save secure data
await _storage.saveToken(data['access_token']); await _storage.saveToken(data['access_token']);
// Note: auth/login might not return device_secret, handle if missing // Note: auth/login might not return device_secret, handle if missing

View File

@@ -6,7 +6,7 @@ class PhoneInputView extends StatelessWidget {
PhoneInputView({super.key}); PhoneInputView({super.key});
final AuthController controller = Get.put(AuthController()); final AuthController controller = Get.put(AuthController());
final TextEditingController phoneController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
@override @override
@@ -37,47 +37,38 @@ class PhoneInputView extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text(
'أدخل رقم هاتفك أو البريد الإلكتروني لتسجيل الدخول', 'أدخل البريد الإلكتروني وكلمة المرور لتسجيل الدخول',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
TextField( TextField(
controller: phoneController, controller: emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
onChanged: (val) => controller.phone.value = val, onChanged: (val) => controller.phone.value = val,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'رقم الهاتف أو البريد الإلكتروني', labelText: 'البريد الإلكتروني',
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Obx(() { TextField(
final isEmail = controller.phone.value.contains('@'); controller: passwordController,
if (!isEmail) return const SizedBox.shrink(); obscureText: true,
textDirection: TextDirection.ltr,
return Column( decoration: InputDecoration(
children: [ labelText: 'كلمة المرور',
TextField( prefixIcon: const Icon(Icons.lock_outline),
controller: passwordController, border: OutlineInputBorder(
obscureText: true, borderRadius: BorderRadius.circular(12),
textDirection: TextDirection.ltr, ),
decoration: InputDecoration( ),
labelText: 'كلمة المرور', ),
prefixIcon: const Icon(Icons.lock_outline), const SizedBox(height: 24),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 24),
],
);
}),
Obx(() => ElevatedButton( Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
@@ -88,22 +79,16 @@ class PhoneInputView extends StatelessWidget {
onPressed: controller.isLoading.value onPressed: controller.isLoading.value
? null ? null
: () { : () {
if (controller.phone.value.contains('@')) { controller.loginWithEmail(
controller.loginWithEmail( emailController.text,
controller.phone.value, passwordController.text
passwordController.text );
);
} else {
controller.requestOtp(phoneController.text);
}
}, },
child: controller.isLoading.value child: controller.isLoading.value
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(color: Colors.white)
: Text( : const Text(
controller.phone.value.contains('@') 'تسجيل الدخول',
? 'تسجيل الدخول' style: TextStyle(fontSize: 16)
: 'إرسال رمز التحقق',
style: const TextStyle(fontSize: 16)
), ),
)), )),
], ],

View File

@@ -254,14 +254,16 @@ class DashboardView extends GetView<DashboardController> {
isDark, isDark,
() => Get.toNamed(AppRoutes.AUDIT_LOG), () => Get.toNamed(AppRoutes.AUDIT_LOG),
), ),
const SizedBox(width: 12), if (!GetPlatform.isIOS) ...[
_buildAdminActionCard( const SizedBox(width: 12),
'ادعُ واكسب', _buildAdminActionCard(
Icons.card_giftcard, 'ادعُ واكسب',
const Color(0xFFD4AF37), Icons.card_giftcard,
isDark, const Color(0xFFD4AF37),
() => Get.toNamed(AppRoutes.REFERRAL), isDark,
), () => Get.toNamed(AppRoutes.REFERRAL),
),
],
const SizedBox(width: 12), const SizedBox(width: 12),
_buildAdminActionCard( _buildAdminActionCard(
'استهلاك AI', 'استهلاك AI',

View File

@@ -5,7 +5,7 @@ import '../../../app/routes/app_pages.dart';
class OnboardingController extends GetxController { class OnboardingController extends GetxController {
var currentPage = 0.obs; var currentPage = 0.obs;
final List<OnboardingModel> items = [ List<OnboardingModel> get items => [
OnboardingModel( OnboardingModel(
title: 'مرحباً بك في مُصادَق', title: 'مرحباً بك في مُصادَق',
description: description:
@@ -19,9 +19,10 @@ class OnboardingController extends GetxController {
imageAsset: 'assets/images/onboarding_2.png', imageAsset: 'assets/images/onboarding_2.png',
), ),
OnboardingModel( OnboardingModel(
title: 'مدفوعات فورية آمنة', title: 'إدارة متكاملة لشركتك',
description: description: GetPlatform.isIOS
'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.', ? 'إدارة فواتير الشركات وأرشفة ضريبية متكاملة بأمان وسهولة تامة.'
: 'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.',
imageAsset: 'assets/images/onboarding_3.png', imageAsset: 'assets/images/onboarding_3.png',
), ),
]; ];

View File

@@ -166,7 +166,7 @@ class SettingsView extends GetView<SettingsController> {
_buildInfoTile( _buildInfoTile(
icon: Icons.diamond_rounded, icon: Icons.diamond_rounded,
title: 'الاشتراكات والباقات', title: 'الاشتراكات والباقات',
trailing: 'ترقية →', trailing: GetPlatform.isIOS ? 'التفاصيل →' : 'ترقية →',
isDark: isDark, isDark: isDark,
onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION), onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION),
), ),

View File

@@ -11,6 +11,7 @@ class SubscriptionController extends GetxController {
var isLoading = true.obs; var isLoading = true.obs;
var isCreatingPayment = false.obs; var isCreatingPayment = false.obs;
var activePaymentRequest = Rxn<Map<String, dynamic>>(); var activePaymentRequest = Rxn<Map<String, dynamic>>();
var isAnnual = true.obs; // Toggle between Monthly and Annual
@override @override
void onInit() { void onInit() {
@@ -69,7 +70,11 @@ class SubscriptionController extends GetxController {
Future<Map<String, dynamic>?> createPaymentRequest(String planId) async { Future<Map<String, dynamic>?> createPaymentRequest(String planId) async {
try { try {
isCreatingPayment.value = true; isCreatingPayment.value = true;
final res = await DioClient().client.post('payments/create', data: {'plan_id': planId}); final cycle = isAnnual.value ? 'annual' : 'monthly';
final res = await DioClient().client.post('payments/create', data: {
'plan_id': planId,
'billing_cycle': cycle,
});
if (res.data['success'] == true && res.data['data'] != null) { if (res.data['success'] == true && res.data['data'] != null) {
final result = Map<String, dynamic>.from(res.data['data']); final result = Map<String, dynamic>.from(res.data['data']);
activePaymentRequest.value = result; activePaymentRequest.value = result;

View File

@@ -34,46 +34,122 @@ class SubscriptionView extends StatelessWidget {
children: [ children: [
// Current Subscription Status // Current Subscription Status
if (controller.currentSubscription.value != null) if (controller.currentSubscription.value != null)
_buildCurrentPlan(controller.currentSubscription.value!, isDark), _buildCurrentPlan(controller.currentSubscription.value!, isDark)
else
_buildFreePlanPlaceholder(isDark),
const SizedBox(height: 24), const SizedBox(height: 24),
// Active Payment Request Banner if (GetPlatform.isIOS) ...[
if (controller.activePaymentRequest.value != null) _buildIOSB2BInfoCard(isDark),
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark), ],
// Plans Header if (!GetPlatform.isIOS) ...[
Row( // Active Payment Request Banner
children: [ if (controller.activePaymentRequest.value != null)
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22), _buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
const SizedBox(width: 8),
Text( // Plans Header
'اختر باقتك', Row(
style: TextStyle( mainAxisAlignment: MainAxisAlignment.spaceBetween,
fontSize: 20, children: [
fontWeight: FontWeight.bold, Column(
color: isDark ? Colors.white : const Color(0xFF0F172A), crossAxisAlignment: CrossAxisAlignment.start,
children: [
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: 20),
// Toggle
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isDark ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
), ),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => controller.isAnnual.value = false,
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: !controller.isAnnual.value ? const Color(0xFF0F4C81) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'دفع شهري',
style: TextStyle(
color: !controller.isAnnual.value ? Colors.white : (isDark ? Colors.white60 : Colors.black54),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
)),
),
),
Expanded(
child: GestureDetector(
onTap: () => controller.isAnnual.value = true,
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: controller.isAnnual.value ? const Color(0xFF0F4C81) : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
'دفع سنوي (توفير ✨)',
style: TextStyle(
color: controller.isAnnual.value ? Colors.white : (isDark ? Colors.white60 : Colors.black54),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
)),
),
),
],
),
),
const SizedBox(height: 20),
// 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: 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), const SizedBox(height: 40),
@@ -85,6 +161,84 @@ class SubscriptionView extends StatelessWidget {
); );
} }
Widget _buildFreePlanPlaceholder(bool isDark) {
return _buildCurrentPlan({
'plan_name': 'الباقة الافتراضية (المجانية)',
'days_remaining': 0,
'invoices': {
'used': 0,
'limit': 100,
}
}, isDark);
}
Widget _buildIOSB2BInfoCard(bool isDark) {
return Container(
width: double.infinity,
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: [
Row(
children: [
Icon(Icons.business_rounded, color: const Color(0xFF0F4C81), size: 24),
const SizedBox(width: 8),
Text(
'الاشتراكات المؤسسية (B2B)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : const Color(0xFF0F172A),
),
),
],
),
const SizedBox(height: 12),
Text(
'تطبيق "مُصادَق" مخصص لإدارة الفواتير الضريبية للشركات ومكاتب المحاسبة. يتم تفعيل وإدارة باقات الاشتراك والميزات الإضافية مركزياً عن طريق لوحة التحكم الخاصة بالمسؤول في منشأتك.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: isDark ? Colors.white70 : Colors.black87,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F4C81).withOpacity(0.05),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Icon(Icons.info_outline, color: const Color(0xFF0F4C81), size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
'إذا كنت بحاجة إلى ترقية باقتك أو زيادة الحصص (Quotas)، يرجى التواصل مع مسؤول تكنولوجيا المعلومات أو المحاسب المسؤول في شركتك.',
style: TextStyle(
fontSize: 12,
height: 1.4,
color: const Color(0xFF0F4C81),
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
);
}
Widget _buildCurrentPlan(Map<String, dynamic> sub, bool isDark) { Widget _buildCurrentPlan(Map<String, dynamic> sub, bool isDark) {
final planName = sub['plan_name'] ?? sub['plan_name_en'] ?? sub['plan_id'] ?? 'مجانية'; final planName = sub['plan_name'] ?? sub['plan_name_en'] ?? sub['plan_id'] ?? 'مجانية';
final daysLeft = sub['days_remaining'] ?? 0; final daysLeft = sub['days_remaining'] ?? 0;
@@ -228,10 +382,20 @@ class SubscriptionView extends StatelessWidget {
Widget _buildPlanCard(Map<String, dynamic> plan, SubscriptionController ctrl, bool isDark) { Widget _buildPlanCard(Map<String, dynamic> plan, SubscriptionController ctrl, bool isDark) {
final isPopular = plan['is_popular'] == true; 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'] ?? 'باقة'; final nameAr = plan['name_ar'] ?? plan['name_en'] ?? 'باقة';
return Obx(() {
final bool annual = ctrl.isAnnual.value;
final price = annual
? (plan['price_annual_jod'] ?? (plan['price_jod'] * 10)).toString()
: (plan['price_monthly_jod'] ?? plan['price_jod']).toString();
final features = (plan['features'] as List?)?.cast<String>() ?? [];
final invoiceLimit = annual
? (plan['max_invoices_month'] * 12).toString()
: (plan['max_invoices_month']).toString();
final cycleText = annual ? 'سنة' : 'شهر';
return Container( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -271,7 +435,7 @@ class SubscriptionView extends StatelessWidget {
text: TextSpan( text: TextSpan(
children: [ children: [
TextSpan(text: price, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81))), 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)), TextSpan(text: ' JOD / $cycleText', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
], ],
), ),
), ),
@@ -302,7 +466,7 @@ class SubscriptionView extends StatelessWidget {
children: [ children: [
_buildPlanStat(Icons.business, '${plan['max_companies'] ?? 0} شركات'), _buildPlanStat(Icons.business, '${plan['max_companies'] ?? 0} شركات'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildPlanStat(Icons.receipt_long, '${plan['max_invoices_month'] ?? 0} فاتورة/شهر'), _buildPlanStat(Icons.receipt_long, '$invoiceLimit فاتورة/$cycleText'),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildPlanStat(Icons.people, '${plan['max_users'] ?? 0} مستخدمين'), _buildPlanStat(Icons.people, '${plan['max_users'] ?? 0} مستخدمين'),
], ],
@@ -338,6 +502,7 @@ class SubscriptionView extends StatelessWidget {
], ],
), ),
); );
});
} }
Widget _buildPlanStat(IconData icon, String text) { Widget _buildPlanStat(IconData icon, String text) {

View File

@@ -9,8 +9,8 @@ import 'tenants_management_controller.dart';
class AddTenantController extends GetxController { class AddTenantController extends GetxController {
final nameController = TextEditingController(); final nameController = TextEditingController();
final emailController = TextEditingController(); final emailController = TextEditingController();
final phoneController = TextEditingController();
final managerNameController = TextEditingController(); final managerNameController = TextEditingController();
final managerEmailController = TextEditingController();
final managerPasswordController = TextEditingController(); final managerPasswordController = TextEditingController();
var isSubmitting = false.obs; var isSubmitting = false.obs;
@@ -20,8 +20,8 @@ class AddTenantController extends GetxController {
void onClose() { void onClose() {
nameController.dispose(); nameController.dispose();
emailController.dispose(); emailController.dispose();
phoneController.dispose();
managerNameController.dispose(); managerNameController.dispose();
managerEmailController.dispose();
managerPasswordController.dispose(); managerPasswordController.dispose();
super.onClose(); super.onClose();
} }
@@ -29,11 +29,11 @@ class AddTenantController extends GetxController {
Future<void> submit() async { Future<void> submit() async {
final name = nameController.text.trim(); final name = nameController.text.trim();
final email = emailController.text.trim(); final email = emailController.text.trim();
final phone = phoneController.text.trim();
final managerName = managerNameController.text.trim(); final managerName = managerNameController.text.trim();
final managerEmail = managerEmailController.text.trim();
final managerPassword = managerPasswordController.text; final managerPassword = managerPasswordController.text;
if (name.isEmpty || email.isEmpty || managerName.isEmpty || managerEmail.isEmpty || managerPassword.isEmpty) { if (name.isEmpty || email.isEmpty || phone.isEmpty || managerName.isEmpty || managerPassword.isEmpty) {
AppSnackbar.showWarning('تنبيه', 'الرجاء إدخال جميع البيانات المطلوبة'); AppSnackbar.showWarning('تنبيه', 'الرجاء إدخال جميع البيانات المطلوبة');
return; return;
} }
@@ -43,8 +43,8 @@ class AddTenantController extends GetxController {
final response = await _dio.post('tenants/create', data: { final response = await _dio.post('tenants/create', data: {
'name': name, 'name': name,
'email': email, 'email': email,
'phone': phone,
'manager_name': managerName, 'manager_name': managerName,
'manager_email': managerEmail,
'manager_password': managerPassword, 'manager_password': managerPassword,
}); });

View File

@@ -36,29 +36,29 @@ class AddTenantView extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: controller.emailController, controller: controller.emailController,
label: 'البريد الإلكتروني للمكتب', label: 'البريد الإلكتروني للعمل',
icon: Icons.email, icon: Icons.email,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
isDark: isDark, isDark: isDark,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const Text( const Text(
'بيانات مدير المكتب', 'بيانات مدير المكتب المسؤول',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: controller.managerNameController, controller: controller.managerNameController,
label: 'اسم المدير', label: 'اسم المدير الكامل',
icon: Icons.person, icon: Icons.person,
isDark: isDark, isDark: isDark,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
controller: controller.managerEmailController, controller: controller.phoneController,
label: 'البريد الإلكتروني للمدير', label: 'رقم هاتف المدير (لتسجيل الدخول OTP)',
icon: Icons.alternate_email, icon: Icons.phone,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.phone,
isDark: isDark, isDark: isDark,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -5,26 +5,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "88.0.0" version: "93.0.0"
_flutterfire_internals: _flutterfire_internals:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.69" version: "1.3.71"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1" version: "10.0.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
barcode: barcode:
dependency: transitive dependency: transitive
description: description:
@@ -77,18 +77,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build name: build
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "4.0.6"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
name: build_config name: build_config
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
build_daemon: build_daemon:
dependency: transitive dependency: transitive
description: description:
@@ -97,30 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
url: "https://pub.dev"
source: hosted
version: "3.0.3"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.1" version: "2.15.0"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
url: "https://pub.dev"
source: hosted
version: "9.3.1"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -217,14 +201,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_builder: code_assets:
dependency: transitive dependency: transitive
description: description:
name: code_builder name: code_assets
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.11.1" version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -293,18 +277,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cunning_document_scanner name: cunning_document_scanner
sha256: bf590e8c8c8a4903ba7873c4f22b67e976604853f11065f594cb19b408bd25ef sha256: de0c0705799f7d5cc9b82b67bfb8b3e965a1fbff4afbd70ea10cd1dad4f3a98c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.4.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.7"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -405,10 +389,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file_selector_macos name: file_selector_macos
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.4+4" version: "0.9.5"
file_selector_platform_interface: file_selector_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -429,50 +413,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158 sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.0" version: "4.9.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_platform_interface name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "7.0.1"
firebase_core_web: firebase_core_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.0" version: "3.7.0"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.2.0" version: "16.2.2"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a" sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.9" version: "4.7.11"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1" sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.5" version: "4.1.7"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -485,10 +469,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flat_buffers name: flat_buffers
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3" sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "23.5.26" version: "25.9.23"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -570,26 +554,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.31" version: "2.0.34"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_secure_storage name: flutter_secure_storage
sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87" sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.0" version: "10.2.0"
flutter_secure_storage_darwin: flutter_secure_storage_darwin:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_darwin name: flutter_secure_storage_darwin
sha256: "3af15a3cb2bf5b8b776832bd01776f8018766aece55623176e28b406481fb320" sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.0" version: "0.3.1"
flutter_secure_storage_linux: flutter_secure_storage_linux:
dependency: transitive dependency: transitive
description: description:
@@ -636,18 +620,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: freerasp name: freerasp
sha256: "76a3fb6f8e3fdd7d83e224866998523e7fb79d5779321983e484a6cfbf4b01b5" sha256: "5b9a3402a7a30d928897e2264e2700a30db1df14de289f500b3a0cf50dc19df2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.12.0" version: "7.5.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -672,6 +648,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
html: html:
dependency: transitive dependency: transitive
description: description:
@@ -708,26 +692,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.4" version: "4.8.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.13+1" version: "0.8.13+17"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -740,10 +724,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.13" version: "0.8.13+6"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
@@ -756,10 +740,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2+1"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -792,14 +776,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.12.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -844,18 +844,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: local_auth_android name: local_auth_android
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.52" version: "1.0.56"
local_auth_darwin: local_auth_darwin:
dependency: transitive dependency: transitive
description: description:
name: local_auth_darwin name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055" sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.1"
local_auth_platform_interface: local_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -884,10 +884,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: lottie name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950 sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -924,10 +924,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: mime name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@@ -940,26 +948,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: objectbox name: objectbox
sha256: "3cc186749178a3556e1020c9082d0897d0f9ecbdefcc27320e65c5bc650f0e57" sha256: "83d58e0ab5c4180a2f67086c449a4f2d1475932b31819271923dfc82a76f73c6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.1" version: "5.3.1"
objectbox_flutter_libs: objectbox_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: objectbox_flutter_libs name: objectbox_flutter_libs
sha256: cd754766e04229a4f51250f121813d9a3c1a74fc21cd68e48b3c6085cbcd6c85 sha256: fd4e8ed03e2b2bfeb8aa965435d7c5523ec7e962f1ffecf6e7da6c1f4d170419
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.1" version: "5.3.1"
objectbox_generator: objectbox_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: objectbox_generator name: objectbox_generator
sha256: "71a3f6948e631be5c7160d512ad2a8cb7471cdbcf1731ec6baf2a794b82386d7" sha256: daa95f21c7140c619ffc1abee2465541d4f0317b8c3bbf2141be1a9e5c507a1d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.1" version: "5.3.1"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@@ -1020,18 +1036,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.19" version: "2.3.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.6.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1060,10 +1076,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: pdf name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.11.3" version: "3.12.0"
pdf_widget_wrapper: pdf_widget_wrapper:
dependency: transitive dependency: transitive
description: description:
@@ -1072,30 +1088,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
pedantic:
dependency: transitive
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.4.0" version: "12.0.1"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.1.0" version: "13.0.1"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@@ -1256,6 +1264,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
record_web: record_web:
dependency: transitive dependency: transitive
description: description:
@@ -1337,66 +1353,66 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "4.2.3"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
speech_to_text: speech_to_text:
dependency: "direct main" dependency: "direct main"
description: description:
name: speech_to_text name: speech_to_text
sha256: c07557664974afa061f221d0d4186935bea4220728ea9446702825e8b988db04 sha256: "75587f7400f485fdf166beacd471549d98fe5d58e634f708916bb65dec05d6a4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.3.0" version: "7.4.0"
speech_to_text_platform_interface: speech_to_text_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: speech_to_text_platform_interface name: speech_to_text_platform_interface
sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114 sha256: a7e16e02853853ed7534ac2bde9a1c4f39c8879970a7974ac6ff832d4bdaa4b0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.4.0"
speech_to_text_windows: speech_to_text_windows:
dependency: transitive dependency: transitive
description: description:
name: speech_to_text_windows name: speech_to_text_windows
sha256: "2c9846d18253c7bbe059a276297ef9f27e8a2745dead32192525beb208195072" sha256: "2d1d10565b23262386b453b33656299608dc7a66784453735d6c1318f13f44d7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+beta.8" version: "1.0.1"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:
name: sqflite name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.2+1"
sqflite_android: sqflite_android:
dependency: transitive dependency: transitive
description: description:
name: sqflite_android name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2+3"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.6" version: "2.5.8"
sqflite_darwin: sqflite_darwin:
dependency: transitive dependency: transitive
description: description:
@@ -1449,10 +1465,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.0" version: "3.4.0+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -1469,14 +1485,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.9"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1497,18 +1505,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.20" version: "6.3.29"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.4" version: "6.4.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1521,10 +1529,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.3" version: "3.2.5"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1537,10 +1545,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1569,10 +1577,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.0" version: "15.2.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -1646,5 +1654,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.11.0 <4.0.0"
flutter: ">=3.32.0" flutter: ">=3.38.4"

View File

@@ -1,7 +1,7 @@
name: musadaq_app name: musadaq_app
description: Jordanian E-Invoicing Automation SaaS description: Jordanian E-Invoicing Automation SaaS
publish_to: 'none' publish_to: 'none'
version: 1.0.3+3 version: 1.0.6+6
environment: environment:
sdk: '>=3.2.0 <4.0.0' sdk: '>=3.2.0 <4.0.0'
@@ -18,8 +18,8 @@ dependencies:
flutter_secure_storage: ^10.1.0 flutter_secure_storage: ^10.1.0
# ─── Local Database (ObjectBox) ───────────────────── # ─── Local Database (ObjectBox) ─────────────────────
objectbox: ^4.0.1 objectbox: ^5.3.1
objectbox_flutter_libs: any objectbox_flutter_libs: ^5.3.1
path_provider: ^2.1.2 path_provider: ^2.1.2
# ─── Authentication & Security ────────────────────── # ─── Authentication & Security ──────────────────────
@@ -28,8 +28,8 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
# ─── Camera & Scanning ────────────────────────────── # ─── Camera & Scanning ──────────────────────────────
camerawesome: ^2.0.0 camerawesome: ^2.5.0
cunning_document_scanner: ^1.2.3 cunning_document_scanner: ^1.4.0
image_picker: ^1.0.7 image_picker: ^1.0.7
file_picker: ^8.1.2 file_picker: ^8.1.2
@@ -46,7 +46,7 @@ dependencies:
# ─── Voice & Audio ────────────────────────────────── # ─── Voice & Audio ──────────────────────────────────
speech_to_text: ^7.3.0 speech_to_text: ^7.3.0
record: ^6.2.0 record: ^6.2.0
permission_handler: ^11.3.0 permission_handler: ^12.0.1
# ─── Connectivity & Background ────────────────────── # ─── Connectivity & Background ──────────────────────
connectivity_plus: ^6.0.3 connectivity_plus: ^6.0.3
@@ -70,13 +70,13 @@ dependencies:
shorebird_code_push: ^2.0.0 shorebird_code_push: ^2.0.0
# ─── Security (Root/Jailbreak/Tamper Detection) ───── # ─── Security (Root/Jailbreak/Tamper Detection) ─────
freerasp: ^6.6.0 freerasp: ^7.5.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
objectbox_generator: any objectbox_generator: ^5.3.1
build_runner: ^2.4.8 build_runner: ^2.4.8
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1

View File

@@ -164,10 +164,22 @@
<h2>اختر الباقة المناسبة لحجم أعمالك</h2> <h2>اختر الباقة المناسبة لحجم أعمالك</h2>
<p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p> <p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p>
</div> </div>
<div style="display:flex; justify-content:center; margin-bottom:40px;">
<div class="cycle-toggle" style="background:rgba(255,255,255,0.05); padding:6px; border-radius:14px; display:inline-flex; border:1px solid rgba(255,255,255,0.1); cursor:pointer;" onclick="this.classList.toggle('monthly'); document.querySelectorAll('.price-monthly').forEach(el=>el.style.display=this.classList.contains('monthly')?'block':'none'); document.querySelectorAll('.price-annual').forEach(el=>el.style.display=this.classList.contains('monthly')?'none':'block');">
<span class="toggle-btn annual-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:white; background:var(--green-mid); transition:all 0.3s;">دفع سنوي (توفير )</span>
<span class="toggle-btn monthly-btn" style="padding:10px 24px; border-radius:10px; font-size:14px; font-weight:700; color:var(--text-3); transition:all 0.3s;">دفع شهري</span>
</div>
</div>
<style>
.cycle-toggle.monthly .annual-btn { background:transparent; color:var(--text-3); }
.cycle-toggle.monthly .monthly-btn { background:var(--green-mid); color:white; }
</style>
<div class="pricing-grid"> <div class="pricing-grid">
<div class="price-card"> <div class="price-card">
<div class="price-name">مجانية</div> <div class="price-name">التجربة المجانية</div>
<div class="price-amount">0 <span>دينار/شهر</span></div> <div class="price-amount">0 <span>دينار/شهر</span></div>
<div class="price-desc">للتجربة الأولية</div> <div class="price-desc">للتجربة الأولية</div>
<ul class="price-features"> <ul class="price-features">
@@ -180,63 +192,38 @@
<a href="/register.php" class="btn btn-outline" style="width:100%">ابدأ مجاناً</a> <a href="/register.php" class="btn btn-outline" style="width:100%">ابدأ مجاناً</a>
</div> </div>
<div class="price-card">
<div class="price-name">أساسية</div>
<div class="price-amount">15 <span>دينار/شهر</span></div>
<div class="price-desc">للمحاسبين المستقلين</div>
<ul class="price-features">
<li><span class="feature-check"></span> حتى 3 شركات</li>
<li><span class="feature-check"></span> 100 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 3 مستخدمين</li>
<li><span class="feature-check"></span> تقارير شهرية</li>
<li><span class="feature-check"></span> دعم فني</li>
</ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a>
</div>
<div class="price-card popular"> <div class="price-card popular">
<div class="popular-badge"> الأكثر اختياراً</div> <div class="popular-badge"> الأكثر اختياراً</div>
<div class="price-name">مكتبية</div> <div class="price-name">الباقة الأساسية</div>
<div class="price-amount">45 <span>دينار/شهر</span></div> <div class="price-amount price-annual">120 <span>دينار/سنة</span></div>
<div class="price-desc">للمكاتب المحاسبية</div> <div class="price-amount price-monthly" style="display:none;">15 <span>دينار/شهر</span></div>
<div class="price-desc">للمحاسبين والشركات الصغيرة</div>
<ul class="price-features"> <ul class="price-features">
<li><span class="feature-check"></span> حتى 10 شركات</li> <li><span class="feature-check"></span> حتى 3 شركات</li>
<li><span class="feature-check"></span> 500 فاتورة شهرياً</li> <li><span class="feature-check"></span> 500 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 10 مستخدمين</li> <li><span class="feature-check"></span> مستخدمين اثنين</li>
<li><span class="feature-check"></span> تصدير Excel متقدم</li> <li><span class="feature-check"></span> دعم فني متكامل</li>
<li><span class="feature-check"></span> دعم بالأولوية</li> <li><span class="feature-check"></span> ربط مباشر مع جوفوترة</li>
</ul> </ul>
<a href="/register.php" class="btn btn-primary" style="width:100%">اشترك الآن</a> <a href="/register.php" class="btn btn-primary" style="width:100%">اشترك الآن</a>
</div> </div>
<div class="price-card"> <div class="price-card">
<div class="price-name">احترافية</div> <div class="price-name">الباقة الاحترافية</div>
<div class="price-amount">99 <span>دينار/شهر</span></div> <div class="price-amount price-annual">290 <span>دينار/سنة</span></div>
<div class="price-desc">للمكاتب الكبيرة</div> <div class="price-amount price-monthly" style="display:none;">35 <span>دينار/شهر</span></div>
<div class="price-desc">للمكاتب الكبيرة والموزعين</div>
<ul class="price-features"> <ul class="price-features">
<li><span class="feature-check"></span> حتى 25 شركة</li> <li><span class="feature-check"></span> شركات غير محدودة</li>
<li><span class="feature-check"></span> 2,000 فاتورة شهرياً</li> <li><span class="feature-check"></span> 3,000 فاتورة شهرياً</li>
<li><span class="feature-check"></span> 25 مستخدم</li> <li><span class="feature-check"></span> 5 مستخدمين</li>
<li><span class="feature-check"></span> تدقيق ذكي استباقي</li> <li><span class="feature-check"></span> تدقيق ذكي استباقي</li>
<li><span class="feature-check"></span> مدير حساب مخصص</li> <li><span class="feature-check"></span> مدير حساب مخصص</li>
<li><span class="feature-check"></span> API كامل للتطبيق</li>
</ul> </ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a> <a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a>
</div> </div>
<div class="price-card">
<div class="price-name">مؤسسية</div>
<div class="price-amount">249 <span>دينار/شهر</span></div>
<div class="price-desc">للمؤسسات الكبرى</div>
<ul class="price-features">
<li><span class="feature-check"></span> شركات بلا حدود</li>
<li><span class="feature-check"></span> فواتير بلا حدود</li>
<li><span class="feature-check"></span> مستخدمين بلا حدود</li>
<li><span class="feature-check"></span> SLA مضمون 99.9%</li>
<li><span class="feature-check"></span> تدريب + نسخ احتياطي</li>
</ul>
<a href="/register.php" class="btn btn-outline" style="width:100%">تواصل معنا</a>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1868,8 +1868,8 @@
<!-- Invoices --> <!-- Invoices -->
<div class="usage-card"> <div class="usage-card">
<div style="display:flex; justify-content:space-between; margin-bottom:6px;"> <div style="display:flex; justify-content:space-between; margin-bottom:6px;">
<span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 الفواتير <span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 رصيد الفواتير
الشهرية</span> <span x-text="subscription?.billing_cycle === 'monthly' ? '(شهري)' : '(سنوي)'"></span></span>
<span class="num-font" style="font-weight:700; color:var(--green-mid);" <span class="num-font" style="font-weight:700; color:var(--green-mid);"
x-text="(subscription?.invoices?.used || 0) + ' من ' + (subscription?.invoices?.limit || 0)"></span> x-text="(subscription?.invoices?.used || 0) + ' من ' + (subscription?.invoices?.limit || 0)"></span>
</div> </div>
@@ -1952,6 +1952,22 @@
</p> </p>
</div> </div>
<!-- Cycle Toggle -->
<div style="display:flex; justify-content:center; margin-bottom:36px;">
<div style="background:var(--bg-secondary); padding:5px; border-radius:12px; display:flex; gap:5px; border:1px solid var(--border);">
<button @click="billingCycle = 'monthly'"
:class="billingCycle === 'monthly' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع شهري
</button>
<button @click="billingCycle = 'annual'"
:class="billingCycle === 'annual' ? 'btn-navy' : 'btn-ghost'"
style="font-size:13px; padding:8px 20px; border-radius:8px; transition:all 0.2s;">
دفع سنوي (توفير )
</button>
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(290px, 1fr)); gap:24px;"> <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(290px, 1fr)); gap:24px;">
<template x-for="p in plans" :key="p.id"> <template x-for="p in plans" :key="p.id">
<div class="plan-card" :class="subscription?.plan_id === p.id ? 'active-plan' : ''"> <div class="plan-card" :class="subscription?.plan_id === p.id ? 'active-plan' : ''">
@@ -1970,9 +1986,9 @@
style="text-align:center; padding:18px 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border);"> style="text-align:center; padding:18px 0; border-top:1px solid var(--border); border-bottom:1px solid var(--border);">
<span class="num-font" <span class="num-font"
style="font-size:46px; font-weight:800; color:var(--green-mid);" style="font-size:46px; font-weight:800; color:var(--green-mid);"
x-text="p.price_jod"></span> x-text="billingCycle === 'monthly' ? (p.price_monthly_jod || p.price_jod) : (p.price_annual_jod || (p.price_jod * 10))"></span>
<span style="font-size:15px; color:var(--text-3); font-weight:500;"> دينار / <span style="font-size:15px; color:var(--text-3); font-weight:500;"> دينار /
شهر</span> <span x-text="billingCycle === 'monthly' ? 'شهر' : 'سنة'"></span></span>
</div> </div>
<ul <ul
@@ -2712,10 +2728,9 @@
class="form-input" required> class="form-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">رقم الهاتف <span <label class="form-label">رقم الهاتف</label>
class="form-label-sub">(اختياري)</span></label>
<input type="text" x-model="newTenant.phone" placeholder="+962 7x xxx xxxx" <input type="text" x-model="newTenant.phone" placeholder="+962 7x xxx xxxx"
class="form-input"> class="form-input" required>
</div> </div>
</div> </div>
</div> </div>
@@ -2731,13 +2746,8 @@
<input type="text" x-model="newTenant.manager_name" placeholder="الاسم الكامل" <input type="text" x-model="newTenant.manager_name" placeholder="الاسم الكامل"
class="form-input" required> class="form-input" required>
</div> </div>
<div class="form-group"> <div class="form-group" style="grid-column:1/-1;">
<label class="form-label">بريد المدير الإلكتروني</label> <label class="form-label">كلمة مرور الدخول للمدير</label>
<input type="email" x-model="newTenant.manager_email" placeholder="manager@office.com"
class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">كلمة مرور الدخول</label>
<input type="password" x-model="newTenant.manager_password" placeholder="••••••••" <input type="password" x-model="newTenant.manager_password" placeholder="••••••••"
class="form-input" required> class="form-input" required>
</div> </div>
@@ -2928,11 +2938,13 @@
isUploadingBatch: false, batchProgress: { total: 0, current: 0 }, isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false, showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
acknowledgedWarnings: false, isEditingInvoice: false, acknowledgedWarnings: false, isEditingInvoice: false,
isBusy: false, globalError: '', isBusy: false,
billingCycle: 'annual', // 'monthly' or 'annual'
globalError: '',
newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' }, newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' }, newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' },
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' }, newTenant: { name: '', email: '', phone: '', manager_name: '', manager_password: '' },
connectData: { client_id: '', secret_key: '', income_source_sequence: '1' }, connectData: { client_id: '', secret_key: '', income_source_sequence: '1' },
uploadData: { company_id: '' }, uploadData: { company_id: '' },
currentCompany: null, currentInvoice: null, companyStats: null, currentCompany: null, currentInvoice: null, companyStats: null,
@@ -3103,7 +3115,7 @@
const res = await this.apiRequest('v1/tenants/create', 'POST', this.newTenant); const res = await this.apiRequest('v1/tenants/create', 'POST', this.newTenant);
if (res) { if (res) {
this.showAddTenantModal = false; this.showAddTenantModal = false;
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' }; this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_password: '' };
this.loadAll(); this.loadAll();
alert('تم إضافة المكتب المحاسبي والمدير المسؤول بنجاح'); alert('تم إضافة المكتب المحاسبي والمدير المسؤول بنجاح');
} }
@@ -3400,7 +3412,10 @@
const res = await fetch('/index.php?route=v1/payments/create', { const res = await fetch('/index.php?route=v1/payments/create', {
method: 'POST', method: 'POST',
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: plan.id }) body: JSON.stringify({
plan_id: plan.id,
billing_cycle: this.billingCycle
})
}); });
const json = await res.json(); const json = await res.json();
this.isBusy = false; this.isBusy = false;

47
update_annual_plans.sql Normal file
View File

@@ -0,0 +1,47 @@
-- 1. تعطيل الباقات القديمة التي لم نعد نستخدمها
UPDATE subscription_plans SET is_active = 0 WHERE id IN ('office', 'enterprise');
-- 2. تحديث الباقات الحالية إلى باقات سنوية بالأسعار والأرقام الجديدة
UPDATE subscription_plans
SET
name_ar = 'الباقة الأساسية (سنوي)',
name_en = 'Basic Plan (Annual)',
price_jod = 120.00,
max_invoices_month = 12000,
max_companies = 1,
max_users = 1,
is_active = 1
WHERE id = 'basic';
UPDATE subscription_plans
SET
name_ar = 'الباقة الاحترافية (سنوي)',
name_en = 'Pro Plan (Annual)',
price_jod = 250.00,
max_invoices_month = 50000,
max_companies = 9999, -- للشركات غير المحدودة
max_users = 5,
is_active = 1
WHERE id = 'pro';
-- 3. إبقاء الباقة المجانية كما هي
UPDATE subscription_plans
SET
name_ar = 'التجربة المجانية',
name_en = 'Free Trial',
price_jod = 0.00,
max_invoices_month = 15,
max_companies = 1,
max_users = 1,
is_active = 1
WHERE id = 'free';
-- 4. ترحيل وتحديث بيانات العملاء المشتركين حالياً
UPDATE subscriptions s
JOIN subscription_plans sp ON s.plan_id = sp.id
SET
s.max_invoices_per_month = sp.max_invoices_month,
s.max_companies = sp.max_companies,
s.max_users = sp.max_users,
-- تمديد فترة الفوترة للمشتركين المدفوعين لتصبح سنة كاملة من تاريخ بدايتها
s.current_period_end = IF(s.plan_id != 'free', DATE_ADD(s.current_period_start, INTERVAL 1 YEAR), s.current_period_end);

6
update_plans.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/app/bootstrap/init.php';
use App\Core\Database;
$db = Database::getInstance();
$plans = $db->query("SELECT * FROM subscription_plans")->fetchAll();
print_r($plans);

View File

@@ -0,0 +1,46 @@
-- 1. إضافة أعمدة الأسعار (شهري وسنوي) وجدولة الفواتير
ALTER TABLE subscription_plans
ADD COLUMN price_annual_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_jod,
ADD COLUMN price_monthly_jod DECIMAL(10,2) DEFAULT 0.00 AFTER price_annual_jod;
-- 2. إضافة دورة الفوترة لجدول الاشتراكات وطلبات الدفع
ALTER TABLE subscriptions
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER status;
ALTER TABLE payment_requests
ADD COLUMN billing_cycle ENUM('monthly', 'annual') DEFAULT 'annual' AFTER plan_id;
-- 3. تحديث الباقات بالقيم الجديدة المقترحة في التحليل الاستراتيجي
-- الباقة الأساسية
UPDATE subscription_plans
SET
name_ar = 'الباقة الأساسية',
price_annual_jod = 120.00,
price_monthly_jod = 15.00,
max_invoices_month = 500, -- تم تخفيضها من 12000 للتحويل المستقبلي
max_companies = 3, -- تم زيادتها من 1 لجذب المحاسبين المستقلين
max_users = 2,
is_active = 1
WHERE id = 'basic';
-- الباقة الاحترافية
UPDATE subscription_plans
SET
name_ar = 'الباقة الاحترافية',
price_annual_jod = 290.00,
price_monthly_jod = 35.00,
max_invoices_month = 3000,
max_companies = 9999,
max_users = 5,
is_active = 1
WHERE id = 'pro';
-- الباقة المجانية
UPDATE subscription_plans
SET
price_annual_jod = 0.00,
price_monthly_jod = 0.00,
max_invoices_month = 15,
max_companies = 1,
max_users = 1
WHERE id = 'free';