Compare commits
22 Commits
48fcdaf4b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f62455113 | ||
|
|
2f1a6f9c85 | ||
|
|
9ad361e992 | ||
|
|
aceb7d324f | ||
|
|
24a9f064a1 | ||
|
|
663896becb | ||
|
|
e93f1d4f34 | ||
|
|
bddee7ca2d | ||
|
|
2d81aa2fb0 | ||
|
|
e798b970f1 | ||
|
|
a98a5abcce | ||
|
|
3f0534ba0d | ||
|
|
e0dc1712ca | ||
|
|
53284b971a | ||
|
|
fa73062023 | ||
|
|
68f6e76da8 | ||
|
|
8b69c99776 | ||
|
|
cf8cf829d8 | ||
|
|
813197c869 | ||
|
|
ee2ea3a111 | ||
|
|
9ecc03adb1 | ||
|
|
3eecb2f602 |
@@ -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,
|
||||
@@ -112,7 +114,7 @@ final class QuotaMiddleware
|
||||
$stmt->execute([$tenantId]);
|
||||
|
||||
// Invalidate cache
|
||||
Cache::forget("quota_sub_{$tenantId}");
|
||||
Cache::delete("quota_sub_{$tenantId}");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -125,8 +125,8 @@ class InvoiceProcessor
|
||||
INSERT INTO invoice_lines (
|
||||
id, invoice_id, line_number, description,
|
||||
quantity, unit_price, tax_rate, tax_amount,
|
||||
discount_amount, net_total, tax_category
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
discount_amount, net_total, line_total, tax_category
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
");
|
||||
foreach ($extracted['lines'] as $idx => $line) {
|
||||
$quantity = (float)($line['quantity'] ?? 1);
|
||||
@@ -148,6 +148,7 @@ class InvoiceProcessor
|
||||
$taxAmount,
|
||||
$discount,
|
||||
$netTotal,
|
||||
$netTotal, // line_total
|
||||
$line['tax_category'] ?? 'standard'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -112,7 +114,7 @@ final class QuotaMiddleware
|
||||
$stmt->execute([$tenantId]);
|
||||
|
||||
// Invalidate cache
|
||||
Cache::forget("quota_sub_{$tenantId}");
|
||||
Cache::delete("quota_sub_{$tenantId}");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -53,10 +53,12 @@ if ($decoded['role'] !== 'super_admin' && $company['tenant_id'] !== $tenantId) {
|
||||
$targetTenantId = $company['tenant_id'];
|
||||
|
||||
// 3. Check quota (preview — don't increment yet)
|
||||
try {
|
||||
QuotaMiddleware::checkInvoiceQuota($targetTenantId);
|
||||
} catch (\Exception $e) {
|
||||
json_error('تم استنفاد رصيد الفواتير لهذا الشهر. قم بترقية باقتك.', 429);
|
||||
if ($decoded['role'] !== 'super_admin') {
|
||||
try {
|
||||
QuotaMiddleware::checkInvoiceQuota($targetTenantId);
|
||||
} catch (\Exception $e) {
|
||||
json_error('تم استنفاد رصيد الفواتير لهذا الشهر. قم بترقية باقتك.', 429);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Generate batch ID
|
||||
|
||||
@@ -50,7 +50,9 @@ try {
|
||||
|
||||
try {
|
||||
// Check quota for each invoice (preventive)
|
||||
QuotaMiddleware::checkInvoiceQuota($tenantId);
|
||||
if ($decoded['role'] !== 'super_admin') {
|
||||
QuotaMiddleware::checkInvoiceQuota($tenantId);
|
||||
}
|
||||
|
||||
$invoiceData = [
|
||||
'id' => Database::generateUuid(),
|
||||
|
||||
@@ -161,28 +161,37 @@ $summarySheet->setTitle('الملخص الإجمالي');
|
||||
$summarySheet->setRightToLeft(true);
|
||||
|
||||
// --- SUMMARY HEADER ---
|
||||
$summarySheet->mergeCells("A1:J1");
|
||||
$summarySheet->setCellValue("A1", 'مُـصَـادَق — ملخص الفواتير الإجمالي');
|
||||
$summarySheet->getStyle("A1")->applyFromArray([
|
||||
// We use A1 for Logo, B1:I1 for Title, J1 for Link/QR to avoid merge issues in some viewers
|
||||
$summarySheet->setCellValue("B1", 'مُـصَـادَق — ملخص الفواتير الإجمالي');
|
||||
$summarySheet->mergeCells("B1:I1");
|
||||
$summarySheet->getStyle("B1:I1")->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 16, 'color' => ['argb' => 'FF' . $headerFont]],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$summarySheet->getRowDimension(1)->setRowHeight(45);
|
||||
|
||||
// Style A1 and J1 background to match the header
|
||||
$summarySheet->getStyle("A1")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg);
|
||||
$summarySheet->getStyle("J1")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg);
|
||||
|
||||
// --- Add Logo ---
|
||||
$logoSummary = new Drawing();
|
||||
$logoSummary->setName('Musadaq Logo');
|
||||
$logoSummary->setPath(ROOT_PATH . '/public/assets/img/logo.jpg');
|
||||
$logoSummary->setHeight(38);
|
||||
$logoSummary->setCoordinates('A1');
|
||||
$logoSummary->setOffsetX(15);
|
||||
$logoSummary->setOffsetY(5);
|
||||
$logoSummary->setWorksheet($summarySheet);
|
||||
try {
|
||||
if (file_exists($logoPath)) {
|
||||
$logoSummary = new Drawing();
|
||||
$logoSummary->setName('Musadaq Logo');
|
||||
$logoSummary->setPath($logoPath);
|
||||
$logoSummary->setHeight(38);
|
||||
$logoSummary->setCoordinates('A1');
|
||||
$logoSummary->setOffsetX(5);
|
||||
$logoSummary->setOffsetY(5);
|
||||
$logoSummary->setWorksheet($summarySheet);
|
||||
}
|
||||
} catch(\Exception $e) { error_log('Logo Summary Error: ' . $e->getMessage()); }
|
||||
|
||||
// --- Add Clickable Website Link ---
|
||||
$summarySheet->setCellValue("J1", 'musadaq.intaleqapp.com');
|
||||
$summarySheet->getCell("J1")->getHyperlink()->setUrl('https://musadaq.intaleqapp.com/');
|
||||
$summarySheet->setCellValue('J1', 'musadaq.intaleqapp.com/verify_qr');
|
||||
$summarySheet->getCell('J1')->getHyperlink()->setUrl('https://musadaq.intaleqapp.com/index.php?route=verify_qr');
|
||||
$summarySheet->getStyle("J1")->applyFromArray([
|
||||
'font' => ['color' => ['argb' => 'FFFFFFFF'], 'underline' => true, 'size' => 9],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
@@ -190,7 +199,7 @@ $summarySheet->getStyle("J1")->applyFromArray([
|
||||
|
||||
// --- Add QR Code to Summary Header ---
|
||||
try {
|
||||
$summaryUrl = "https://musadaq.intaleqapp.com/";
|
||||
$summaryUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr";
|
||||
$qrApiUrl = "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=" . urlencode($summaryUrl);
|
||||
$qrData = $downloadUrl($qrApiUrl);
|
||||
if ($qrData) {
|
||||
@@ -200,8 +209,8 @@ try {
|
||||
$drawingQr->setName('Musadaq QR');
|
||||
$drawingQr->setPath($tmpQr);
|
||||
$drawingQr->setHeight(38);
|
||||
$drawingQr->setCoordinates('I1');
|
||||
$drawingQr->setOffsetX(40);
|
||||
$drawingQr->setCoordinates('J1');
|
||||
$drawingQr->setOffsetX(5);
|
||||
$drawingQr->setOffsetY(5);
|
||||
$drawingQr->setWorksheet($summarySheet);
|
||||
}
|
||||
@@ -319,36 +328,47 @@ foreach ($invoices as $invIdx => $inv) {
|
||||
$invRow = 1;
|
||||
|
||||
// ── INVOICE HEADER ──────────────────────────
|
||||
$sheet->mergeCells("A{$invRow}:I{$invRow}");
|
||||
$sheet->setCellValue("A{$invRow}", 'مُـصَـادَق — تقرير فاتورة مشتريات');
|
||||
$sheet->getStyle("A{$invRow}")->applyFromArray([
|
||||
// We use A for Logo, B:H for Title, I for QR to avoid merge issues
|
||||
$sheet->setCellValue("B{$invRow}", 'مُـصَـادَق — تقرير فاتورة مشتريات');
|
||||
$sheet->mergeCells("B{$invRow}:H{$invRow}");
|
||||
$sheet->getStyle("B{$invRow}:H{$invRow}")->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 16, 'color' => ['argb' => 'FF' . $headerFont]],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$sheet->getRowDimension($invRow)->setRowHeight(45);
|
||||
|
||||
// Background color for side cells
|
||||
$sheet->getStyle("A{$invRow}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg);
|
||||
$sheet->getStyle("I{$invRow}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg);
|
||||
|
||||
// --- Add Logo ---
|
||||
$logoInv = new Drawing();
|
||||
$logoInv->setName('Musadaq Logo');
|
||||
$logoInv->setPath(ROOT_PATH . '/public/assets/img/logo.jpg');
|
||||
$logoInv->setHeight(38);
|
||||
$logoInv->setCoordinates('A' . $invRow);
|
||||
$logoInv->setOffsetX(15);
|
||||
$logoInv->setOffsetY(5);
|
||||
$logoInv->setWorksheet($sheet);
|
||||
try {
|
||||
if (file_exists($logoPath)) {
|
||||
$logoInv = new Drawing();
|
||||
$logoInv->setName('Musadaq Logo');
|
||||
$logoInv->setPath($logoPath);
|
||||
$logoInv->setHeight(38);
|
||||
$logoInv->setCoordinates('A' . $invRow);
|
||||
$logoInv->setOffsetX(5);
|
||||
$logoInv->setOffsetY(5);
|
||||
$logoInv->setWorksheet($sheet);
|
||||
}
|
||||
} catch(\Exception $e) { error_log('Logo Invoice Error: ' . $e->getMessage()); }
|
||||
|
||||
// --- Add Clickable Website Link ---
|
||||
$sheet->setCellValue("I" . $invRow, 'musadaq.intaleqapp.com');
|
||||
$sheet->getCell("I" . $invRow)->getHyperlink()->setUrl('https://musadaq.intaleqapp.com/');
|
||||
// We'll move the link slightly down or put it in I1 with the QR
|
||||
$sheet->setCellValue("I" . $invRow, 'musadaq.intaleqapp.com/verify_qr');
|
||||
$verifyUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr&id=" . $inv['id'];
|
||||
$sheet->getCell("I" . $invRow)->getHyperlink()->setUrl($verifyUrl);
|
||||
$sheet->getStyle("I" . $invRow)->applyFromArray([
|
||||
'font' => ['color' => ['argb' => 'FFFFFFFF'], 'underline' => true, 'size' => 9],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
'font' => ['color' => ['argb' => 'FFFFFFFF'], 'underline' => true, 'size' => 8],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'vertical' => Alignment::VERTICAL_TOP],
|
||||
]);
|
||||
|
||||
// --- Add Verification QR Code ---
|
||||
try {
|
||||
$verifyUrl = "https://musadaq.intaleqapp.com/index.php?route=v1/verify&id=" . $inv['id'];
|
||||
$verifyUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr&id=" . $inv['id'];
|
||||
$qrApiUrl = "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=" . urlencode($verifyUrl);
|
||||
$qrData = $downloadUrl($qrApiUrl);
|
||||
if ($qrData) {
|
||||
@@ -358,8 +378,8 @@ foreach ($invoices as $invIdx => $inv) {
|
||||
$drawingQr->setName('Verification QR');
|
||||
$drawingQr->setPath($tmpQr);
|
||||
$drawingQr->setHeight(38);
|
||||
$drawingQr->setCoordinates('H' . $invRow);
|
||||
$drawingQr->setOffsetX(40);
|
||||
$drawingQr->setCoordinates('I' . $invRow);
|
||||
$drawingQr->setOffsetX(5);
|
||||
$drawingQr->setOffsetY(5);
|
||||
$drawingQr->setWorksheet($sheet);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* Public Invoice Verification Page
|
||||
* GET /v1/verify?id=INVOICE_ID
|
||||
*/
|
||||
// Minimal public verification
|
||||
if (!defined('ROOT_PATH')) define('ROOT_PATH', realpath(dirname(__DIR__, 2)));
|
||||
|
||||
// Load Env manually
|
||||
$envFile = '/home/intaleqapp-musadaq/env/.env';
|
||||
if (!file_exists($envFile)) $envFile = ROOT_PATH . '/.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with(trim($line), '#')) continue;
|
||||
$parts = explode('=', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$n = trim($parts[0]); $v = trim($parts[1], " \t\n\r\0\x0B\"'");
|
||||
$_ENV[$n] = $v; $_SERVER[$n] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
|
||||
header_remove("Content-Security-Policy");
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
|
||||
|
||||
try {
|
||||
$invoiceId = $_GET['id'] ?? null;
|
||||
|
||||
@@ -167,6 +185,7 @@ try {
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
die("خطأ في النظام");
|
||||
}
|
||||
|
||||
@@ -32,8 +32,13 @@ if ($errors) {
|
||||
|
||||
$db = Database::getInstance();
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
$planId = $data['plan_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');
|
||||
$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("
|
||||
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) -->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
PODS:
|
||||
- camerawesome (0.0.1):
|
||||
- Flutter
|
||||
- charset_converter (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- cunning_document_scanner (1.0.0):
|
||||
@@ -176,6 +178,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- camerawesome (from `.symlinks/plugins/camerawesome/ios`)
|
||||
- charset_converter (from `.symlinks/plugins/charset_converter/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- cunning_document_scanner (from `.symlinks/plugins/cunning_document_scanner/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
@@ -224,6 +227,8 @@ SPEC REPOS:
|
||||
EXTERNAL SOURCES:
|
||||
camerawesome:
|
||||
:path: ".symlinks/plugins/camerawesome/ios"
|
||||
charset_converter:
|
||||
:path: ".symlinks/plugins/charset_converter/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
cunning_document_scanner:
|
||||
@@ -271,6 +276,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
camerawesome: a961fa32dafc00d2f093d824311c84f849586b58
|
||||
charset_converter: 82bc1d2e3c70dcb51bf769e9772e3ae5b2571695
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cunning_document_scanner: 43a2bda11ef6a33fd68766b287ed056b4caf6a06
|
||||
CwlCatchException: 7acc161b299a6de7f0a46a6ed741eae2c8b4d75a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:esc_pos_printer/esc_pos_printer.dart';
|
||||
import 'package:esc_pos_utils/esc_pos_utils.dart';
|
||||
import 'package:esc_pos_printer_plus/esc_pos_printer_plus.dart';
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
class ThermalPrinterService {
|
||||
@@ -29,7 +29,7 @@ class ThermalPrinterService {
|
||||
printer.feed(2);
|
||||
printer.cut();
|
||||
printer.disconnect();
|
||||
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
AppLogger.error('Thermal printing failed', e);
|
||||
@@ -37,7 +37,8 @@ class ThermalPrinterService {
|
||||
}
|
||||
}
|
||||
|
||||
void _buildInvoiceTicket(NetworkPrinter printer, Map<String, dynamic> invoice) {
|
||||
void _buildInvoiceTicket(
|
||||
NetworkPrinter printer, Map<String, dynamic> invoice) {
|
||||
// Header
|
||||
printer.text(
|
||||
'M U S A D A Q',
|
||||
@@ -66,8 +67,14 @@ class ThermalPrinterService {
|
||||
// Table Header
|
||||
printer.row([
|
||||
PosColumn(text: 'البند', width: 6),
|
||||
PosColumn(text: 'الكمية', width: 2, styles: const PosStyles(align: PosAlign.center)),
|
||||
PosColumn(text: 'الإجمالي', width: 4, styles: const PosStyles(align: PosAlign.right)),
|
||||
PosColumn(
|
||||
text: 'الكمية',
|
||||
width: 2,
|
||||
styles: const PosStyles(align: PosAlign.center)),
|
||||
PosColumn(
|
||||
text: 'الإجمالي',
|
||||
width: 4,
|
||||
styles: const PosStyles(align: PosAlign.right)),
|
||||
]);
|
||||
printer.hr();
|
||||
|
||||
@@ -76,8 +83,14 @@ class ThermalPrinterService {
|
||||
for (var item in items) {
|
||||
printer.row([
|
||||
PosColumn(text: item['description'] ?? '-', width: 6),
|
||||
PosColumn(text: item['quantity']?.toString() ?? '1', width: 2, styles: const PosStyles(align: PosAlign.center)),
|
||||
PosColumn(text: item['line_total']?.toString() ?? '0', width: 4, styles: const PosStyles(align: PosAlign.right)),
|
||||
PosColumn(
|
||||
text: item['quantity']?.toString() ?? '1',
|
||||
width: 2,
|
||||
styles: const PosStyles(align: PosAlign.center)),
|
||||
PosColumn(
|
||||
text: item['line_total']?.toString() ?? '0',
|
||||
width: 4,
|
||||
styles: const PosStyles(align: PosAlign.right)),
|
||||
]);
|
||||
}
|
||||
printer.hr();
|
||||
@@ -85,21 +98,33 @@ class ThermalPrinterService {
|
||||
// Totals
|
||||
printer.row([
|
||||
PosColumn(text: 'المجموع الجزئي:', width: 8),
|
||||
PosColumn(text: '${invoice['subtotal'] ?? 0}', width: 4, styles: const PosStyles(align: PosAlign.right)),
|
||||
PosColumn(
|
||||
text: '${invoice['subtotal'] ?? 0}',
|
||||
width: 4,
|
||||
styles: const PosStyles(align: PosAlign.right)),
|
||||
]);
|
||||
printer.row([
|
||||
PosColumn(text: 'الضريبة:', width: 8),
|
||||
PosColumn(text: '${invoice['tax_amount'] ?? 0}', width: 4, styles: const PosStyles(align: PosAlign.right)),
|
||||
PosColumn(
|
||||
text: '${invoice['tax_amount'] ?? 0}',
|
||||
width: 4,
|
||||
styles: const PosStyles(align: PosAlign.right)),
|
||||
]);
|
||||
printer.row([
|
||||
PosColumn(text: 'الإجمالي الكلي:', width: 8, styles: const PosStyles(bold: true)),
|
||||
PosColumn(text: '${invoice['grand_total'] ?? 0} JOD', width: 4, styles: const PosStyles(align: PosAlign.right, bold: true)),
|
||||
PosColumn(
|
||||
text: 'الإجمالي الكلي:',
|
||||
width: 8,
|
||||
styles: const PosStyles(bold: true)),
|
||||
PosColumn(
|
||||
text: '${invoice['grand_total'] ?? 0} JOD',
|
||||
width: 4,
|
||||
styles: const PosStyles(align: PosAlign.right, bold: true)),
|
||||
]);
|
||||
|
||||
printer.hr();
|
||||
printer.text(
|
||||
'شكراً لاستخدامكم مُصادَق',
|
||||
styles: const PosStyles(align: PosAlign.center, italic: true),
|
||||
styles: const PosStyles(align: PosAlign.center),
|
||||
);
|
||||
printer.text(
|
||||
'www.musadaq.com',
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../../../core/services/push_notification_service.dart';
|
||||
class AuthController extends GetxController {
|
||||
final Dio _dio = DioClient().client;
|
||||
final SecureStorage _storage = SecureStorage();
|
||||
|
||||
|
||||
var isLoading = false.obs;
|
||||
var phone = ''.obs;
|
||||
|
||||
@@ -23,20 +23,20 @@ class AuthController extends GetxController {
|
||||
return;
|
||||
}
|
||||
isLoading.value = true;
|
||||
|
||||
|
||||
// Normalize phone number
|
||||
String normalizedPhone = phoneNumber.replaceAll(RegExp(r'[^0-9+]'), '');
|
||||
if (normalizedPhone.startsWith('+')) {
|
||||
normalizedPhone = normalizedPhone.substring(1);
|
||||
}
|
||||
if (normalizedPhone.startsWith('07')) {
|
||||
normalizedPhone = '962' + normalizedPhone.substring(1);
|
||||
normalizedPhone = '962${normalizedPhone.substring(1)}';
|
||||
} else if (normalizedPhone.startsWith('7')) {
|
||||
normalizedPhone = '962' + normalizedPhone;
|
||||
normalizedPhone = '962$normalizedPhone';
|
||||
}
|
||||
|
||||
|
||||
phone.value = normalizedPhone;
|
||||
|
||||
|
||||
final response = await _dio.post('auth/mobile/request-otp', data: {
|
||||
'phone': normalizedPhone,
|
||||
});
|
||||
@@ -60,12 +60,12 @@ class AuthController extends GetxController {
|
||||
Future<void> verifyOtp(String otp) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
|
||||
// Get device info
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
String deviceId = '';
|
||||
String deviceName = '';
|
||||
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
deviceId = androidInfo.id;
|
||||
@@ -92,16 +92,16 @@ class AuthController extends GetxController {
|
||||
if (response.statusCode == 200) {
|
||||
AppLogger.print('OTP Verify Success. Tokens received.');
|
||||
final data = response.data['data'];
|
||||
|
||||
|
||||
// Save secure data
|
||||
await _storage.saveToken(data['access_token']);
|
||||
await _storage.saveDeviceSecret(data['device_secret']);
|
||||
if (data['user']['email'] != null) {
|
||||
await _storage.saveEmail(data['user']['email']);
|
||||
}
|
||||
|
||||
|
||||
AppSnackbar.showSuccess('مرحباً بك', 'تم تسجيل الدخول بنجاح');
|
||||
|
||||
|
||||
// Navigate to Biometric Setup (unless it's the reviewer)
|
||||
if (data['user']['email'] == 'reviewer@musadaq.jo') {
|
||||
Get.offAllNamed(AppRoutes.MAIN);
|
||||
@@ -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;
|
||||
@@ -129,7 +131,7 @@ class AuthController extends GetxController {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
String deviceId = '';
|
||||
String deviceName = '';
|
||||
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
deviceId = androidInfo.id;
|
||||
@@ -150,22 +152,31 @@ 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
|
||||
if (data['device_secret'] != null) {
|
||||
await _storage.saveDeviceSecret(data['device_secret']);
|
||||
}
|
||||
|
||||
|
||||
if (data['user']['email'] != null) {
|
||||
await _storage.saveEmail(data['user']['email']);
|
||||
}
|
||||
|
||||
|
||||
AppSnackbar.showSuccess('مرحباً بك', 'تم تسجيل الدخول بنجاح');
|
||||
|
||||
|
||||
// Navigate to Dashboard for reviewer, else Biometric Setup
|
||||
if (email == 'reviewer@musadaq.jo') {
|
||||
Get.offAllNamed(AppRoutes.MAIN);
|
||||
|
||||
@@ -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,47 +37,38 @@ 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,
|
||||
textDirection: TextDirection.ltr,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'كلمة المرور',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
textDirection: TextDirection.ltr,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'كلمة المرور',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
passwordController.text
|
||||
);
|
||||
} else {
|
||||
controller.requestOtp(phoneController.text);
|
||||
}
|
||||
controller.loginWithEmail(
|
||||
emailController.text,
|
||||
passwordController.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,14 +254,16 @@ class DashboardView extends GetView<DashboardController> {
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.AUDIT_LOG),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildAdminActionCard(
|
||||
'ادعُ واكسب',
|
||||
Icons.card_giftcard,
|
||||
const Color(0xFFD4AF37),
|
||||
isDark,
|
||||
() => Get.toNamed(AppRoutes.REFERRAL),
|
||||
),
|
||||
if (!GetPlatform.isIOS) ...[
|
||||
const SizedBox(width: 12),
|
||||
_buildAdminActionCard(
|
||||
'ادعُ واكسب',
|
||||
Icons.card_giftcard,
|
||||
const Color(0xFFD4AF37),
|
||||
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,46 +34,122 @@ 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),
|
||||
|
||||
// Active Payment Request Banner
|
||||
if (controller.activePaymentRequest.value != null)
|
||||
_buildActivePaymentBanner(controller.activePaymentRequest.value!, isDark),
|
||||
if (GetPlatform.isIOS) ...[
|
||||
_buildIOSB2BInfoCard(isDark),
|
||||
],
|
||||
|
||||
// Plans Header
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.diamond_rounded, color: Color(0xFFD4AF37), size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'اختر باقتك',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : const Color(0xFF0F172A),
|
||||
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),
|
||||
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),
|
||||
@@ -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,9 +382,19 @@ 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),
|
||||
@@ -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:
|
||||
@@ -185,6 +169,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
charset_converter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charset_converter
|
||||
sha256: a601f27b78ca86c3d88899d53059786d9c3f3c485b64974e9105c06c2569aef5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -209,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:
|
||||
@@ -273,22 +265,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cunning_document_scanner:
|
||||
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:
|
||||
@@ -329,6 +329,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
esc_pos_printer_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: esc_pos_printer_plus
|
||||
sha256: "76e0d8347fb10b25ad78311fc416a968528a00d18e7bad311fa6c34f4d662d71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
esc_pos_utils_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: esc_pos_utils_plus
|
||||
sha256: "2a22d281cb6f04600ba3ebd607ad8df03a4b2446d814007d22525bab4d50c2ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -373,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:
|
||||
@@ -397,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:
|
||||
@@ -453,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
|
||||
@@ -538,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:
|
||||
@@ -604,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:
|
||||
@@ -640,6 +648,22 @@ 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:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -668,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:
|
||||
@@ -700,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:
|
||||
@@ -716,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:
|
||||
@@ -752,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:
|
||||
@@ -804,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:
|
||||
@@ -844,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:
|
||||
@@ -884,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:
|
||||
@@ -900,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:
|
||||
@@ -980,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:
|
||||
@@ -1020,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:
|
||||
@@ -1032,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:
|
||||
@@ -1216,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:
|
||||
@@ -1297,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:
|
||||
@@ -1409,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:
|
||||
@@ -1429,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:
|
||||
@@ -1457,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:
|
||||
@@ -1481,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:
|
||||
@@ -1497,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:
|
||||
@@ -1529,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:
|
||||
@@ -1606,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
|
||||
|
||||
@@ -40,13 +40,13 @@ dependencies:
|
||||
# ─── PDF Generation ─────────────────────────────────
|
||||
pdf: ^3.10.8
|
||||
printing: ^5.12.0
|
||||
esc_pos_utils: ^1.1.0
|
||||
esc_pos_printer: ^4.1.0
|
||||
esc_pos_utils_plus: any
|
||||
esc_pos_printer_plus: ^0.1.1
|
||||
|
||||
# ─── 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
|
||||
|
||||
|
||||
10
public/.htaccess
Normal file
10
public/.htaccess
Normal file
@@ -0,0 +1,10 @@
|
||||
RewriteEngine On
|
||||
|
||||
# Ensure HTTPS
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Handle Clean URLs
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php?route=$1 [QSA,L]
|
||||
@@ -3,19 +3,24 @@
|
||||
* Simple Router & Entry Point
|
||||
*/
|
||||
|
||||
// 1. Load Bootstrap
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
// Global Request Logging (non-sensitive)
|
||||
error_log("Incoming Request: " . ($_SERVER['REQUEST_METHOD'] ?? 'GET') . " " . ($_SERVER['REQUEST_URI'] ?? '/'));
|
||||
|
||||
// Public Verification Bypass (Top Priority)
|
||||
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$route = $_GET['route'] ?? str_replace('/api/', '', $uri);
|
||||
$route = trim($route, '/');
|
||||
|
||||
error_log("Router: Resolved route '{$route}'");
|
||||
if ($route === 'verify_qr' || $route === 'verify' || $route === 'v.php' || $route === 'v1/verify') {
|
||||
$id = $_GET['id'] ?? null;
|
||||
require_once APP_PATH . '/modules_app/invoices/verify_public.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Route map: route => [allowed_method, module_file]
|
||||
$routes = [
|
||||
'v.php' => ['GET', '../public/v.php'],
|
||||
'verify' => ['GET', 'invoices/verify_public.php'],
|
||||
'v1/auth/login' => ['POST', 'auth/login.php'],
|
||||
'v1/auth/refresh' => ['POST', 'auth/refresh.php'],
|
||||
'v1/auth/logout' => ['POST', 'auth/logout.php'],
|
||||
|
||||
@@ -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,
|
||||
@@ -3039,7 +3051,7 @@
|
||||
getQrSrc(inv) {
|
||||
if (!inv) return '';
|
||||
if (inv.jofotara?.qr_image_uri) return inv.jofotara.qr_image_uri;
|
||||
const verifyUrl = `https://musadaq.intaleqapp.com/index.php?route=v1/verify&id=${inv.id}`;
|
||||
const verifyUrl = `https://musadaq.intaleqapp.com/index.php?route=verify_qr&id=${inv.id}`;
|
||||
const qr = new QRious({ value: verifyUrl, size: 300, level: 'H' });
|
||||
return qr.toDataURL();
|
||||
},
|
||||
@@ -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;
|
||||
|
||||
199
public/v.php
Normal file
199
public/v.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
// Barebones verification script
|
||||
define('ROOT_PATH', realpath(dirname(__DIR__)));
|
||||
define('APP_PATH', ROOT_PATH . '/app');
|
||||
|
||||
// Minimal autoload for Core classes
|
||||
spl_autoload_register(function ($class) {
|
||||
$file = ROOT_PATH . '/' . str_replace('\\', '/', $class) . '.php';
|
||||
if (file_exists($file)) require $file;
|
||||
});
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
|
||||
// Load Environment variables manually if needed
|
||||
$envFile = '/home/intaleqapp-musadaq/env/.env';
|
||||
if (!file_exists($envFile)) $envFile = ROOT_PATH . '/.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with(trim($line), '#')) continue;
|
||||
$parts = explode('=', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
$n = trim($parts[0]); $v = trim($parts[1], " \t\n\r\0\x0B\"'");
|
||||
$_ENV[$n] = $v; $_SERVER[$n] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// die('V.PHP REACHED'); // Debug point
|
||||
header('X-Debug-V: Reached');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
|
||||
|
||||
try {
|
||||
$invoiceId = $_GET['id'] ?? null;
|
||||
|
||||
if (!$invoiceId) {
|
||||
die("<h1>رابط التحقق غير صالح</h1>");
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
// Fetch invoice with company and supplier details
|
||||
$stmt = $db->prepare("
|
||||
SELECT i.*, c.name as company_name_raw
|
||||
FROM invoices i
|
||||
JOIN companies c ON i.company_id = c.id
|
||||
WHERE i.id = ? AND i.deleted_at IS NULL
|
||||
");
|
||||
$stmt->execute([$invoiceId]);
|
||||
$invoice = $stmt->fetch();
|
||||
|
||||
if (!$invoice) {
|
||||
die("<h1>الفاتورة غير موجودة أو تم حذفها</h1>");
|
||||
}
|
||||
|
||||
// Decrypt helper
|
||||
$dec = function($val) {
|
||||
if (empty($val)) return '-';
|
||||
$result = Encryption::decrypt((string)$val);
|
||||
return ($result !== false && $result !== null) ? $result : (string)$val;
|
||||
};
|
||||
|
||||
$supplierName = $dec($invoice['supplier_name']);
|
||||
$companyName = $dec($invoice['company_name_raw']);
|
||||
$total = number_format((float)$invoice['grand_total'], 3);
|
||||
$date = $invoice['invoice_date'] ?: 'غير محدد';
|
||||
$status = match($invoice['status']) {
|
||||
'extracted' => 'مستخرجة',
|
||||
'approved' => 'معتمدة ✅',
|
||||
'submitted' => 'مقدمة للضريبة 🏛️',
|
||||
'rejected' => 'مرفوضة ❌',
|
||||
default => 'قيد المعالجة'
|
||||
};
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>التحقق من الفاتورة - مُصادَق</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1C1550;
|
||||
--accent: #00D1B2;
|
||||
--bg: #F8F9FA;
|
||||
}
|
||||
body {
|
||||
font-family: 'Tajawal', sans-serif;
|
||||
background-color: var(--bg);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
.verify-card {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
max-width: 450px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
border-radius: 50px;
|
||||
background: #E9ECEF;
|
||||
font-weight: bold;
|
||||
margin-bottom: 25px;
|
||||
color: var(--primary);
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #EEE;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.info-item label {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.info-item span {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
.footer-note {
|
||||
margin-top: 30px;
|
||||
font-size: 12px;
|
||||
color: #AAA;
|
||||
}
|
||||
.btn-home {
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="verify-card">
|
||||
<div class="logo">مُـصَـادَق</div>
|
||||
<div class="status-badge"><?php echo $status; ?></div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>اسم المكتب (الشركة)</label>
|
||||
<span><?php echo htmlspecialchars($companyName); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>اسم المورّد</label>
|
||||
<span><?php echo htmlspecialchars($supplierName); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>رقم الفاتورة</label>
|
||||
<span><?php echo htmlspecialchars($invoice['invoice_number'] ?: '-'); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>تاريخ الفاتورة</label>
|
||||
<span><?php echo htmlspecialchars($date); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>المبلغ الإجمالي</label>
|
||||
<span style="font-size: 24px; color: var(--accent);"><?php echo $total; ?> JOD</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
تم التحقق من هذه الفاتورة رسمياً عبر منصة مُصادَق.<br>
|
||||
<?php echo date('Y-m-d H:i:s'); ?>
|
||||
</div>
|
||||
<a href="https://musadaq.intaleqapp.com/" class="btn-home">زيارة منصة مُصادَق</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
} catch (\Exception $e) {
|
||||
die("خطأ في النظام: " . $e->getMessage());
|
||||
}
|
||||
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