Compare commits
7 Commits
bddee7ca2d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f62455113 | ||
|
|
2f1a6f9c85 | ||
|
|
9ad361e992 | ||
|
|
aceb7d324f | ||
|
|
24a9f064a1 | ||
|
|
663896becb | ||
|
|
e93f1d4f34 |
@@ -31,7 +31,7 @@ final class QuotaMiddleware
|
||||
|
||||
// Fetch subscription with plan info
|
||||
$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
|
||||
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
|
||||
WHERE s.tenant_id = ?
|
||||
@@ -57,10 +57,12 @@ final class QuotaMiddleware
|
||||
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()) {
|
||||
$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("
|
||||
UPDATE subscriptions
|
||||
@@ -76,15 +78,15 @@ final class QuotaMiddleware
|
||||
$sub['current_period_start'] = $newStart;
|
||||
$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
|
||||
$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) {
|
||||
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [
|
||||
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
|
||||
'quota_type' => 'invoices',
|
||||
'used' => $used,
|
||||
'limit' => $limit,
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
return [
|
||||
'free' => [
|
||||
'id' => 'free',
|
||||
'name_ar' => 'مجانية',
|
||||
'name_en' => 'Free',
|
||||
'name_ar' => 'التجربة المجانية',
|
||||
'name_en' => 'Free Trial',
|
||||
'max_companies' => 1,
|
||||
'max_invoices_month' => 15,
|
||||
'max_users' => 1,
|
||||
'price_jod' => 0.00,
|
||||
'price_monthly_jod' => 0.00,
|
||||
'price_annual_jod' => 0.00,
|
||||
'ai_features' => true,
|
||||
'jofotara_enabled' => true,
|
||||
'badge_color' => 'gray',
|
||||
@@ -29,90 +31,50 @@ return [
|
||||
],
|
||||
'basic' => [
|
||||
'id' => 'basic',
|
||||
'name_ar' => 'أساسية',
|
||||
'name_en' => 'Basic',
|
||||
'name_ar' => 'الباقة الأساسية',
|
||||
'name_en' => 'Basic Plan',
|
||||
'max_companies' => 3,
|
||||
'max_invoices_month' => 100,
|
||||
'max_users' => 3,
|
||||
'price_jod' => 15.00,
|
||||
'max_invoices_month' => 500,
|
||||
'max_users' => 2,
|
||||
'price_jod' => 15.00, // Default legacy price
|
||||
'price_monthly_jod' => 15.00,
|
||||
'price_annual_jod' => 120.00,
|
||||
'ai_features' => true,
|
||||
'jofotara_enabled' => true,
|
||||
'badge_color' => 'blue',
|
||||
'description_ar' => 'للمحاسبين المستقلين — 3 شركات',
|
||||
'description_ar' => 'للمحاسبين المستقلين والشركات الصغيرة — 3 شركات',
|
||||
'features' => [
|
||||
'استخراج الفواتير بالذكاء الاصطناعي',
|
||||
'الربط المباشر مع جوفوترة',
|
||||
'حتى 3 شركات',
|
||||
'100 فاتورة شهرياً',
|
||||
'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 مستخدمين',
|
||||
'تقارير متقدمة + تصدير',
|
||||
'دعم فني بالأولوية',
|
||||
'حتى 3 شركات (بدلاً من واحدة)',
|
||||
'500 فاتورة شهرياً (سخية جداً)',
|
||||
'مستخدمين اثنين',
|
||||
'دعم فني عبر الواتساب',
|
||||
],
|
||||
],
|
||||
'pro' => [
|
||||
'id' => 'pro',
|
||||
'name_ar' => 'احترافية',
|
||||
'name_en' => 'Pro',
|
||||
'max_companies' => 25,
|
||||
'max_invoices_month' => 2000,
|
||||
'max_users' => 25,
|
||||
'price_jod' => 99.00,
|
||||
'ai_features' => true,
|
||||
'jofotara_enabled' => true,
|
||||
'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,
|
||||
'name_ar' => 'الباقة الاحترافية',
|
||||
'name_en' => 'Pro Plan',
|
||||
'max_companies' => 9999,
|
||||
'max_invoices_month' => 3000,
|
||||
'max_users' => 5,
|
||||
'price_jod' => 35.00, // Default legacy price
|
||||
'price_monthly_jod' => 35.00,
|
||||
'price_annual_jod' => 290.00,
|
||||
'ai_features' => true,
|
||||
'jofotara_enabled' => true,
|
||||
'badge_color' => 'gold',
|
||||
'description_ar' => 'للمؤسسات — بلا حدود مع دعم مخصص',
|
||||
'is_popular' => true,
|
||||
'description_ar' => 'للمكاتب الكبيرة والموزعين — حجم عمل ضخم',
|
||||
'features' => [
|
||||
'كل ميزات الاحترافية',
|
||||
'شركات وفواتير بلا حدود عملية',
|
||||
'مستخدمين بلا حدود',
|
||||
'SLA مضمون 99.9%',
|
||||
'ربط API مخصص',
|
||||
'تدريب فريق المحاسبة',
|
||||
'نسخ احتياطي مخصص',
|
||||
'استخراج الفواتير بالذكاء الاصطناعي',
|
||||
'الربط المباشر مع جوفوترة',
|
||||
'عدد شركات غير محدود',
|
||||
'3,000 فاتورة شهرياً',
|
||||
'5 مستخدمين',
|
||||
'API كامل لتطبيق الهاتف',
|
||||
'مدير حساب مخصص',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -31,7 +31,7 @@ final class QuotaMiddleware
|
||||
|
||||
// Fetch subscription with plan info
|
||||
$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
|
||||
LEFT JOIN subscription_plans sp ON s.plan_id = sp.id
|
||||
WHERE s.tenant_id = ?
|
||||
@@ -57,10 +57,12 @@ final class QuotaMiddleware
|
||||
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()) {
|
||||
$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("
|
||||
UPDATE subscriptions
|
||||
@@ -76,15 +78,15 @@ final class QuotaMiddleware
|
||||
$sub['current_period_start'] = $newStart;
|
||||
$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
|
||||
$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) {
|
||||
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة هذا الشهر (' . $limit . ' فاتورة). يرجى ترقية باقتك.', 429, [
|
||||
json_error('لقد وصلت للحد الأقصى من الفواتير المسموحة في باقتك الحالية (' . $limit . ' فاتورة). يرجى ترقية باقتك للاستمرار.', 429, [
|
||||
'quota_type' => 'invoices',
|
||||
'used' => $used,
|
||||
'limit' => $limit,
|
||||
|
||||
@@ -39,8 +39,71 @@ if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
json_error('بيانات الدخول غير صحيحة', 401);
|
||||
}
|
||||
|
||||
// 3. Handle device registration if provided (for mobile app login)
|
||||
$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';
|
||||
$deviceSecret = null;
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ $db = Database::getInstance();
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
$planId = $data['plan_id'];
|
||||
$cycle = $data['billing_cycle'] ?? 'annual'; // Default to annual
|
||||
|
||||
if (!in_array($cycle, ['monthly', 'annual'])) {
|
||||
json_error('دورة الفوترة غير صالحة.', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Get plan details
|
||||
@@ -45,6 +50,9 @@ try {
|
||||
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
|
||||
$stmt = $db->prepare("SELECT id FROM payment_requests WHERE tenant_id = ? AND status = 'pending' LIMIT 1");
|
||||
$stmt->execute([$tenantId]);
|
||||
@@ -68,15 +76,16 @@ try {
|
||||
// 6. Create payment request
|
||||
$paymentId = Database::generateUuid();
|
||||
$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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())
|
||||
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())
|
||||
");
|
||||
$stmt->execute([
|
||||
$paymentId,
|
||||
$tenantId,
|
||||
$userId,
|
||||
$planId,
|
||||
$plan['price_jod'],
|
||||
$cycle,
|
||||
$amount,
|
||||
$referenceNumber,
|
||||
$cliqAlias,
|
||||
$user['name'] ?? ''
|
||||
@@ -88,17 +97,17 @@ try {
|
||||
$tenantId,
|
||||
$userId,
|
||||
$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([
|
||||
'payment_id' => $paymentId,
|
||||
'reference_number' => $referenceNumber,
|
||||
'cliq_alias' => $cliqAlias,
|
||||
'amount_jod' => (float)$plan['price_jod'],
|
||||
'plan_name' => $plan['name_ar'] ?? $plan['name_en'],
|
||||
'amount_jod' => (float)$amount,
|
||||
'plan_name' => ($plan['name_ar'] ?? $plan['name_en']) . " (" . ($cycle === 'monthly' ? 'شهري' : 'سنوي') . ")",
|
||||
'payer_name' => $user['name'] ?? '',
|
||||
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$plan['price_jod']} دينار أردني.",
|
||||
'instructions' => "قم بالتحويل عبر CliQ إلى الاسم المستعار: {$cliqAlias} بمبلغ {$amount} دينار أردني.",
|
||||
], 'تم إنشاء طلب الدفع بنجاح');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -47,18 +47,33 @@ try {
|
||||
$plan = $stmt->fetch();
|
||||
|
||||
if ($plan) {
|
||||
$cycle = $payment['billing_cycle'] ?? 'annual';
|
||||
$startDate = date('Y-m-d H:i:s');
|
||||
|
||||
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("
|
||||
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)
|
||||
VALUES (:t_id, :p_id, :max_c, :max_i, :max_u, :price, 'active', :start, :end, NOW())
|
||||
INSERT INTO subscriptions (
|
||||
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
|
||||
plan_id = VALUES(plan_id),
|
||||
max_companies = VALUES(max_companies),
|
||||
max_invoices_per_month = VALUES(max_invoices_per_month),
|
||||
max_users = VALUES(max_users),
|
||||
price_jod = VALUES(price_jod),
|
||||
billing_cycle = VALUES(billing_cycle),
|
||||
status = 'active',
|
||||
current_period_start = VALUES(current_period_start),
|
||||
current_period_end = VALUES(current_period_end),
|
||||
@@ -68,9 +83,10 @@ try {
|
||||
't_id' => $payment['tenant_id'],
|
||||
'p_id' => $plan['id'],
|
||||
'max_c' => $plan['max_companies'],
|
||||
'max_i' => $plan['max_invoices_month'],
|
||||
'max_i' => $maxInvoices,
|
||||
'max_u' => $plan['max_users'],
|
||||
'price' => $plan['price_jod'],
|
||||
'price' => $price,
|
||||
'cycle' => $cycle,
|
||||
'start' => $startDate,
|
||||
'end' => $endDate
|
||||
]);
|
||||
|
||||
@@ -13,7 +13,7 @@ $db = Database::getInstance();
|
||||
try {
|
||||
$stmt = $db->query("
|
||||
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
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC
|
||||
@@ -36,6 +36,8 @@ try {
|
||||
$plan['max_invoices_month'] = (int)$plan['max_invoices_month'];
|
||||
$plan['max_users'] = (int)$plan['max_users'];
|
||||
$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['jofotara_enabled'] = (bool)$plan['jofotara_enabled'];
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ $data = input();
|
||||
$errors = Validator::validate($data, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email',
|
||||
'phone' => 'required',
|
||||
'manager_name' => 'required',
|
||||
'manager_email' => 'required|email',
|
||||
'manager_password' => 'required'
|
||||
]);
|
||||
|
||||
@@ -43,12 +43,23 @@ try {
|
||||
$encryptedTenantName = \App\Core\Encryption::encrypt($data['name']);
|
||||
$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->execute([
|
||||
$tenantId,
|
||||
$encryptedTenantName,
|
||||
$encryptedTenantEmail,
|
||||
$data['phone'] ?? null
|
||||
$phone
|
||||
]);
|
||||
|
||||
// Generate User UUID
|
||||
@@ -60,17 +71,19 @@ try {
|
||||
|
||||
// Encrypt sensitive user data
|
||||
$encryptedName = \App\Core\Encryption::encrypt($data['manager_name']);
|
||||
$encryptedEmail = \App\Core\Encryption::encrypt($data['manager_email']);
|
||||
$emailHash = hash('sha256', strtolower($data['manager_email']));
|
||||
$encryptedEmail = \App\Core\Encryption::encrypt($data['email']);
|
||||
$emailHash = hash('sha256', strtolower($data['email']));
|
||||
|
||||
// 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([
|
||||
$userId,
|
||||
$tenantId,
|
||||
$encryptedName,
|
||||
$encryptedEmail,
|
||||
$emailHash,
|
||||
$encryptedPhone,
|
||||
$phoneHash,
|
||||
password_hash($data['manager_password'], PASSWORD_DEFAULT)
|
||||
]);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<!-- Camera & Storage (Invoice Scanning & Picking) -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<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"/>
|
||||
|
||||
<!-- Audio (Voice Assistant) -->
|
||||
|
||||
@@ -30,9 +30,9 @@ class AuthController extends GetxController {
|
||||
normalizedPhone = normalizedPhone.substring(1);
|
||||
}
|
||||
if (normalizedPhone.startsWith('07')) {
|
||||
normalizedPhone = '962' + normalizedPhone.substring(1);
|
||||
normalizedPhone = '962${normalizedPhone.substring(1)}';
|
||||
} else if (normalizedPhone.startsWith('7')) {
|
||||
normalizedPhone = '962' + normalizedPhone;
|
||||
normalizedPhone = '962$normalizedPhone';
|
||||
}
|
||||
|
||||
phone.value = normalizedPhone;
|
||||
@@ -111,7 +111,8 @@ class AuthController extends GetxController {
|
||||
}
|
||||
} on DioException catch (e, stackTrace) {
|
||||
AppLogger.error('OTP Verify Failed', e.response?.data, stackTrace);
|
||||
AppSnackbar.showError('خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
|
||||
AppSnackbar.showError(
|
||||
'خطأ', e.response?.data['message'] ?? 'رمز التحقق غير صحيح');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -120,7 +121,8 @@ class AuthController extends GetxController {
|
||||
Future<void> loginWithEmail(String email, String password) async {
|
||||
try {
|
||||
if (email.trim().isEmpty || password.trim().isEmpty) {
|
||||
AppSnackbar.showError('خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور');
|
||||
AppSnackbar.showError(
|
||||
'خطأ', 'الرجاء إدخال البريد الإلكتروني وكلمة المرور');
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
@@ -150,9 +152,18 @@ class AuthController extends GetxController {
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
AppLogger.print('Email Login Success. Tokens received.');
|
||||
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
|
||||
await _storage.saveToken(data['access_token']);
|
||||
// Note: auth/login might not return device_secret, handle if missing
|
||||
|
||||
@@ -6,7 +6,7 @@ class PhoneInputView extends StatelessWidget {
|
||||
PhoneInputView({super.key});
|
||||
|
||||
final AuthController controller = Get.put(AuthController());
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController emailController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
@@ -37,31 +37,25 @@ class PhoneInputView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'أدخل رقم هاتفك أو البريد الإلكتروني لتسجيل الدخول',
|
||||
'أدخل البريد الإلكتروني وكلمة المرور لتسجيل الدخول',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textDirection: TextDirection.ltr,
|
||||
onChanged: (val) => controller.phone.value = val,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الهاتف أو البريد الإلكتروني',
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
labelText: 'البريد الإلكتروني',
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Obx(() {
|
||||
final isEmail = controller.phone.value.contains('@');
|
||||
if (!isEmail) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
@@ -75,9 +69,6 @@ class PhoneInputView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Obx(() => ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
@@ -88,22 +79,16 @@ class PhoneInputView extends StatelessWidget {
|
||||
onPressed: controller.isLoading.value
|
||||
? null
|
||||
: () {
|
||||
if (controller.phone.value.contains('@')) {
|
||||
controller.loginWithEmail(
|
||||
controller.phone.value,
|
||||
emailController.text,
|
||||
passwordController.text
|
||||
);
|
||||
} else {
|
||||
controller.requestOtp(phoneController.text);
|
||||
}
|
||||
},
|
||||
child: controller.isLoading.value
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: Text(
|
||||
controller.phone.value.contains('@')
|
||||
? 'تسجيل الدخول'
|
||||
: 'إرسال رمز التحقق',
|
||||
style: const TextStyle(fontSize: 16)
|
||||
: const Text(
|
||||
'تسجيل الدخول',
|
||||
style: TextStyle(fontSize: 16)
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
||||
@@ -254,6 +254,7 @@ class DashboardView extends GetView<DashboardController> {
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.AUDIT_LOG),
|
||||
),
|
||||
if (!GetPlatform.isIOS) ...[
|
||||
const SizedBox(width: 12),
|
||||
_buildAdminActionCard(
|
||||
'ادعُ واكسب',
|
||||
@@ -262,6 +263,7 @@ class DashboardView extends GetView<DashboardController> {
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.REFERRAL),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
_buildAdminActionCard(
|
||||
'استهلاك AI',
|
||||
|
||||
@@ -5,7 +5,7 @@ import '../../../app/routes/app_pages.dart';
|
||||
class OnboardingController extends GetxController {
|
||||
var currentPage = 0.obs;
|
||||
|
||||
final List<OnboardingModel> items = [
|
||||
List<OnboardingModel> get items => [
|
||||
OnboardingModel(
|
||||
title: 'مرحباً بك في مُصادَق',
|
||||
description:
|
||||
@@ -19,9 +19,10 @@ class OnboardingController extends GetxController {
|
||||
imageAsset: 'assets/images/onboarding_2.png',
|
||||
),
|
||||
OnboardingModel(
|
||||
title: 'مدفوعات فورية آمنة',
|
||||
description:
|
||||
'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.',
|
||||
title: 'إدارة متكاملة لشركتك',
|
||||
description: GetPlatform.isIOS
|
||||
? 'إدارة فواتير الشركات وأرشفة ضريبية متكاملة بأمان وسهولة تامة.'
|
||||
: 'قم بشحن محفظتك وتفعيل اشتراكك عبر نظام كليك (CliQ) بكل سرعة وأمان.',
|
||||
imageAsset: 'assets/images/onboarding_3.png',
|
||||
),
|
||||
];
|
||||
|
||||
@@ -166,7 +166,7 @@ class SettingsView extends GetView<SettingsController> {
|
||||
_buildInfoTile(
|
||||
icon: Icons.diamond_rounded,
|
||||
title: 'الاشتراكات والباقات',
|
||||
trailing: 'ترقية →',
|
||||
trailing: GetPlatform.isIOS ? 'التفاصيل →' : 'ترقية →',
|
||||
isDark: isDark,
|
||||
onTap: () => Get.toNamed(AppRoutes.SUBSCRIPTION),
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ class SubscriptionController extends GetxController {
|
||||
var isLoading = true.obs;
|
||||
var isCreatingPayment = false.obs;
|
||||
var activePaymentRequest = Rxn<Map<String, dynamic>>();
|
||||
var isAnnual = true.obs; // Toggle between Monthly and Annual
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@@ -69,7 +70,11 @@ class SubscriptionController extends GetxController {
|
||||
Future<Map<String, dynamic>?> createPaymentRequest(String planId) async {
|
||||
try {
|
||||
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) {
|
||||
final result = Map<String, dynamic>.from(res.data['data']);
|
||||
activePaymentRequest.value = result;
|
||||
|
||||
@@ -34,15 +34,28 @@ class SubscriptionView extends StatelessWidget {
|
||||
children: [
|
||||
// Current Subscription Status
|
||||
if (controller.currentSubscription.value != null)
|
||||
_buildCurrentPlan(controller.currentSubscription.value!, isDark),
|
||||
_buildCurrentPlan(controller.currentSubscription.value!, isDark)
|
||||
else
|
||||
_buildFreePlanPlaceholder(isDark),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (GetPlatform.isIOS) ...[
|
||||
_buildIOSB2BInfoCard(isDark),
|
||||
],
|
||||
|
||||
if (!GetPlatform.isIOS) ...[
|
||||
// Active Payment Request Banner
|
||||
if (controller.activePaymentRequest.value != null)
|
||||
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
|
||||
|
||||
// Plans Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22),
|
||||
@@ -62,7 +75,69 @@ class SubscriptionView extends StatelessWidget {
|
||||
'ادفع عبر CliQ — بدون عمولة!',
|
||||
style: TextStyle(fontSize: 13, color: isDark ? Colors.white38 : Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
@@ -75,6 +150,7 @@ class SubscriptionView extends StatelessWidget {
|
||||
const SizedBox(height: 12),
|
||||
...controller.myPayments.map((p) => _buildPaymentHistoryItem(p, isDark)),
|
||||
],
|
||||
],
|
||||
|
||||
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) {
|
||||
final planName = sub['plan_name'] ?? sub['plan_name_en'] ?? sub['plan_id'] ?? 'مجانية';
|
||||
final daysLeft = sub['days_remaining'] ?? 0;
|
||||
@@ -228,10 +382,20 @@ class SubscriptionView extends StatelessWidget {
|
||||
|
||||
Widget _buildPlanCard(Map<String, dynamic> plan, SubscriptionController ctrl, bool isDark) {
|
||||
final isPopular = plan['is_popular'] == true;
|
||||
final price = (plan['price_jod'] ?? 0).toString();
|
||||
final features = (plan['features'] as List?)?.cast<String>() ?? [];
|
||||
final nameAr = plan['name_ar'] ?? plan['name_en'] ?? 'باقة';
|
||||
|
||||
return 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(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -271,7 +435,7 @@ class SubscriptionView extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: price, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w900, color: isDark ? const Color(0xFF5EEAD4) : const Color(0xFF0F4C81))),
|
||||
TextSpan(text: ' JOD', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
||||
TextSpan(text: ' JOD / $cycleText', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -302,7 +466,7 @@ class SubscriptionView extends StatelessWidget {
|
||||
children: [
|
||||
_buildPlanStat(Icons.business, '${plan['max_companies'] ?? 0} شركات'),
|
||||
const SizedBox(width: 8),
|
||||
_buildPlanStat(Icons.receipt_long, '${plan['max_invoices_month'] ?? 0} فاتورة/شهر'),
|
||||
_buildPlanStat(Icons.receipt_long, '$invoiceLimit فاتورة/$cycleText'),
|
||||
const SizedBox(width: 8),
|
||||
_buildPlanStat(Icons.people, '${plan['max_users'] ?? 0} مستخدمين'),
|
||||
],
|
||||
@@ -338,6 +502,7 @@ class SubscriptionView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPlanStat(IconData icon, String text) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import 'tenants_management_controller.dart';
|
||||
class AddTenantController extends GetxController {
|
||||
final nameController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final managerNameController = TextEditingController();
|
||||
final managerEmailController = TextEditingController();
|
||||
final managerPasswordController = TextEditingController();
|
||||
|
||||
var isSubmitting = false.obs;
|
||||
@@ -20,8 +20,8 @@ class AddTenantController extends GetxController {
|
||||
void onClose() {
|
||||
nameController.dispose();
|
||||
emailController.dispose();
|
||||
phoneController.dispose();
|
||||
managerNameController.dispose();
|
||||
managerEmailController.dispose();
|
||||
managerPasswordController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
@@ -29,11 +29,11 @@ class AddTenantController extends GetxController {
|
||||
Future<void> submit() async {
|
||||
final name = nameController.text.trim();
|
||||
final email = emailController.text.trim();
|
||||
final phone = phoneController.text.trim();
|
||||
final managerName = managerNameController.text.trim();
|
||||
final managerEmail = managerEmailController.text.trim();
|
||||
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('تنبيه', 'الرجاء إدخال جميع البيانات المطلوبة');
|
||||
return;
|
||||
}
|
||||
@@ -43,8 +43,8 @@ class AddTenantController extends GetxController {
|
||||
final response = await _dio.post('tenants/create', data: {
|
||||
'name': name,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'manager_name': managerName,
|
||||
'manager_email': managerEmail,
|
||||
'manager_password': managerPassword,
|
||||
});
|
||||
|
||||
|
||||
@@ -36,29 +36,29 @@ class AddTenantView extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
controller: controller.emailController,
|
||||
label: 'البريد الإلكتروني للمكتب',
|
||||
label: 'البريد الإلكتروني للعمل',
|
||||
icon: Icons.email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'بيانات مدير المكتب',
|
||||
'بيانات مدير المكتب المسؤول',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
controller: controller.managerNameController,
|
||||
label: 'اسم المدير',
|
||||
label: 'اسم المدير الكامل',
|
||||
icon: Icons.person,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
controller: controller.managerEmailController,
|
||||
label: 'البريد الإلكتروني للمدير',
|
||||
icon: Icons.alternate_email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
controller: controller.phoneController,
|
||||
label: 'رقم هاتف المدير (لتسجيل الدخول OTP)',
|
||||
icon: Icons.phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -5,26 +5,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
|
||||
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "88.0.0"
|
||||
version: "93.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad
|
||||
sha256: "8f89e371e2883de35cdc78f648e725fa4da5f3b6c927269f00fa68f1ea92b598"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.69"
|
||||
version: "1.3.71"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
|
||||
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
version: "10.0.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -77,18 +77,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
|
||||
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.0.6"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
|
||||
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -97,30 +97,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
|
||||
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.1"
|
||||
version: "2.15.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -217,14 +201,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.1"
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -293,18 +277,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cunning_document_scanner
|
||||
sha256: bf590e8c8c8a4903ba7873c4f22b67e976604853f11065f594cb19b408bd25ef
|
||||
sha256: de0c0705799f7d5cc9b82b67bfb8b3e965a1fbff4afbd70ea10cd1dad4f3a98c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.4.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
|
||||
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.7"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -405,10 +389,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+4"
|
||||
version: "0.9.5"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -429,50 +413,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158
|
||||
sha256: "93a5bde9775fd5adcc937f39dfa04ae0bc89c4d79bea6abc49de3f7b049d9ff6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.9.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||
sha256: "4a120366dbf7d5a8ee9438978530b664b855728fb8dcc3a201017660817e555b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
version: "7.0.1"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c
|
||||
sha256: "7c98f10b8c8e5adedc0b810b66a877120696675e2c22d9ca9caca092da0d9e57"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
version: "3.7.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a
|
||||
sha256: "8d0dc81a31cd030170508dc3e89bfd14355b20a1b991340af5f018e37daab5d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.2.0"
|
||||
version: "16.2.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a"
|
||||
sha256: "37abb0b0535c5497605ee94c12470e1ebbbe47e71a22d0c20bffcc912311f8cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.9"
|
||||
version: "4.7.11"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1"
|
||||
sha256: "54e22b43e2c26a2728a3f68c188de0f9011993ae19ae959a06d476dad935c776"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.5"
|
||||
version: "4.1.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -485,10 +469,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flat_buffers
|
||||
sha256: "380bdcba5664a718bfd4ea20a45d39e13684f5318fcd8883066a55e21f37f4c3"
|
||||
sha256: "7c1de2d6eb5f3e61e5c50040841109f509deaaf2b12ec0d57b92456d9ea50345"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "23.5.26"
|
||||
version: "25.9.23"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -570,26 +554,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
|
||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.31"
|
||||
version: "2.0.34"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "8b302d17096ba88f911b7eb317c71d5e691da60a259549f42b38c658d1776d87"
|
||||
sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.0"
|
||||
version: "10.2.0"
|
||||
flutter_secure_storage_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_darwin
|
||||
sha256: "3af15a3cb2bf5b8b776832bd01776f8018766aece55623176e28b406481fb320"
|
||||
sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.3.1"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -636,18 +620,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freerasp
|
||||
sha256: "76a3fb6f8e3fdd7d83e224866998523e7fb79d5779321983e484a6cfbf4b01b5"
|
||||
sha256: "5b9a3402a7a30d928897e2264e2700a30db1df14de289f500b3a0cf50dc19df2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.12.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "7.5.1"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -672,6 +648,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -708,26 +692,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
version: "4.8.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||
sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||
sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
version: "0.8.13+17"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -740,10 +724,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13"
|
||||
version: "0.8.13+6"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -756,10 +740,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.2.2+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -792,14 +776,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
version: "4.12.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -844,18 +844,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.52"
|
||||
version: "1.0.56"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
|
||||
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
version: "1.6.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -884,10 +884,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: lottie
|
||||
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
|
||||
sha256: "8b6359a7422167014aa73ce763fa133fb832065dcc0ac4d1dec1f603a5cef7d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.3.3"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -924,10 +924,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -940,26 +948,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: objectbox
|
||||
sha256: "3cc186749178a3556e1020c9082d0897d0f9ecbdefcc27320e65c5bc650f0e57"
|
||||
sha256: "83d58e0ab5c4180a2f67086c449a4f2d1475932b31819271923dfc82a76f73c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.1"
|
||||
version: "5.3.1"
|
||||
objectbox_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: objectbox_flutter_libs
|
||||
sha256: cd754766e04229a4f51250f121813d9a3c1a74fc21cd68e48b3c6085cbcd6c85
|
||||
sha256: fd4e8ed03e2b2bfeb8aa965435d7c5523ec7e962f1ffecf6e7da6c1f4d170419
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.1"
|
||||
version: "5.3.1"
|
||||
objectbox_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: objectbox_generator
|
||||
sha256: "71a3f6948e631be5c7160d512ad2a8cb7471cdbcf1731ec6baf2a794b82386d7"
|
||||
sha256: daa95f21c7140c619ffc1abee2465541d4f0317b8c3bbf2141be1a9e5c507a1d
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1020,18 +1036,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.19"
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1060,10 +1076,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
version: "3.12.0"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1072,30 +1088,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1256,6 +1264,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1337,66 +1353,66 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
|
||||
sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.2.3"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
speech_to_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: speech_to_text
|
||||
sha256: c07557664974afa061f221d0d4186935bea4220728ea9446702825e8b988db04
|
||||
sha256: "75587f7400f485fdf166beacd471549d98fe5d58e634f708916bb65dec05d6a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "7.4.0"
|
||||
speech_to_text_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_platform_interface
|
||||
sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114
|
||||
sha256: a7e16e02853853ed7534ac2bde9a1c4f39c8879970a7974ac6ff832d4bdaa4b0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.4.0"
|
||||
speech_to_text_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_windows
|
||||
sha256: "2c9846d18253c7bbe059a276297ef9f27e8a2745dead32192525beb208195072"
|
||||
sha256: "2d1d10565b23262386b453b33656299608dc7a66784453735d6c1318f13f44d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+beta.8"
|
||||
version: "1.0.1"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.4.2+1"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
version: "2.5.8"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1449,10 +1465,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
version: "3.4.0+1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1469,14 +1485,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1497,18 +1505,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.20"
|
||||
version: "6.3.29"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.4"
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1521,10 +1529,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1537,10 +1545,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1569,10 +1577,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1646,5 +1654,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0-0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: musadaq_app
|
||||
description: Jordanian E-Invoicing Automation SaaS
|
||||
publish_to: 'none'
|
||||
version: 1.0.3+3
|
||||
version: 1.0.6+6
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
@@ -18,8 +18,8 @@ dependencies:
|
||||
flutter_secure_storage: ^10.1.0
|
||||
|
||||
# ─── Local Database (ObjectBox) ─────────────────────
|
||||
objectbox: ^4.0.1
|
||||
objectbox_flutter_libs: any
|
||||
objectbox: ^5.3.1
|
||||
objectbox_flutter_libs: ^5.3.1
|
||||
path_provider: ^2.1.2
|
||||
|
||||
# ─── Authentication & Security ──────────────────────
|
||||
@@ -28,8 +28,8 @@ dependencies:
|
||||
crypto: ^3.0.3
|
||||
|
||||
# ─── Camera & Scanning ──────────────────────────────
|
||||
camerawesome: ^2.0.0
|
||||
cunning_document_scanner: ^1.2.3
|
||||
camerawesome: ^2.5.0
|
||||
cunning_document_scanner: ^1.4.0
|
||||
image_picker: ^1.0.7
|
||||
file_picker: ^8.1.2
|
||||
|
||||
@@ -46,7 +46,7 @@ dependencies:
|
||||
# ─── Voice & Audio ──────────────────────────────────
|
||||
speech_to_text: ^7.3.0
|
||||
record: ^6.2.0
|
||||
permission_handler: ^11.3.0
|
||||
permission_handler: ^12.0.1
|
||||
|
||||
# ─── Connectivity & Background ──────────────────────
|
||||
connectivity_plus: ^6.0.3
|
||||
@@ -70,13 +70,13 @@ dependencies:
|
||||
shorebird_code_push: ^2.0.0
|
||||
|
||||
# ─── Security (Root/Jailbreak/Tamper Detection) ─────
|
||||
freerasp: ^6.6.0
|
||||
freerasp: ^7.5.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
objectbox_generator: any
|
||||
objectbox_generator: ^5.3.1
|
||||
build_runner: ^2.4.8
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
|
||||
@@ -164,10 +164,22 @@
|
||||
<h2>اختر الباقة المناسبة لحجم أعمالك</h2>
|
||||
<p>لا رسوم خفية. لا عقود طويلة. ابدأ مجاناً وتدرّج حسب احتياجك.</p>
|
||||
</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="price-card">
|
||||
<div class="price-name">مجانية</div>
|
||||
<div class="price-name">التجربة المجانية</div>
|
||||
<div class="price-amount">0 <span>دينار/شهر</span></div>
|
||||
<div class="price-desc">للتجربة الأولية</div>
|
||||
<ul class="price-features">
|
||||
@@ -180,63 +192,38 @@
|
||||
<a href="/register.php" class="btn btn-outline" style="width:100%">ابدأ مجاناً</a>
|
||||
</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="popular-badge">⭐ الأكثر اختياراً</div>
|
||||
<div class="price-name">مكتبية</div>
|
||||
<div class="price-amount">45 <span>دينار/شهر</span></div>
|
||||
<div class="price-desc">للمكاتب المحاسبية</div>
|
||||
<div class="price-name">الباقة الأساسية</div>
|
||||
<div class="price-amount price-annual">120 <span>دينار/سنة</span></div>
|
||||
<div class="price-amount price-monthly" style="display:none;">15 <span>دينار/شهر</span></div>
|
||||
<div class="price-desc">للمحاسبين والشركات الصغيرة</div>
|
||||
<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> 10 مستخدمين</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>
|
||||
<li><span class="feature-check">✔</span> ربط مباشر مع جوفوترة</li>
|
||||
</ul>
|
||||
<a href="/register.php" class="btn btn-primary" style="width:100%">اشترك الآن</a>
|
||||
</div>
|
||||
|
||||
<div class="price-card">
|
||||
<div class="price-name">احترافية</div>
|
||||
<div class="price-amount">99 <span>دينار/شهر</span></div>
|
||||
<div class="price-desc">للمكاتب الكبيرة</div>
|
||||
<div class="price-name">الباقة الاحترافية</div>
|
||||
<div class="price-amount price-annual">290 <span>دينار/سنة</span></div>
|
||||
<div class="price-amount price-monthly" style="display:none;">35 <span>دينار/شهر</span></div>
|
||||
<div class="price-desc">للمكاتب الكبيرة والموزعين</div>
|
||||
<ul class="price-features">
|
||||
<li><span class="feature-check">✔</span> حتى 25 شركة</li>
|
||||
<li><span class="feature-check">✔</span> 2,000 فاتورة شهرياً</li>
|
||||
<li><span class="feature-check">✔</span> 25 مستخدم</li>
|
||||
<li><span class="feature-check">✔</span> شركات غير محدودة</li>
|
||||
<li><span class="feature-check">✔</span> 3,000 فاتورة شهرياً</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> API كامل للتطبيق</li>
|
||||
</ul>
|
||||
<a href="/register.php" class="btn btn-outline" style="width:100%">اشترك الآن</a>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@@ -1868,8 +1868,8 @@
|
||||
<!-- Invoices -->
|
||||
<div class="usage-card">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
|
||||
<span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 الفواتير
|
||||
الشهرية</span>
|
||||
<span style="font-weight:700; color:var(--text-1); font-size:14px;">📄 رصيد الفواتير
|
||||
<span x-text="subscription?.billing_cycle === 'monthly' ? '(شهري)' : '(سنوي)'"></span></span>
|
||||
<span class="num-font" style="font-weight:700; color:var(--green-mid);"
|
||||
x-text="(subscription?.invoices?.used || 0) + ' من ' + (subscription?.invoices?.limit || 0)"></span>
|
||||
</div>
|
||||
@@ -1952,6 +1952,22 @@
|
||||
</p>
|
||||
</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;">
|
||||
<template x-for="p in plans" :key="p.id">
|
||||
<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);">
|
||||
<span class="num-font"
|
||||
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>
|
||||
<span x-text="billingCycle === 'monthly' ? 'شهر' : 'سنة'"></span></span>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
@@ -2712,10 +2728,9 @@
|
||||
class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">رقم الهاتف <span
|
||||
class="form-label-sub">(اختياري)</span></label>
|
||||
<label class="form-label">رقم الهاتف</label>
|
||||
<input type="text" x-model="newTenant.phone" placeholder="+962 7x xxx xxxx"
|
||||
class="form-input">
|
||||
class="form-input" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2731,13 +2746,8 @@
|
||||
<input type="text" x-model="newTenant.manager_name" placeholder="الاسم الكامل"
|
||||
class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<div class="form-group" style="grid-column:1/-1;">
|
||||
<label class="form-label">كلمة مرور الدخول للمدير</label>
|
||||
<input type="password" x-model="newTenant.manager_password" placeholder="••••••••"
|
||||
class="form-input" required>
|
||||
</div>
|
||||
@@ -2928,11 +2938,13 @@
|
||||
isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
|
||||
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
|
||||
acknowledgedWarnings: false, isEditingInvoice: false,
|
||||
isBusy: false, globalError: '',
|
||||
isBusy: false,
|
||||
billingCycle: 'annual', // 'monthly' or 'annual'
|
||||
globalError: '',
|
||||
|
||||
newUser: { name: '', email: '', password: '', role: 'accountant', 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' },
|
||||
uploadData: { company_id: '' },
|
||||
currentCompany: null, currentInvoice: null, companyStats: null,
|
||||
@@ -3103,7 +3115,7 @@
|
||||
const res = await this.apiRequest('v1/tenants/create', 'POST', this.newTenant);
|
||||
if (res) {
|
||||
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();
|
||||
alert('تم إضافة المكتب المحاسبي والمدير المسؤول بنجاح');
|
||||
}
|
||||
@@ -3400,7 +3412,10 @@
|
||||
const res = await fetch('/index.php?route=v1/payments/create', {
|
||||
method: 'POST',
|
||||
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();
|
||||
this.isBusy = false;
|
||||
|
||||
47
update_annual_plans.sql
Normal file
47
update_annual_plans.sql
Normal 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
6
update_plans.php
Normal 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);
|
||||
46
update_subscription_strategy.sql
Normal file
46
update_subscription_strategy.sql
Normal 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';
|
||||
Reference in New Issue
Block a user