diff --git a/app/Controllers/Admin/DashboardController.php b/app/Controllers/Admin/DashboardController.php index 8dc37a4..f812e77 100644 --- a/app/Controllers/Admin/DashboardController.php +++ b/app/Controllers/Admin/DashboardController.php @@ -5,18 +5,71 @@ namespace App\Controllers\Admin; use App\Controllers\Controller; use App\Core\Request; use App\Core\Response; +use App\Services\Database\Connection; +use PDO; class DashboardController extends Controller { - /** - * Display admin dashboard. - */ + private PDO $pdo; + + public function __construct(Connection $connection) + { + parent::__construct(); + $this->pdo = $connection->getPdo(); + } + public function index(Request $request, Response $response): string { $user = $request->routeParam('_authenticated_user'); + $lang = $this->session->get('lang', 'en'); + + // Real stats from database + $orgCount = (int)$this->pdo->query("SELECT COUNT(*) FROM organizations WHERE deleted_at IS NULL")->fetchColumn(); + $vcCount = (int)$this->pdo->query("SELECT COUNT(*) FROM organizations WHERE type='vc' AND deleted_at IS NULL")->fetchColumn(); + $acceleratorCount = (int)$this->pdo->query("SELECT COUNT(*) FROM organizations WHERE type='accelerator' AND deleted_at IS NULL")->fetchColumn(); + $opportunityCount = (int)$this->pdo->query("SELECT COUNT(*) FROM opportunities WHERE deleted_at IS NULL AND status='active'")->fetchColumn(); + $contactCount = (int)$this->pdo->query("SELECT COUNT(*) FROM contacts WHERE deleted_at IS NULL")->fetchColumn(); + $sourceCount = (int)$this->pdo->query("SELECT COUNT(*) FROM sources WHERE status='active'")->fetchColumn(); + $todayOpps = (int)$this->pdo->query("SELECT COUNT(*) FROM opportunities WHERE DATE(created_at) = CURDATE()")->fetchColumn(); + + // Recent opportunities + $stmt = $this->pdo->query( + "SELECT o.*, org.name as org_name FROM opportunities o + LEFT JOIN organizations org ON org.id = o.organization_id + WHERE o.deleted_at IS NULL + ORDER BY o.created_at DESC LIMIT 10" + ); + $recentOpps = $stmt->fetchAll() ?: []; + + // Opportunities by type + $byType = $this->pdo->query( + "SELECT type, COUNT(*) as count FROM opportunities WHERE deleted_at IS NULL GROUP BY type" + )->fetchAll(PDO::FETCH_KEY_PAIR) ?: []; + + // Recent activity + $stmt = $this->pdo->query("SELECT * FROM activity_logs ORDER BY created_at DESC LIMIT 10"); + $recentActivities = $stmt->fetchAll() ?: []; + + $langFile = __DIR__ . "/../../resources/lang/{$lang}.php"; + $t = file_exists($langFile) ? require $langFile : []; + return $this->render('admin/dashboard', [ 'user' => $user, - 'title' => 'Dashboard', + 'title' => $t['dashboard'] ?? 'Dashboard', + 't' => $t, + 'lang' => $lang, + 'stats' => [ + 'organizations' => $orgCount, + 'vc' => $vcCount, + 'accelerators' => $acceleratorCount, + 'opportunities' => $opportunityCount, + 'contacts' => $contactCount, + 'sources' => $sourceCount, + 'today' => $todayOpps, + ], + 'recent_opportunities' => $recentOpps, + 'opportunities_by_type' => $byType, + 'recent_activities' => $recentActivities, ], 'admin'); } -} +} \ No newline at end of file diff --git a/app/Controllers/Admin/SettingsController.php b/app/Controllers/Admin/SettingsController.php new file mode 100644 index 0000000..f51523f --- /dev/null +++ b/app/Controllers/Admin/SettingsController.php @@ -0,0 +1,97 @@ +pdo = $connection->getPdo(); + $this->notifier = $notifier; + } + + public function index(Request $request, Response $response): string + { + $lang = $this->session->get('lang', 'en'); + $langFile = __DIR__ . "/../../resources/lang/{$lang}.php"; + $t = file_exists($langFile) ? require $langFile : []; + + // Get telegram settings from database + $tgToken = $this->getSetting('telegram_bot_token', ''); + $tgChatId = $this->getSetting('telegram_chat_id', ''); + $tgEnabled = $this->getSetting('telegram_enabled', '0'); + + return $this->render('admin/settings/index', [ + 't' => $t, + 'lang' => $lang, + 'tg_token' => $tgToken, + 'tg_chat_id' => $tgChatId, + 'tg_enabled' => $tgEnabled, + ], 'admin'); + } + + public function save(Request $request, Response $response): void + { + $tgToken = $request->post('telegram_bot_token', ''); + $tgChatId = $request->post('telegram_chat_id', ''); + $tgEnabled = $request->post('telegram_enabled', '0'); + + $this->saveSetting('telegram_bot_token', $tgToken); + $this->saveSetting('telegram_chat_id', $tgChatId); + $this->saveSetting('telegram_enabled', $tgEnabled); + + $this->session->setFlash('success', 'Settings saved successfully.'); + $response->redirect('/admin/settings'); + } + + public function testTelegram(Request $request, Response $response): void + { + $tgToken = $request->post('telegram_bot_token', ''); + $tgChatId = $request->post('telegram_chat_id', ''); + + $this->notifier->configure($tgToken, $tgChatId); + + if ($this->notifier->sendTest()) { + $this->session->setFlash('success', 'Test notification sent to Telegram!'); + } else { + $this->session->setFlash('error', 'Failed to send Telegram notification. Check your token and chat ID.'); + } + $response->redirect('/admin/settings'); + } + + public function switchLang(Request $request, Response $response): void + { + $lang = $request->get('lang', 'en'); + if (in_array($lang, ['ar', 'en'])) { + $this->session->set('lang', $lang); + } + $ref = $request->getHeader('Referer') ?? '/admin/dashboard'; + $response->redirect($ref); + } + + private function getSetting(string $key, string $default = ''): string + { + $stmt = $this->pdo->prepare("SELECT `value` FROM settings WHERE `key` = ?"); + $stmt->execute([$key]); + $val = $stmt->fetchColumn(); + return $val !== false ? $val : $default; + } + + private function saveSetting(string $key, string $value): void + { + $stmt = $this->pdo->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?"); + $stmt->execute([$key, $value, $value]); + } +} \ No newline at end of file diff --git a/app/Controllers/Admin/SourcesController.php b/app/Controllers/Admin/SourcesController.php index 373fa8b..8296c5c 100644 --- a/app/Controllers/Admin/SourcesController.php +++ b/app/Controllers/Admin/SourcesController.php @@ -75,8 +75,9 @@ class SourcesController extends Controller $response->redirect('/admin/sources'); } - public function run(Request $request, Response $response, int $id): void + public function run(Request $request, Response $response): void { + $id = (int)$request->routeParam('id'); $stmt = $this->pdo->prepare("SELECT * FROM sources WHERE id = ?"); $stmt->execute([$id]); $source = $stmt->fetch(); diff --git a/app/Services/Notification/TelegramNotifier.php b/app/Services/Notification/TelegramNotifier.php new file mode 100644 index 0000000..369bc12 --- /dev/null +++ b/app/Services/Notification/TelegramNotifier.php @@ -0,0 +1,116 @@ +botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? null; + $this->chatId = $_ENV['TELEGRAM_CHAT_ID'] ?? null; + } + + /** + * Configure from database settings. + */ + public function configure(string $botToken, string $chatId): void + { + $this->botToken = $botToken; + $this->chatId = $chatId; + } + + /** + * Send a notification to Telegram. + */ + public function send(string $message, string $type = 'info'): bool + { + if (!$this->botToken || !$this->chatId) { + return false; + } + + $icons = [ + 'info' => "\xF0\x9F\x93\xA1", // 📡 + 'success' => "\xE2\x9C\x85", // ✅ + 'warning' => "\xE2\x9A\xA0\xEF\xB8\x8F", // ⚠️ + 'error' => "\xE2\x9D\x8C", // ❌ + 'opportunity' => "\xF0\x9F\x92\xA1", // 💡 + 'funding' => "\xF0\x9F\x92\xB0", // 💰 + ]; + + $icon = $icons[$type] ?? $icons['info']; + $fullMessage = "{$icon} *ScoutIQ*\n\n{$message}"; + + try { + $url = "https://api.telegram.org/bot{$this->botToken}/sendMessage"; + $payload = json_encode([ + 'chat_id' => $this->chatId, + 'text' => $fullMessage, + 'parse_mode' => 'Markdown', + 'disable_web_page_preview' => true, + ]); + + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $payload, + 'timeout' => 10, + ], + 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], + ]); + + $response = @file_get_contents($url, false, $context); + return $response !== false; + } catch (Throwable $e) { + return false; + } + } + + /** + * Notify about a new opportunity. + */ + public function notifyNewOpportunity(array $opportunity): void + { + $title = $opportunity['title'] ?? 'Untitled'; + $type = $opportunity['type'] ?? 'other'; + $score = $opportunity['score'] ?? 0; + $url = $opportunity['url'] ?? ''; + $desc = mb_substr($opportunity['description'] ?? '', 0, 200); + $orgName = $opportunity['org_name'] ?? ''; + + $message = "*New Opportunity:* {$title}\n"; + $message .= "*Type:* {$type} | *Score:* {$score}/100\n"; + if ($orgName) $message .= "*Organization:* {$orgName}\n"; + if ($desc) $message .= "_{$desc}_\n"; + if ($url) $message .= "\n[Open Link]({$url})"; + + $this->send($message, 'opportunity'); + } + + /** + * Notify about collector results. + */ + public function notifyCollectorResults(array $results): void + { + $message = "*Data Collection Complete*\n\n"; + $message .= "Sources: {$results['processed']}/{$results['total_sources']}\n"; + $message .= "New Opportunities: {$results['new_opportunities']}\n"; + $message .= "New Organizations: {$results['new_organizations']}\n"; + $message .= "Errors: {$results['errors']}"; + + $this->send($message, 'success'); + } + + /** + * Send test message. + */ + public function sendTest(): bool + { + return $this->send("Test notification from ScoutIQ. Your Telegram integration is working correctly!", 'success'); + } +} \ No newline at end of file diff --git a/public/index.php b/public/index.php index 00f2e57..975bc8b 100644 --- a/public/index.php +++ b/public/index.php @@ -10,6 +10,7 @@ use App\Controllers\Admin\OrganizationsController; use App\Controllers\Admin\OpportunitiesController; use App\Controllers\Admin\ContactsController; use App\Controllers\Admin\SourcesController; +use App\Controllers\Admin\SettingsController; use App\Middleware\SecurityHeaders; use App\Middleware\RateLimit; use App\Middleware\CsrfProtection; @@ -72,6 +73,13 @@ $app->router->group([ $r->post('/sources/{id}/update', [SourcesController::class, 'store']); $r->get('/sources/{id}/delete', [SourcesController::class, 'delete']); $r->get('/sources/{id}/run', [SourcesController::class, 'run']); + + // Settings + $r->get('/settings', [SettingsController::class, 'index']); + $r->post('/settings/save', [SettingsController::class, 'save']); + + // Language switch (no CSRF needed for GET) + $r->get('/lang/{lang}', [SettingsController::class, 'switchLang']); }); // Logout endpoint diff --git a/resources/lang/ar.php b/resources/lang/ar.php new file mode 100644 index 0000000..8a7c944 --- /dev/null +++ b/resources/lang/ar.php @@ -0,0 +1,74 @@ + 'لوحة التحكم', + 'organizations' => 'المنظمات', + 'contacts' => 'جهات الاتصال', + 'opportunities' => 'الفرص', + 'sources' => 'مصادر البيانات', + 'settings' => 'الإعدادات', + 'logout' => 'تسجيل خروج', + 'welcome' => 'مرحباً', + 'add' => 'إضافة', + 'edit' => 'تعديل', + 'delete' => 'حذف', + 'save' => 'حفظ', + 'cancel' => 'إلغاء', + 'search' => 'بحث', + 'filter' => 'تصفية', + 'name' => 'الاسم', + 'type' => 'النوع', + 'status' => 'الحالة', + 'country' => 'الدولة', + 'city' => 'المدينة', + 'url' => 'الرابط', + 'description' => 'الوصف', + 'score' => 'النتيجة', + 'created' => 'تاريخ الإنشاء', + 'updated' => 'آخر تحديث', + 'actions' => 'إجراءات', + 'no_data' => 'لا توجد بيانات', + 'total_organizations' => 'إجمالي المنظمات', + 'total_opportunities' => 'إجمالي الفرص', + 'total_contacts' => 'إجمالي جهات الاتصال', + 'active_sources' => 'المصادر النشطة', + 'today_opportunities' => 'فرص اليوم', + 'vc_funds' => 'صناديق استثمار', + 'accelerators' => 'مسرعات', + 'recent_opportunities' => 'آخر الفرص', + 'recent_activities' => 'آخر النشاطات', + 'view_all' => 'عرض الكل', + 'run_collector' => 'تشغيل الجمع', + 'last_collected' => 'آخر جمع', + 'platform_mode' => 'نمط المنصة', + 'local' => 'محلي', + 'production' => 'إنتاج', + 'email' => 'البريد الإلكتروني', + 'phone' => 'رقم الهاتف', + 'position' => 'المسمى الوظيفي', + 'notes' => 'ملاحظات', + 'interactions' => 'التفاعلات', + 'add_interaction' => 'إضافة تفاعل', + 'log_interaction' => 'تسجيل تفاعل', + 'call' => 'مكالمة', + 'meeting' => 'اجتماع', + 'applications' => 'الطلبات', + 'tags' => 'الوسوم', + 'deadline' => 'الموعد النهائي', + 'amount' => 'المبلغ', + 'funding_stage' => 'مرحلة التمويل', + 'crm_status' => 'حالة CRM', + 'domain' => 'النطاق', + 'website' => 'الموقع الإلكتروني', + 'run' => 'تشغيل', + 'save_source' => 'حفظ المصدر', + 'add_source' => 'إضافة مصدر', + 'telegram_notifications' => 'إشعارات تيليجرام', + 'telegram_bot_token' => 'رمز البوت', + 'telegram_chat_id' => 'معرف المحادثة', + 'telegram_enabled' => 'تفعيل الإشعارات', + 'test_notification' => 'إرسال تجربة', + 'language' => 'اللغة', + 'arabic' => 'العربية', + 'english' => 'English', +]; \ No newline at end of file diff --git a/resources/lang/en.php b/resources/lang/en.php new file mode 100644 index 0000000..e35e5a1 --- /dev/null +++ b/resources/lang/en.php @@ -0,0 +1,74 @@ + 'Dashboard', + 'organizations' => 'Organizations', + 'contacts' => 'Contacts', + 'opportunities' => 'Opportunities', + 'sources' => 'Data Sources', + 'settings' => 'Settings', + 'logout' => 'Logout', + 'welcome' => 'Welcome', + 'add' => 'Add', + 'edit' => 'Edit', + 'delete' => 'Delete', + 'save' => 'Save', + 'cancel' => 'Cancel', + 'search' => 'Search', + 'filter' => 'Filter', + 'name' => 'Name', + 'type' => 'Type', + 'status' => 'Status', + 'country' => 'Country', + 'city' => 'City', + 'url' => 'URL', + 'description' => 'Description', + 'score' => 'Score', + 'created' => 'Created', + 'updated' => 'Updated', + 'actions' => 'Actions', + 'no_data' => 'No data', + 'total_organizations' => 'Total Organizations', + 'total_opportunities' => 'Total Opportunities', + 'total_contacts' => 'Total Contacts', + 'active_sources' => 'Active Sources', + 'today_opportunities' => "Today's Opportunities", + 'vc_funds' => 'VC Funds', + 'accelerators' => 'Accelerators', + 'recent_opportunities' => 'Recent Opportunities', + 'recent_activities' => 'Recent Activities', + 'view_all' => 'View All', + 'run_collector' => 'Run Collector', + 'last_collected' => 'Last Collected', + 'platform_mode' => 'Platform Mode', + 'local' => 'Local', + 'production' => 'Production', + 'email' => 'Email', + 'phone' => 'Phone', + 'position' => 'Position', + 'notes' => 'Notes', + 'interactions' => 'Interactions', + 'add_interaction' => 'Add Interaction', + 'log_interaction' => 'Log Interaction', + 'call' => 'Call', + 'meeting' => 'Meeting', + 'applications' => 'Applications', + 'tags' => 'Tags', + 'deadline' => 'Deadline', + 'amount' => 'Amount', + 'funding_stage' => 'Funding Stage', + 'crm_status' => 'CRM Status', + 'domain' => 'Domain', + 'website' => 'Website', + 'run' => 'Run', + 'save_source' => 'Save Source', + 'add_source' => 'Add Source', + 'telegram_notifications' => 'Telegram Notifications', + 'telegram_bot_token' => 'Bot Token', + 'telegram_chat_id' => 'Chat ID', + 'telegram_enabled' => 'Enable Notifications', + 'test_notification' => 'Send Test', + 'language' => 'Language', + 'arabic' => 'العربية', + 'english' => 'English', +]; \ No newline at end of file diff --git a/resources/views/admin/settings/index.php b/resources/views/admin/settings/index.php new file mode 100644 index 0000000..435a00c --- /dev/null +++ b/resources/views/admin/settings/index.php @@ -0,0 +1,60 @@ + + +session->getFlash('success')): ?> +
escape($flashSuccess) ?>
+ +session->getFlash('error')): ?> +
escape($flashError) ?>
+ + +
+ +
+

+ + +

+
+ + +
+ + +
+
+ + +
+
+ + style="width: 20px; height: 20px;"> +
+
+ + +
+
+
+ + +
+

How to set up Telegram

+
    +
  1. Create a bot via @BotFather on Telegram
  2. +
  3. Copy the bot token (e.g., 123456:ABC-DEF1234ghIkl)
  4. +
  5. Add the bot to your group/channel
  6. +
  7. Send any message in the group
  8. +
  9. Visit https://api.telegram.org/bot/getUpdates
  10. +
  11. Copy your Chat ID from the response
  12. +
  13. Click "Send Test" to verify
  14. +
+
+
+ + \ No newline at end of file