diff --git a/app/Controllers/Admin/DashboardController.php b/app/Controllers/Admin/DashboardController.php index f812e77..210dd58 100644 --- a/app/Controllers/Admin/DashboardController.php +++ b/app/Controllers/Admin/DashboardController.php @@ -20,9 +20,6 @@ class DashboardController extends Controller 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(); @@ -31,6 +28,8 @@ class DashboardController extends Controller $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(); + $todayContacts = (int)$this->pdo->query("SELECT COUNT(*) FROM contacts WHERE DATE(created_at) = CURDATE() AND deleted_at IS NULL")->fetchColumn(); + $todayOrgs = (int)$this->pdo->query("SELECT COUNT(*) FROM organizations WHERE DATE(created_at) = CURDATE() AND deleted_at IS NULL")->fetchColumn(); // Recent opportunities $stmt = $this->pdo->query( @@ -50,14 +49,45 @@ class DashboardController extends Controller $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 : []; + // 6-month growth data + $months = []; + $opportunityMonthly = []; + $interactionMonthly = []; + for ($i = 5; $i >= 0; $i--) { + $date = new \DateTime("-$i months"); + $key = $date->format('Y-m'); + $name = $date->format('M'); + $months[$key] = $name; + $opportunityMonthly[$key] = 0; + $interactionMonthly[$key] = 0; + } + + $oppsQuery = $this->pdo->query( + "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count + FROM opportunities + WHERE deleted_at IS NULL AND created_at >= DATE_SUB(NOW(), INTERVAL 6 MONTH) + GROUP BY month" + )->fetchAll(PDO::FETCH_KEY_PAIR) ?: []; + + $interQuery = $this->pdo->query( + "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count + FROM interactions + WHERE created_at >= DATE_SUB(NOW(), INTERVAL 6 MONTH) + GROUP BY month" + )->fetchAll(PDO::FETCH_KEY_PAIR) ?: []; + + foreach ($oppsQuery as $monthKey => $count) { + if (isset($opportunityMonthly[$monthKey])) { + $opportunityMonthly[$monthKey] = (int)$count; + } + } + foreach ($interQuery as $monthKey => $count) { + if (isset($interactionMonthly[$monthKey])) { + $interactionMonthly[$monthKey] = (int)$count; + } + } return $this->render('admin/dashboard', [ - 'user' => $user, - 'title' => $t['dashboard'] ?? 'Dashboard', - 't' => $t, - 'lang' => $lang, 'stats' => [ 'organizations' => $orgCount, 'vc' => $vcCount, @@ -66,10 +96,15 @@ class DashboardController extends Controller 'contacts' => $contactCount, 'sources' => $sourceCount, 'today' => $todayOpps, + 'today_contacts' => $todayContacts, + 'today_orgs' => $todayOrgs, ], 'recent_opportunities' => $recentOpps, 'opportunities_by_type' => $byType, 'recent_activities' => $recentActivities, + 'growth_labels' => array_values($months), + 'growth_opportunities' => array_values($opportunityMonthly), + 'growth_interactions' => array_values($interactionMonthly), ], 'admin'); } } \ No newline at end of file diff --git a/app/Controllers/Admin/SettingsController.php b/app/Controllers/Admin/SettingsController.php index f51523f..0f5d08a 100644 --- a/app/Controllers/Admin/SettingsController.php +++ b/app/Controllers/Admin/SettingsController.php @@ -24,18 +24,12 @@ class SettingsController extends Controller 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, @@ -47,33 +41,29 @@ class SettingsController extends Controller $tgToken = $request->post('telegram_bot_token', ''); $tgChatId = $request->post('telegram_chat_id', ''); $tgEnabled = $request->post('telegram_enabled', '0'); + $action = $request->post('action', 'save'); $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!'); + if ($action === 'test') { + $this->notifier->configure($tgToken, $tgChatId); + if ($this->notifier->sendTest()) { + $this->session->setFlash('success', 'Settings saved and test notification sent to Telegram!'); + } else { + $err = $this->notifier->getLastError() ?: 'Check your token and chat ID.'; + $this->session->setFlash('error', 'Settings saved, but failed to send Telegram notification: ' . $err); + } } else { - $this->session->setFlash('error', 'Failed to send Telegram notification. Check your token and chat ID.'); + $this->session->setFlash('success', 'Settings saved successfully.'); } $response->redirect('/admin/settings'); } - public function switchLang(Request $request, Response $response): void + public function switchLang(Request $request, Response $response, string $lang = 'en'): void { - $lang = $request->get('lang', 'en'); + $lang = $request->routeParam('lang', $lang); if (in_array($lang, ['ar', 'en'])) { $this->session->set('lang', $lang); } diff --git a/app/Controllers/Controller.php b/app/Controllers/Controller.php index 7368f9f..d8a566b 100644 --- a/app/Controllers/Controller.php +++ b/app/Controllers/Controller.php @@ -24,6 +24,18 @@ abstract class Controller throw new \Exception("View template {$view} not found."); } + // Automatically inject current language, translation array, and authenticated user + $lang = $this->session->get('lang', 'en'); + $langFile = __DIR__ . "/../../resources/lang/{$lang}.php"; + $t = file_exists($langFile) ? require $langFile : []; + $user = \App\Core\App::$app->request->routeParam('_authenticated_user'); + + $data = array_merge([ + 'lang' => $lang, + 't' => $t, + 'user' => $user, + ], $data); + // Extract variables to local scope extract($data); diff --git a/app/Core/App.php b/app/Core/App.php index c14811d..7c50cbf 100644 --- a/app/Core/App.php +++ b/app/Core/App.php @@ -40,14 +40,15 @@ class App $this->request->setRouteParams($params); // Run Middleware Chain - $this->executeMiddlewareChain($middlewares, function() use ($callback) { + $this->executeMiddlewareChain($middlewares, function() use ($callback, $params) { // Execute Route action if (is_callable($callback)) { - $response = $callback($this->request, $this->response); + $response = call_user_func_array($callback, array_merge([$this->request, $this->response], array_values($params))); } else { [$controllerClass, $method] = $callback; $controller = $this->container->get($controllerClass); - $response = $controller->$method($this->request, $this->response); + $args = array_merge([$this->request, $this->response], array_values($params)); + $response = call_user_func_array([$controller, $method], $args); } // Auto-output string responses as HTML diff --git a/app/Services/Crawler/AiAnalyzer.php b/app/Services/Crawler/AiAnalyzer.php index ef119c5..13aeacf 100644 --- a/app/Services/Crawler/AiAnalyzer.php +++ b/app/Services/Crawler/AiAnalyzer.php @@ -13,7 +13,7 @@ class AiAnalyzer { $config = require __DIR__ . '/../../../config/ai.php'; $this->apiKey = $config['gemini']['api_key'] ?? null; - $this->model = $config['gemini']['model'] ?? 'gemini-1.5-flash-latest'; + $this->model = $config['gemini']['model'] ?? 'gemini-flash-lite-latest'; } /** diff --git a/app/Services/Crawler/Collector.php b/app/Services/Crawler/Collector.php index a5e64f7..c2585f3 100644 --- a/app/Services/Crawler/Collector.php +++ b/app/Services/Crawler/Collector.php @@ -4,6 +4,7 @@ namespace App\Services\Crawler; use App\Services\Database\Connection; use App\Services\Database\ActivityLogger; +use App\Services\Notification\TelegramNotifier; use PDO; use Throwable; @@ -13,17 +14,20 @@ class Collector private RssParser $rssParser; private AiAnalyzer $aiAnalyzer; private ActivityLogger $logger; + private TelegramNotifier $notifier; public function __construct( Connection $connection, RssParser $rssParser, AiAnalyzer $aiAnalyzer, - ActivityLogger $logger + ActivityLogger $logger, + TelegramNotifier $notifier ) { $this->pdo = $connection->getPdo(); $this->rssParser = $rssParser; $this->aiAnalyzer = $aiAnalyzer; $this->logger = $logger; + $this->notifier = $notifier; } /** @@ -76,6 +80,12 @@ class Collector 'new_organizations' => $results['new_organizations'], ])); + // Send Telegram notification if enabled + if ($this->getSetting('telegram_enabled') === '1') { + $this->notifier->loadSettings(); + $this->notifier->notifyCollectorResults($results); + } + return $results; } @@ -203,6 +213,25 @@ class Collector } } + // Trigger Telegram notification if enabled + if ($this->getSetting('telegram_enabled') === '1') { + $orgName = ''; + if ($orgId) { + $orgStmt = $this->pdo->prepare("SELECT name FROM organizations WHERE id = ?"); + $orgStmt->execute([$orgId]); + $orgName = $orgStmt->fetchColumn() ?: ''; + } + $this->notifier->loadSettings(); + $this->notifier->notifyNewOpportunity([ + 'title' => $entry['title'], + 'type' => $analysis['opportunity_type'] ?? $analysis['type'] ?? 'other', + 'score' => $score, + 'url' => $entry['url'], + 'description' => $analysis['summary'] ?? $entry['description'], + 'org_name' => $orgName, + ]); + } + } catch (Throwable $e) { // Log but don't fail } @@ -246,4 +275,19 @@ class Collector ); return $stmt->fetchAll() ?: []; } + + /** + * Get a setting by key from the database. + */ + private function getSetting(string $key): ?string + { + try { + $stmt = $this->pdo->prepare("SELECT `value` FROM settings WHERE `key` = ?"); + $stmt->execute([$key]); + $val = $stmt->fetchColumn(); + return $val !== false ? $val : null; + } catch (Throwable $e) { + return null; + } + } } \ No newline at end of file diff --git a/app/Services/Notification/TelegramNotifier.php b/app/Services/Notification/TelegramNotifier.php index 369bc12..6e06564 100644 --- a/app/Services/Notification/TelegramNotifier.php +++ b/app/Services/Notification/TelegramNotifier.php @@ -2,21 +2,49 @@ namespace App\Services\Notification; +use App\Services\Database\Connection; +use PDO; use Throwable; class TelegramNotifier { - private ?string $botToken; - private ?string $chatId; + private PDO $pdo; + private ?string $botToken = null; + private ?string $chatId = null; + private ?string $lastError = null; - public function __construct() + public function __construct(Connection $connection) { - $this->botToken = $_ENV['TELEGRAM_BOT_TOKEN'] ?? null; - $this->chatId = $_ENV['TELEGRAM_CHAT_ID'] ?? null; + $this->pdo = $connection->getPdo(); + $this->loadSettings(); } /** - * Configure from database settings. + * Load settings from database settings table. + */ + public function loadSettings(): void + { + $this->botToken = $this->getSetting('telegram_bot_token'); + $this->chatId = $this->getSetting('telegram_chat_id'); + } + + /** + * Helper to retrieve setting by key. + */ + private function getSetting(string $key): ?string + { + try { + $stmt = $this->pdo->prepare("SELECT `value` FROM settings WHERE `key` = ?"); + $stmt->execute([$key]); + $val = $stmt->fetchColumn(); + return $val !== false ? $val : null; + } catch (Throwable $e) { + return null; + } + } + + /** + * Configure manually (for testing unsaved parameters). */ public function configure(string $botToken, string $chatId): void { @@ -24,12 +52,23 @@ class TelegramNotifier $this->chatId = $chatId; } + /** + * Retrieve the last HTTP or API error message. + */ + public function getLastError(): ?string + { + return $this->lastError; + } + /** * Send a notification to Telegram. */ public function send(string $message, string $type = 'info'): bool { + $this->lastError = null; + if (!$this->botToken || !$this->chatId) { + $this->lastError = "Telegram token or chat ID is missing."; return false; } @@ -54,19 +93,62 @@ class TelegramNotifier '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], - ]); + if (function_exists('curl_init')) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); - $response = @file_get_contents($url, false, $context); - return $response !== false; + $response = curl_exec($ch); + if ($response === false) { + $this->lastError = "Connection error: " . curl_error($ch); + curl_close($ch); + return false; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + $resDecoded = json_decode($response, true); + $this->lastError = $resDecoded['description'] ?? "API error HTTP {$httpCode}"; + error_log("Telegram API returned code {$httpCode}: {$response}"); + return false; + } + + return true; + } else { + $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); + if ($response === false) { + $error = error_get_last(); + $this->lastError = $error['message'] ?? "Connection timed out."; + return false; + } + + $resDecoded = json_decode($response, true); + if (($resDecoded['ok'] ?? false) === false) { + $this->lastError = $resDecoded['description'] ?? "API error"; + return false; + } + + return true; + } } catch (Throwable $e) { + $this->lastError = $e->getMessage(); return false; } } diff --git a/public/index.php b/public/index.php index 1f6bcbb..4c53829 100644 --- a/public/index.php +++ b/public/index.php @@ -77,7 +77,6 @@ $app->router->group([ // Settings $r->get('/settings', [SettingsController::class, 'index']); $r->post('/settings/save', [SettingsController::class, 'save']); - $r->post('/settings/test-telegram', [SettingsController::class, 'testTelegram']); }); // Logout endpoint diff --git a/resources/lang/ar.php b/resources/lang/ar.php index 8a7c944..8f1f4bf 100644 --- a/resources/lang/ar.php +++ b/resources/lang/ar.php @@ -71,4 +71,18 @@ return [ 'language' => 'اللغة', 'arabic' => 'العربية', 'english' => 'English', + 'title' => 'العنوان', + 'grant' => 'المنح', + 'competition' => 'المسابقات', + 'demo_day' => 'أيام العروض', + 'event' => 'الفعاليات', + 'partnership' => 'الشراكات', + 'investment' => 'الاستثمارات', + 'other' => 'أخرى', + 'ingested_opportunities' => 'الفرص المجمعة', + 'interactions_logged' => 'التفاعلات المسجلة', + 'today_contacts' => 'جهات اتصال مضافة اليوم', + 'today_orgs' => 'منظمات جديدة', + 'recent_activities' => 'آخر النشاطات', + 'recent_opportunities' => 'آخر الفرص', ]; \ No newline at end of file diff --git a/resources/lang/en.php b/resources/lang/en.php index e35e5a1..fb556ad 100644 --- a/resources/lang/en.php +++ b/resources/lang/en.php @@ -71,4 +71,18 @@ return [ 'language' => 'Language', 'arabic' => 'العربية', 'english' => 'English', + 'title' => 'Title', + 'grant' => 'Grants', + 'competition' => 'Competitions', + 'demo_day' => 'Demo Days', + 'event' => 'Events', + 'partnership' => 'Partnerships', + 'investment' => 'Investments', + 'other' => 'Other', + 'ingested_opportunities' => 'Ingested Opportunities', + 'interactions_logged' => 'Interactions Logged', + 'today_contacts' => 'Contacts Added Today', + 'today_orgs' => 'new organizations', + 'recent_activities' => 'Recent Activities', + 'recent_opportunities' => 'Recent Opportunities', ]; \ No newline at end of file diff --git a/resources/views/admin/dashboard.php b/resources/views/admin/dashboard.php index 91cbfef..afd8ee3 100644 --- a/resources/views/admin/dashboard.php +++ b/resources/views/admin/dashboard.php @@ -1,143 +1,107 @@
-

Welcome, escape($user['name']) ?>

-

Here is your daily intelligence summary for ScoutIQ.

+
+
+

+

, escape($user['name']) ?>

+
+ +
- Total Investors - 142 + + - +12% vs last month + VC • Accelerators
- Total Accelerators - 38 + + - +5% vs last month + new today
- Open Opportunities - 29 - - - 4 closing this week + + + + +
- Contacts Added Today - 7 + + - - +3 new organizations + + RSS feeds active
-
+
- Opportunities by Category -
- +
+ + +
+
+ +

No opportunities yet. Run the collector to start gathering data.

+ + +
+
+
+ escape(mb_substr($opp['title'], 0, 80)) ?> +
+ escape($opp['type']) ?> + Score: +
+
+ +
+
+ +
+
- Monthly Growth & Ingestion -
- +
+ +
+
+ +

No recent activity.

+ + +
+
+
+ escape($activity['action'] ?? 'info') ?> + escape(mb_substr($activity['description'] ?? '', 0, 100)) ?> +
+ +
+
+ +
- + diff --git a/resources/views/admin/settings/index.php b/resources/views/admin/settings/index.php index 945988a..d585d20 100644 --- a/resources/views/admin/settings/index.php +++ b/resources/views/admin/settings/index.php @@ -33,15 +33,9 @@
+
- -
- - - - -