Deploy on 2026-06-05 16:08:53

This commit is contained in:
Hamza-Ayed
2026-06-05 16:08:53 +03:00
parent c0da60069f
commit 54065628bf
12 changed files with 316 additions and 167 deletions

View File

@@ -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');
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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';
}
/**

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}