diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php new file mode 100644 index 0000000..00b1b00 --- /dev/null +++ b/ai/LocalAIApi.php @@ -0,0 +1,311 @@ + [ +// ['role' => 'system', 'content' => 'You are a helpful assistant.'], +// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], +// ], +// ]); +// if (!empty($response['success'])) { +// $decoded = LocalAIApi::decodeJsonFromResponse($response); +// } + +class LocalAIApi +{ + /** @var array|null */ + private static ?array $configCache = null; + + /** + * Signature compatible with the OpenAI Responses API. + * + * @param array $params Request body (model, input, text, reasoning, metadata, etc.). + * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). + * @return array{ + * success:bool, + * status?:int, + * data?:mixed, + * error?:string, + * response?:mixed, + * message?:string + * } + */ + public static function createResponse(array $params, array $options = []): array + { + $cfg = self::config(); + $payload = $params; + + if (empty($payload['input']) || !is_array($payload['input'])) { + return [ + 'success' => false, + 'error' => 'input_missing', + 'message' => 'Parameter "input" is required and must be an array.', + ]; + } + + if (!isset($payload['model']) || $payload['model'] === '') { + $payload['model'] = $cfg['default_model']; + } + + return self::request($options['path'] ?? null, $payload, $options); + } + + /** + * Snake_case alias for createResponse (matches the provided example). + * + * @param array $params + * @param array $options + * @return array + */ + public static function create_response(array $params, array $options = []): array + { + return self::createResponse($params, $options); + } + + /** + * Perform a raw request to the AI proxy. + * + * @param string $path Endpoint (may be an absolute URL). + * @param array $payload JSON payload. + * @param array $options Additional request options. + * @return array + */ + public static function request(?string $path = null, array $payload = [], array $options = []): array + { + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'curl_missing', + 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', + ]; + } + + $cfg = self::config(); + + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting AI request.', + ]; + } + + $defaultPath = $cfg['responses_path'] ?? null; + $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); + if (empty($resolvedPath)) { + return [ + 'success' => false, + 'error' => 'project_id_missing', + 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', + ]; + } + + $url = self::buildUrl($resolvedPath, $cfg['base_url']); + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + ]; + $headers[] = $projectHeader . ': ' . $projectUuid; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { + $payload['project_uuid'] = $projectUuid; + } + + $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if ($body === false) { + return [ + 'success' => false, + 'error' => 'json_encode_failed', + 'message' => 'Failed to encode request body to JSON.', + ]; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + + $responseBody = curl_exec($ch); + if ($responseBody === false) { + $error = curl_error($ch) ?: 'Unknown cURL error'; + curl_close($ch); + return [ + 'success' => false, + 'error' => 'curl_error', + 'message' => $error, + ]; + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $decoded = null; + if ($responseBody !== '' && $responseBody !== null) { + $decoded = json_decode($responseBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $decoded = null; + } + } + + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded ?? $responseBody, + ]; + } + + $errorMessage = 'AI proxy request failed'; + if (is_array($decoded)) { + $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; + } elseif (is_string($responseBody) && $responseBody !== '') { + $errorMessage = $responseBody; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $errorMessage, + 'response' => $decoded ?? $responseBody, + ]; + } + + /** + * Extract plain text from a Responses API payload. + * + * @param array $response Result of LocalAIApi::createResponse|request. + * @return string + */ + public static function extractText(array $response): string + { + $payload = $response['data'] ?? $response; + if (!is_array($payload)) { + return ''; + } + + if (!empty($payload['output']) && is_array($payload['output'])) { + $combined = ''; + foreach ($payload['output'] as $item) { + if (!isset($item['content']) || !is_array($item['content'])) { + continue; + } + foreach ($item['content'] as $block) { + if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { + $combined .= $block['text']; + } + } + } + if ($combined !== '') { + return $combined; + } + } + + if (!empty($payload['choices'][0]['message']['content'])) { + return (string) $payload['choices'][0]['message']['content']; + } + + return ''; + } + + /** + * Attempt to decode JSON emitted by the model (handles markdown fences). + * + * @param array $response + * @return array|null + */ + public static function decodeJsonFromResponse(array $response): ?array + { + $text = self::extractText($response); + if ($text === '') { + return null; + } + + $decoded = json_decode($text, true); + if (is_array($decoded)) { + return $decoded; + } + + $stripped = preg_replace('/^```json|```$/m', '', trim($text)); + if ($stripped !== null && $stripped !== $text) { + $decoded = json_decode($stripped, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } + + /** + * Load configuration from ai/config.php. + * + * @return array + */ + private static function config(): array + { + if (self::$configCache === null) { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new RuntimeException('AI config file not found: ai/config.php'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new RuntimeException('Invalid AI config format: expected array'); + } + self::$configCache = $cfg; + } + + return self::$configCache; + } + + /** + * Build an absolute URL from base_url and a path. + */ + private static function buildUrl(string $path, string $baseUrl): string + { + $trimmed = trim($path); + if ($trimmed === '') { + return $baseUrl; + } + if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { + return $trimmed; + } + if ($trimmed[0] === '/') { + return $baseUrl . $trimmed; + } + return $baseUrl . '/' . $trimmed; + } +} + +// Legacy alias for backward compatibility with the previous class name. +if (!class_exists('OpenAIService')) { + class_alias(LocalAIApi::class, 'OpenAIService'); +} diff --git a/ai/config.php b/ai/config.php new file mode 100644 index 0000000..1ba1596 --- /dev/null +++ b/ai/config.php @@ -0,0 +1,52 @@ + $baseUrl, + 'responses_path' => $responsesPath, + 'project_id' => $projectId, + 'project_uuid' => $projectUuid, + 'project_header' => 'project-uuid', + 'default_model' => 'gpt-5', + 'timeout' => 30, + 'verify_tls' => true, +]; diff --git a/api/chat.php b/api/chat.php new file mode 100644 index 0000000..b084fc0 --- /dev/null +++ b/api/chat.php @@ -0,0 +1,67 @@ + 'No message provided.']); + exit; +} + +// --- Context Gathering --- +$context = ""; + +try { + $pdo = db(); + + // Fetch candidates + $stmt = $pdo->query("SELECT name, status, email FROM candidates ORDER BY created_at DESC LIMIT 10"); + $candidates = $stmt->fetchAll(PDO::FETCH_ASSOC); + $context .= "\n\nRecent Candidates:\n" . json_encode($candidates, JSON_PRETTY_PRINT); + + // Fetch tasks + $stmt = $pdo->query("SELECT t.task_name, c.name as assigned_to, t.status, t.due_date FROM tasks t JOIN candidates c ON t.candidate_id = c.id ORDER BY t.created_at DESC LIMIT 10"); + $tasks = $stmt->fetchAll(PDO::FETCH_ASSOC); + $context .= "\n\nRecent Tasks:\n" . json_encode($tasks, JSON_PRETTY_PRINT); + +} catch (PDOException $e) { + // Don't expose DB errors to the user, but log them. + error_log("AI Chat Context Error: " . $e->getMessage()); + // Provide a fallback context + $context = "\n\nCould not fetch real-time data. Please rely on general knowledge."; +} + + +// --- AI Interaction --- +$systemPrompt = << 'system', 'content' => $systemPrompt], + ['role' => 'user', 'content' => $userMessage] +]; + +$response = LocalAIApi::createResponse([ + 'input' => $messages, +]); + +if (!empty($response['success'])) { + $decoded = LocalAIApi::decodeJsonFromResponse($response); + $aiReply = $decoded['choices'][0]['message']['content'] ?? 'Sorry, I could not process that.'; + echo json_encode(['reply' => $aiReply]); +} else { + error_log('AI error: ' . ($response['error'] ?? 'unknown')); + echo json_encode(['error' => 'Failed to get a response from the AI.']); +} + +?> \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..23678e4 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,271 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap'); + +:root { + --primary-color: #3B82F6; + --secondary-color: #10B981; + --background-color: #F3F4F6; + --surface-color: #FFFFFF; + --text-color: #1F2937; + --text-color-light: #6B7280; + --border-color: #D1D5DB; + --border-radius: 0.75rem; +} + +body { + font-family: 'Poppins', sans-serif; + background-color: var(--background-color); + color: var(--text-color); +} + +.header { + background-color: var(--surface-color); + border-bottom: 1px solid var(--border-color); + padding: 1.5rem 2.5rem; +} + +.logo { + font-weight: 700; + font-size: 1.75rem; + color: var(--text-color); +} + +.logo .dot { + color: var(--primary-color); +} + +.main-content { + padding: 2.5rem; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.page-title { + font-size: 2.25rem; + font-weight: 700; +} + +.btn-primary { + background-color: var(--primary-color); + border-color: var(--primary-color); + border-radius: var(--border-radius); + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all 0.2s ease-in-out; +} + +.btn-primary:hover { + background-color: #2563EB; + border-color: #2563EB; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.card { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease-in-out; +} + +.card:hover { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + transform: translateY(-2px); +} + +.table { + border-collapse: separate; + border-spacing: 0; +} + +.table th { + color: var(--text-color-light); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.875rem; + border-bottom: 2px solid var(--border-color) !important; + padding: 1rem 1.5rem; +} + +.table td { + vertical-align: middle; + padding: 1.25rem 1.5rem; +} + +.table tbody tr { + transition: all 0.2s ease-in-out; +} + +.table tbody tr:hover { + background-color: #F9FAFB; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.table tbody tr:last-child td { + border-bottom: none; +} + +.avatar { + width: 48px; + height: 48px; + border-radius: 50%; + margin-right: 1.25rem; + object-fit: cover; +} + +.candidate-name { + font-weight: 600; + font-size: 1.125rem; +} + +.candidate-email { + color: var(--text-color-light); + font-size: 1rem; +} + +.status-badge { + display: inline-block; + padding: 0.35em 0.75em; + font-size: .875em; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.5rem; +} + +.status-new { background-color: #DBEAFE; color: #2563EB; } +.status-interview { background-color: #FEF3C7; color: #D97706; } +.status-hired { background-color: #D1FAE5; color: #059669; } +.status-rejected { background-color: #FEE2E2; color: #DC2626; } + +.action-icon { + color: var(--text-color-light); + cursor: pointer; + transition: all 0.2s ease-in-out; +} +.action-icon:hover { + color: var(--text-color); + transform: scale(1.1); +} + +.status-todo { background-color: #E0E7FF; color: #4338CA; } +.status-in-progress { background-color: #FEF9C3; color: #A16207; } +.status-done { background-color: #D1FAE5; color: #059669; } + +/* Chat Interface Styles */ +.chat-toggle-button { + position: fixed; + bottom: 2.5rem; + right: 2.5rem; + width: 64px; + height: 64px; + border-radius: 50%; + background-color: var(--primary-color); + color: white; + border: none; + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0 8px 16px rgba(0,0,0,0.2); + cursor: pointer; + z-index: 1000; + transition: all 0.2s ease-in-out; +} + +.chat-toggle-button:hover { + transform: scale(1.05); + box-shadow: 0 12px 24px rgba(0,0,0,0.2); +} + +.chat-container { + position: fixed; + bottom: 7rem; + right: 2.5rem; + width: 375px; + max-width: 90%; + background-color: var(--surface-color); + border-radius: var(--border-radius); + box-shadow: 0 8px 24px rgba(0,0,0,0.15); + display: none; /* Hidden by default */ + flex-direction: column; + z-index: 1000; +} + +.chat-header { + padding: 1.25rem; + background-color: var(--primary-color); + color: white; + display: flex; + justify-content: space-between; + align-items: center; + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} + +.chat-body { + padding: 1.25rem; + height: 350px; + overflow-y: auto; +} + +.chat-input-container { + display: flex; + padding: 1.25rem; + border-top: 1px solid var(--border-color); +} + +#chat-input { + flex-grow: 1; + margin-right: 0.75rem; + border-radius: var(--border-radius); +} + +.chat-message { + padding: 0.75rem 1.25rem; + border-radius: 1.25rem; + margin-bottom: 0.75rem; + max-width: 85%; + line-height: 1.5; +} + +.chat-message-user { + background-color: var(--primary-color); + color: white; + align-self: flex-end; + margin-left: auto; +} + +.chat-message-ai { + background-color: #E5E7EB; + color: var(--text-color); + align-self: flex-start; +} + +/* Dashboard Specific Styles */ +.dashboard-card { + margin-bottom: 1.5rem; +} + +.card-title { + font-weight: 600; +} + +.pagination { + justify-content: center; +} + +.table-responsive { + margin-top: 1rem; +} + +.badge { + font-size: 0.9em; + padding: 0.5em 0.75em; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..802412a --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,74 @@ +// FinMox Flow main.js + +document.addEventListener('DOMContentLoaded', function() { + const chatContainer = document.getElementById('chat-container'); + const chatToggle = document.getElementById('chat-toggle'); + const closeChat = document.getElementById('close-chat'); + const chatInput = document.getElementById('chat-input'); + const sendChat = document.getElementById('send-chat'); + const chatBody = document.getElementById('chat-body'); + + // Toggle chat window + if (chatToggle) { + chatToggle.addEventListener('click', function() { + chatContainer.style.display = (chatContainer.style.display === 'flex') ? 'none' : 'flex'; + }); + } + + // Close chat window + if (closeChat) { + closeChat.addEventListener('click', function() { + chatContainer.style.display = 'none'; + }); + } + + // Send message + const sendMessage = () => { + const message = chatInput.value.trim(); + if (message === '') return; + + appendMessage(message, 'user'); + chatInput.value = ''; + + fetch('api/chat.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message: message }) + }) + .then(response => response.json()) + .then(data => { + if (data.reply) { + appendMessage(data.reply, 'ai'); + } else { + appendMessage('Sorry, something went wrong.', 'ai'); + } + }) + .catch(error => { + console.error('Error:', error); + appendMessage('Sorry, something went wrong.', 'ai'); + }); + }; + + if (sendChat) { + sendChat.addEventListener('click', sendMessage); + } + + if (chatInput) { + chatInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + sendMessage(); + } + }); + } + + // Append message to chat body + const appendMessage = (message, sender) => { + const messageElement = document.createElement('div'); + messageElement.classList.add('chat-message', `chat-message-${sender}`); + messageElement.textContent = message; + chatBody.appendChild(messageElement); + chatBody.scrollTop = chatBody.scrollHeight; // Scroll to bottom + }; +}); diff --git a/assets/pasted-20251111-052937-ae05f6a6.jpg b/assets/pasted-20251111-052937-ae05f6a6.jpg new file mode 100644 index 0000000..9b1e34d Binary files /dev/null and b/assets/pasted-20251111-052937-ae05f6a6.jpg differ diff --git a/assets/pasted-20251111-062128-0e3cd006.jpg b/assets/pasted-20251111-062128-0e3cd006.jpg new file mode 100644 index 0000000..02dd8ba Binary files /dev/null and b/assets/pasted-20251111-062128-0e3cd006.jpg differ diff --git a/assets/vm-shot-2025-11-11T05-29-19-670Z.jpg b/assets/vm-shot-2025-11-11T05-29-19-670Z.jpg new file mode 100644 index 0000000..9b1e34d Binary files /dev/null and b/assets/vm-shot-2025-11-11T05-29-19-670Z.jpg differ diff --git a/assets/vm-shot-2025-11-11T06-19-43-142Z.jpg b/assets/vm-shot-2025-11-11T06-19-43-142Z.jpg new file mode 100644 index 0000000..02dd8ba Binary files /dev/null and b/assets/vm-shot-2025-11-11T06-19-43-142Z.jpg differ diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..ea1d589 --- /dev/null +++ b/auth.php @@ -0,0 +1,86 @@ +prepare("SELECT id FROM roles WHERE name = 'Admin'"); + $stmt->execute(); + $role = $stmt->fetch(); + $default_role_id = $role ? $role['id'] : null; + + try { + $stmt = $pdo->prepare("INSERT INTO users (username, password, role_id, email) VALUES (?, ?, ?, ?)"); + return $stmt->execute([$username, $password_hash, $default_role_id, $email]); + } catch (PDOException $e) { + // Handle duplicate username + error_log($e->getMessage()); + return false; + } +} + +function login_user($username, $password) { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role_id'] = $user['role_id']; + return true; + } + return false; +} + +function is_logged_in() { + return isset($_SESSION['user_id']); +} + +function logout_user() { + session_unset(); + session_destroy(); +} + +function get_user_role_id() { + return $_SESSION['role_id'] ?? null; +} + +function hasPermission($permission) { + if (!is_logged_in()) { + return false; + } + + $role_id = get_user_role_id(); + if (!$role_id) { + return false; + } + + // Super admin (role_id 1) has all permissions + if ($role_id == 1) { + return true; + } + + $pdo = db(); + + // Get the permission ID from the permission name + $stmt = $pdo->prepare("SELECT id FROM permissions WHERE name = ?"); + $stmt->execute([$permission]); + $permission_id = $stmt->fetchColumn(); + + if (!$permission_id) { + return false; // Permission not found + } + + // Check if the role has the permission + $stmt = $pdo->prepare("SELECT 1 FROM role_permissions WHERE role_id = ? AND permission_id = ?"); + $stmt->execute([$role_id, $permission_id]); + + return $stmt->fetchColumn() !== false; +} + +?> \ No newline at end of file diff --git a/chat.php b/chat.php new file mode 100644 index 0000000..9feab1d --- /dev/null +++ b/chat.php @@ -0,0 +1,164 @@ + + + + + + + + + AI Chat - FinMox + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..875111d --- /dev/null +++ b/dashboard.php @@ -0,0 +1,460 @@ +query('SELECT status, COUNT(*) as count FROM candidates GROUP BY status'); +$candidate_stats = $stmt->fetchAll(PDO::FETCH_ASSOC); +$total_candidates = array_sum(array_column($candidate_stats, 'count')); + +// Task stats +$stmt = $pdo->query('SELECT status, COUNT(*) as count FROM tasks GROUP BY status'); +$task_stats = $stmt->fetchAll(PDO::FETCH_ASSOC); +$total_tasks = array_sum(array_column($task_stats, 'count')); +$completed_tasks = 0; +foreach ($task_stats as $stat) { + if ($stat['status'] === 'Completed') { + $completed_tasks = $stat['count']; + break; + } +} + +$candidate_status_labels = json_encode(array_column($candidate_stats, 'status')); +$candidate_status_data = json_encode(array_column($candidate_stats, 'count')); + +$task_status_labels = json_encode(array_column($task_stats, 'status')); +$task_status_data = json_encode(array_column($task_stats, 'count')); + +// Candidates per day +$stmt = $pdo->query("SELECT DATE(created_at) as date, COUNT(*) as count FROM candidates GROUP BY DATE(created_at) ORDER BY DATE(created_at) ASC"); +$candidates_per_day = $stmt->fetchAll(PDO::FETCH_ASSOC); +$candidates_per_day_labels = json_encode(array_column($candidates_per_day, 'date')); +$candidates_per_day_data = json_encode(array_column($candidates_per_day, 'count')); + +// Tasks per day +$stmt = $pdo->query("SELECT DATE(created_at) as date, COUNT(*) as count FROM tasks GROUP BY DATE(created_at) ORDER BY DATE(created_at) ASC"); +$tasks_per_day = $stmt->fetchAll(PDO::FETCH_ASSOC); +$tasks_per_day_labels = json_encode(array_column($tasks_per_day, 'date')); +$tasks_per_day_data = json_encode(array_column($tasks_per_day, 'count')); + +// Tasks completed per day +$stmt = $pdo->query("SELECT DATE(updated_at) as date, COUNT(*) as count FROM tasks WHERE status = 'Done' GROUP BY DATE(updated_at) ORDER BY DATE(updated_at) ASC"); +$tasks_completed_per_day = $stmt->fetchAll(PDO::FETCH_ASSOC); +$tasks_completed_per_day_labels = json_encode(array_column($tasks_completed_per_day, 'date')); +$tasks_completed_per_day_data = json_encode(array_column($tasks_completed_per_day, 'count')); + +// Fetch candidates for table +$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; +$limit = 5; +$offset = ($page - 1) * $limit; +$stmt = $pdo->prepare("SELECT * FROM candidates LIMIT :limit OFFSET :offset"); +$stmt->bindParam(':limit', $limit, PDO::PARAM_INT); +$stmt->bindParam(':offset', $offset, PDO::PARAM_INT); +$stmt->execute(); +$candidates = $stmt->fetchAll(PDO::FETCH_ASSOC); +$stmt = $pdo->query("SELECT COUNT(*) FROM candidates"); +$total_candidates_records = $stmt->fetchColumn(); +$total_candidate_pages = ceil($total_candidates_records / $limit); + +// Fetch tasks for table +$stmt = $pdo->prepare("SELECT * FROM tasks LIMIT :limit OFFSET :offset"); +$stmt->bindParam(':limit', $limit, PDO::PARAM_INT); +$stmt->bindParam(':offset', $offset, PDO::PARAM_INT); +$stmt->execute(); +$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC); +$stmt = $pdo->query("SELECT COUNT(*) FROM tasks"); +$total_tasks_records = $stmt->fetchColumn(); +$total_task_pages = ceil($total_tasks_records / $limit); + +?> + + + + + + Analytics Dashboard + + + + + +
+ + +
+ +
+

Analytics Dashboard

+ + + + + +
+ +
+ +
+ +
+
+
+
Total Candidates
+

+
+
+
+ + +
+
+
+
Total Tasks
+

+
+
+
+
+
+
+
Completed Tasks
+

+
+
+
+ +
+ + +
+ +
+
+
+
Candidates by Status
+ +
+
+
+ + +
+
+
+
Tasks by Status
+ +
+
+
+ + +
+
+
+
Candidates per Day
+ +
+
+
+ +
+
+ +
+
+
+
Tasks Created per Day
+ +
+
+
+ + +
+
+
+
Tasks Completed per Day
+ +
+
+
+ +
+
+ + + +
+
+
+
+
+
+
Recent Candidates
+ Add Candidate +
+
+ + + + + + + + + + + + + + + + + + + +
NameEmailStatusActions
+ Edit + Delete +
+
+ +
+
+
+
+
+ + + + +
+
+
+
+
+
+
Recent Tasks
+ Add Task +
+
+ + + + + + + + + + + + + + + + + + + +
TitleStatusAssigned ToActions
+ Edit + Delete +
+
+ +
+
+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/db/config.php b/db/config.php index f12ebaf..f2db299 100644 --- a/db/config.php +++ b/db/config.php @@ -6,12 +6,38 @@ define('DB_USER', 'app_31009'); define('DB_PASS', '2c66b530-2a65-423a-a106-6760b49ad1a2'); function db() { - static $pdo; - if (!$pdo) { - $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - } - return $pdo; + static $pdo; + if ($pdo) { + return $pdo; + } + + try { + $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } catch (PDOException $e) { + if ($e->getCode() === 1049) { // SQLSTATE[HY000] [1049] Unknown database + try { + $tempPdo = new PDO('mysql:host='.DB_HOST, DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]); + $tempPdo->exec('CREATE DATABASE IF NOT EXISTS `'.DB_NAME.'`'); + + // Now, reconnect to the newly created database + $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } catch (PDOException $e) { + // If database creation also fails, re-throw the exception + throw $e; + } + } else { + // For any other PDO exception, re-throw it + throw $e; + } + } + + return $pdo; } diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..53228e8 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,21 @@ +exec($sql); + echo "Migration successful: " . basename($file) . "\n"; + } catch (PDOException $e) { + echo "Migration failed for " . basename($file) . ": " . $e->getMessage() . "\n"; + } + } +} + +run_migrations(); + diff --git a/db/migrations/001_create_candidates_table.sql b/db/migrations/001_create_candidates_table.sql new file mode 100644 index 0000000..c91caa5 --- /dev/null +++ b/db/migrations/001_create_candidates_table.sql @@ -0,0 +1,10 @@ + +CREATE TABLE IF NOT EXISTS `candidates` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL, + `phone` VARCHAR(50), + `status` ENUM('Applied', 'Interviewing', 'Offered', 'Hired', 'Rejected') NOT NULL DEFAULT 'Applied', + `notes` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/db/migrations/002_create_tasks_table.sql b/db/migrations/002_create_tasks_table.sql new file mode 100644 index 0000000..d7ba52f --- /dev/null +++ b/db/migrations/002_create_tasks_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS `tasks` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `candidate_id` INT, + `task_name` VARCHAR(255) NOT NULL, + `description` TEXT, + `due_date` DATE, + `status` VARCHAR(50) DEFAULT 'To Do', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`candidate_id`) REFERENCES `candidates`(`id`) ON DELETE CASCADE +); diff --git a/db/migrations/003_create_roles_and_permissions_tables.sql b/db/migrations/003_create_roles_and_permissions_tables.sql new file mode 100644 index 0000000..a141aee --- /dev/null +++ b/db/migrations/003_create_roles_and_permissions_tables.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS `roles` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL UNIQUE +); + +INSERT INTO `roles` (name) VALUES ('Admin'), ('HR'), ('Manager'), ('Employee'); + +CREATE TABLE IF NOT EXISTS `permissions` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL UNIQUE +); + +INSERT INTO `permissions` (name) VALUES +('view_dashboard'), +('manage_candidates'), +('manage_tasks'), +('manage_users'), +('view_reports'); + +CREATE TABLE IF NOT EXISTS `role_permissions` ( + `role_id` INT, + `permission_id` INT, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles(id), + FOREIGN KEY (permission_id) REFERENCES permissions(id) +); + +-- Admin permissions +INSERT INTO `role_permissions` (role_id, permission_id) VALUES +(1, 1), (1, 2), (1, 3), (1, 4), (1, 5); + +-- HR permissions +INSERT INTO `role_permissions` (role_id, permission_id) VALUES +(2, 1), (2, 2), (2, 3), (2, 5); + +-- Manager permissions +INSERT INTO `role_permissions` (role_id, permission_id) VALUES +(3, 1), (3, 3); + +-- Employee permissions +INSERT INTO `role_permissions` (role_id, permission_id) VALUES +(4, 3); \ No newline at end of file diff --git a/db/migrations/004_create_users_table.sql b/db/migrations/004_create_users_table.sql new file mode 100644 index 0000000..abef335 --- /dev/null +++ b/db/migrations/004_create_users_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `username` VARCHAR(255) NOT NULL UNIQUE, + `password` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL UNIQUE, + `role_id` INT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (role_id) REFERENCES roles(id) +); \ No newline at end of file diff --git a/db/migrations/005_create_workflows_table.sql b/db/migrations/005_create_workflows_table.sql new file mode 100644 index 0000000..f711c10 --- /dev/null +++ b/db/migrations/005_create_workflows_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `workflows` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `trigger` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/migrations/006_create_workflow_actions_table.sql b/db/migrations/006_create_workflow_actions_table.sql new file mode 100644 index 0000000..7caf35e --- /dev/null +++ b/db/migrations/006_create_workflow_actions_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS `workflow_actions` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `workflow_id` INT NOT NULL, + `action_type` VARCHAR(255) NOT NULL, + `config` JSON, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/db/migrations/007_add_role_id_to_users.sql b/db/migrations/007_add_role_id_to_users.sql new file mode 100644 index 0000000..5d3721c --- /dev/null +++ b/db/migrations/007_add_role_id_to_users.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN role_id INT, ADD FOREIGN KEY (role_id) REFERENCES roles(id); \ No newline at end of file diff --git a/db/migrations/008_create_tasks_table.sql b/db/migrations/008_create_tasks_table.sql new file mode 100644 index 0000000..ed28ad6 --- /dev/null +++ b/db/migrations/008_create_tasks_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + assignee_id INT, + status VARCHAR(50) NOT NULL DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (assignee_id) REFERENCES users(id) +); diff --git a/db/migrations/009_add_candidates_permissions.sql b/db/migrations/009_add_candidates_permissions.sql new file mode 100644 index 0000000..72c2297 --- /dev/null +++ b/db/migrations/009_add_candidates_permissions.sql @@ -0,0 +1,6 @@ +INSERT INTO permissions (name) VALUES ('manage_candidates'); + +INSERT INTO role_permissions (role_id, permission_id) +SELECT + (SELECT id FROM roles WHERE name = 'Admin'), + (SELECT id FROM permissions WHERE name = 'manage_candidates'); diff --git a/db/migrations/010_add_tasks_permissions.sql b/db/migrations/010_add_tasks_permissions.sql new file mode 100644 index 0000000..571a77f --- /dev/null +++ b/db/migrations/010_add_tasks_permissions.sql @@ -0,0 +1,11 @@ +INSERT INTO permissions (name) VALUES ('manage_tasks'), ('view_tasks'); + +INSERT INTO role_permissions (role_id, permission_id) +SELECT + (SELECT id FROM roles WHERE name = 'Admin'), + (SELECT id FROM permissions WHERE name = 'manage_tasks'); + +INSERT INTO role_permissions (role_id, permission_id) +SELECT + (SELECT id FROM roles WHERE name = 'Admin'), + (SELECT id FROM permissions WHERE name = 'view_tasks'); diff --git a/db/migrations/011_create_workflow_logs_table.sql b/db/migrations/011_create_workflow_logs_table.sql new file mode 100644 index 0000000..86b2450 --- /dev/null +++ b/db/migrations/011_create_workflow_logs_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS workflow_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + workflow_id INT NOT NULL, + status VARCHAR(50) NOT NULL, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (workflow_id) REFERENCES workflows(id) +); diff --git a/db/migrations/012_add_updated_at_to_tasks.sql b/db/migrations/012_add_updated_at_to_tasks.sql new file mode 100644 index 0000000..398f441 --- /dev/null +++ b/db/migrations/012_add_updated_at_to_tasks.sql @@ -0,0 +1 @@ +ALTER TABLE `tasks` ADD `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/db/migrations/013_add_delete_permissions.sql b/db/migrations/013_add_delete_permissions.sql new file mode 100644 index 0000000..0a144df --- /dev/null +++ b/db/migrations/013_add_delete_permissions.sql @@ -0,0 +1 @@ +INSERT INTO permissions (name) VALUES ('delete_candidates'), ('delete_tasks'); diff --git a/delete_candidate.php b/delete_candidate.php new file mode 100644 index 0000000..e139489 --- /dev/null +++ b/delete_candidate.php @@ -0,0 +1,20 @@ +prepare("DELETE FROM candidates WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); +} + +header('Location: dashboard.php'); +exit; diff --git a/delete_task.php b/delete_task.php new file mode 100644 index 0000000..c37ab47 --- /dev/null +++ b/delete_task.php @@ -0,0 +1,20 @@ +prepare("DELETE FROM tasks WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); +} + +header('Location: dashboard.php'); +exit; diff --git a/edit_candidate.php b/edit_candidate.php new file mode 100644 index 0000000..cf7b3c9 --- /dev/null +++ b/edit_candidate.php @@ -0,0 +1,126 @@ +prepare("SELECT * FROM candidates WHERE id = ?"); +$stmt->execute([$candidate_id]); +$candidate = $stmt->fetch(); + +if (!$candidate) { + header('Location: index.php'); + exit; +} + +// Handle form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_candidate'])) { + $name = $_POST['name'] ?? ''; + $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; + $status = $_POST['status'] ?? 'Applied'; + $notes = $_POST['notes'] ?? ''; + + if (!empty($name) && !empty($email)) { + try { + $stmt = $pdo->prepare("UPDATE candidates SET name = ?, email = ?, phone = ?, status = ?, notes = ? WHERE id = ?"); + $stmt->execute([$name, $email, $phone, $status, $notes, $candidate_id]); + header('Location: index.php'); + exit; + } catch (PDOException $e) { + error_log("Error updating candidate: " . $e->getMessage()); + } + } +} +?> + + + + + + Edit Candidate + + + + +
+ + +
+ +
+

Edit Candidate

+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+
+
+ + + + \ No newline at end of file diff --git a/edit_role.php b/edit_role.php new file mode 100644 index 0000000..340bd73 --- /dev/null +++ b/edit_role.php @@ -0,0 +1,142 @@ +prepare("SELECT * FROM roles WHERE id = ?"); +$stmt->execute([$role_id]); +$role = $stmt->fetch(); + +if (!$role) { + header('Location: roles.php'); + exit; +} + +// Define all available permissions +$available_permissions = [ + 'manage_candidates', + 'view_candidates', + 'manage_tasks', + 'view_tasks', + 'manage_workflows', + 'view_workflows', + 'manage_roles', + 'view_roles', + 'manage_users', + 'view_users' +]; + +// Fetch current permissions for the role +$stmt = $pdo->prepare("SELECT permission_name FROM role_permissions WHERE role_id = ?"); +$stmt->execute([$role_id]); +$current_permissions = $stmt->fetchAll(PDO::FETCH_COLUMN); + +// Handle form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_permissions'])) { + $selected_permissions = $_POST['permissions'] ?? []; + + try { + // Start a transaction + $pdo->beginTransaction(); + + // Delete existing permissions for the role + $delete_stmt = $pdo->prepare("DELETE FROM role_permissions WHERE role_id = ?"); + $delete_stmt->execute([$role_id]); + + // Insert new permissions + $insert_stmt = $pdo->prepare("INSERT INTO role_permissions (role_id, permission_name) VALUES (?, ?)"); + foreach ($selected_permissions as $permission) { + if (in_array($permission, $available_permissions)) { + $insert_stmt->execute([$role_id, $permission]); + } + } + + // Commit the transaction + $pdo->commit(); + + header("Location: roles.php"); + exit; + } catch (PDOException $e) { + $pdo->rollBack(); + error_log("Error updating permissions: " . $e->getMessage()); + } +} + +?> + + + + + + Edit Role + + + + +
+ + +
+ +
+

Edit Role:

+ +
+
+
+ +
+ + +
+ > + +
+ +
+ + Cancel +
+
+
+
+ + + + \ No newline at end of file diff --git a/edit_task.php b/edit_task.php new file mode 100644 index 0000000..909562f --- /dev/null +++ b/edit_task.php @@ -0,0 +1,133 @@ +prepare("SELECT * FROM tasks WHERE id = ?"); +$stmt->execute([$task_id]); +$task = $stmt->fetch(); + +if (!$task) { + header('Location: index.php'); + exit; +} + +// Fetch candidates for the dropdown +$stmt = $pdo->query("SELECT id, name FROM candidates"); +$candidates = $stmt->fetchAll(); + +// Handle form submission +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_task'])) { + $task_name = $_POST['task_name'] ?? ''; + $candidate_id = $_POST['candidate_id'] ?? null; + $due_date = $_POST['due_date'] ?? null; + $status = $_POST['status'] ?? 'To Do'; + $description = $_POST['description'] ?? ''; + + if (!empty($task_name) && !empty($candidate_id)) { + try { + $stmt = $pdo->prepare("UPDATE tasks SET task_name = ?, candidate_id = ?, due_date = ?, status = ?, description = ? WHERE id = ?"); + $stmt->execute([$task_name, $candidate_id, $due_date, $status, $description, $task_id]); + header('Location: index.php'); + exit; + } catch (PDOException $e) { + error_log("Error updating task: " . $e->getMessage()); + } + } +} +?> + + + + + + Edit Task + + + + +
+ + +
+ +
+

Edit Task

+
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+
+
+ + + + \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..64055ee 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,442 @@ prepare("DELETE FROM tasks WHERE id = ?"); + $stmt->execute([$task_id]); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error deleting task: " . $e->getMessage()); + } + } +} + +// Handle delete candidate +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_candidate']) && hasPermission('manage_candidates')) { + $candidate_id = $_POST['delete_candidate_id'] ?? null; + + if (!empty($candidate_id)) { + try { + $pdo = db(); + $stmt = $pdo->prepare("DELETE FROM candidates WHERE id = ?"); + $stmt->execute([$candidate_id]); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error deleting candidate: " . $e->getMessage()); + } + } +} + +// Handle form submission for new candidate +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_candidate']) && hasPermission('manage_candidates')) { + $name = $_POST['name'] ?? ''; + $email = $_POST['email'] ?? ''; + $phone = $_POST['phone'] ?? ''; + $status = $_POST['status'] ?? 'Applied'; + $notes = $_POST['notes'] ?? ''; + + if (!empty($name) && !empty($email)) { + try { + $pdo = db(); + $stmt = $pdo->prepare("INSERT INTO candidates (name, email, phone, status, notes) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$name, $email, $phone, $status, $notes]); + require_once 'workflow_engine.php'; + trigger_workflow('candidate_created', ['candidate.id' => $pdo->lastInsertId(), 'candidate.name' => $name, 'candidate.email' => $email]); + // Redirect to avoid form resubmission + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + // Handle error, e.g., show an error message + error_log("Error adding candidate: " . $e->getMessage()); + } + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['complete_task']) && hasPermission('manage_tasks')) { + $task_id = $_POST['task_id'] ?? null; + + if (!empty($task_id)) { + try { + $pdo = db(); + $stmt = $pdo->prepare("UPDATE tasks SET status = 'Done' WHERE id = ?"); + $stmt->execute([$task_id]); + + // Fetch task details to pass to the workflow + $stmt = $pdo->prepare("SELECT * FROM tasks WHERE id = ?"); + $stmt->execute([$task_id]); + $task = $stmt->fetch(); + + require_once 'workflow_engine.php'; + trigger_workflow('task_completed', ['task.id' => $task['id'], 'task.name' => $task['task_name'], 'task.status' => $task['status']]); + + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error completing task: " . $e->getMessage()); + } + } +} + +// Handle form submission for new task +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_task']) && hasPermission('manage_tasks')) { + $task_name = $_POST['task_name'] ?? ''; + $candidate_id = $_POST['candidate_id'] ?? null; + $due_date = $_POST['due_date'] ?? null; + $status = $_POST['status'] ?? 'To Do'; + $description = $_POST['description'] ?? ''; + + if (!empty($task_name) && !empty($candidate_id)) { + try { + $pdo = db(); + $stmt = $pdo->prepare("INSERT INTO tasks (task_name, candidate_id, due_date, status, description) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$task_name, $candidate_id, $due_date, $status, $description]); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error adding task: " . $e->getMessage()); + } + } +} + +// Fetch tasks from the database +try { + $pdo = db(); + $stmt = $pdo->query("SELECT tasks.*, candidates.name as candidate_name FROM tasks JOIN candidates ON tasks.candidate_id = candidates.id ORDER BY created_at DESC"); + $tasks = $stmt->fetchAll(); +} catch (PDOException $e) { + error_log("Error fetching tasks: " . $e->getMessage()); + $tasks = []; // Ensure $tasks is an array +} + +// Fetch candidates from the database +try { + $pdo = db(); + $stmt = $pdo->query("SELECT * FROM candidates ORDER BY created_at DESC"); + $candidates = $stmt->fetchAll(); +} catch (PDOException $e) { + // Handle error, e.g., show an error message + error_log("Error fetching candidates: " . $e->getMessage()); + $candidates = []; // Ensure $candidates is an array +} + +function getStatusClass($status) { + switch ($status) { + case 'Applied': return 'status-new'; + case 'Interviewing': return 'status-interview'; + case 'Hired': return 'status-hired'; + case 'Rejected': return 'status-rejected'; + case 'Offered': return 'status-offered'; + case 'To Do': return 'status-todo'; + case 'In Progress': return 'status-in-progress'; + case 'Done': return 'status-done'; + default: return ''; + } +} ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + + + FinMox Flow + + + + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+ + +
+ +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + +
NamePhoneStatusActions
+
+
+
+
+
+
+
+ + + + + Edit +
+ + +
+
+
+
+
+ + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Task NameAssigned ToDue DateStatusActions
+ + + + + Edit +
+ + +
+
+ + +
+
+
+
+
+ +
+
+ + +
-
- Page updated: (UTC) -
+ + + + + + + + + +
+
+ AI Assistant + +
+
+ +
+
+ + +
+
+ + - + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..62e18dc --- /dev/null +++ b/login.php @@ -0,0 +1,68 @@ + + + + + + + + Login - FinMox + + + +
+
+
+
+
+

Login

+
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..2d3709a --- /dev/null +++ b/logout.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/register.php b/register.php new file mode 100644 index 0000000..9e66efe --- /dev/null +++ b/register.php @@ -0,0 +1,63 @@ + + + + + + + + Register - FinMox + + + +
+
+
+
+
+

Register

+
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/roles.php b/roles.php new file mode 100644 index 0000000..20810ec --- /dev/null +++ b/roles.php @@ -0,0 +1,128 @@ +prepare("INSERT INTO roles (name) VALUES (?)"); + $stmt->execute([$role_name]); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error adding role: " . $e->getMessage()); + } + } +} + + +// Fetch roles +$stmt = $pdo->query("SELECT * FROM roles ORDER BY name"); +$roles = $stmt->fetchAll(); + +?> + + + + + + Role Management + + + + +
+ + +
+ +
+
+

Role Management

+ +
+ +
+
+ + + + + + + + + + + + + + + +
Role NameActions
+ Edit +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/workflow_actions.php b/workflow_actions.php new file mode 100644 index 0000000..fa2ebc9 --- /dev/null +++ b/workflow_actions.php @@ -0,0 +1,230 @@ +prepare("SELECT * FROM workflows WHERE id = ?"); +$stmt->execute([$workflow_id]); +$workflow = $stmt->fetch(); + +if (!$workflow) { + header('Location: workflows.php'); + exit; +} + +// Handle form submission for new action +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_action']) && hasPermission('manage_workflows')) { + $action_type = $_POST['action_type'] ?? ''; + $config = []; + + if ($action_type === 'send_email') { + $config['to'] = $_POST['to'] ?? ''; + $config['subject'] = $_POST['subject'] ?? ''; + $config['message'] = $_POST['message'] ?? ''; + } elseif ($action_type === 'create_task') { + $config['task_name'] = $_POST['task_name'] ?? ''; + $config['assign_to'] = $_POST['assign_to'] ?? ''; + } elseif ($action_type === 'send_slack_notification') { + $config['webhook_url'] = $_POST['webhook_url'] ?? ''; + $config['message'] = $_POST['slack_message'] ?? ''; + } elseif ($action_type === 'update_candidate_status') { + $config['new_status'] = $_POST['new_status'] ?? ''; + } + + if (!empty($action_type)) { + try { + $stmt = $pdo->prepare("INSERT INTO workflow_actions (workflow_id, action_type, config) VALUES (?, ?, ?)"); + $stmt->execute([$workflow_id, $action_type, json_encode($config)]); + header("Location: " . $_SERVER['REQUEST_URI']); + exit; + } catch (PDOException $e) { + error_log("Error adding action: " . $e->getMessage()); + } + } +} + +// Fetch actions for the workflow +$stmt = $pdo->prepare("SELECT * FROM workflow_actions WHERE workflow_id = ? ORDER BY created_at DESC"); +$stmt->execute([$workflow_id]); +$actions = $stmt->fetchAll(); + +?> + + + + + + Workflow Actions + + + + +
+ + +
+ +
+
+

Actions for ""

+ + + +
+ +
+
+ + + + + + + + + + + + + + + +
Action TypeConfiguration
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/workflow_engine.php b/workflow_engine.php new file mode 100644 index 0000000..9af7ac2 --- /dev/null +++ b/workflow_engine.php @@ -0,0 +1,65 @@ +prepare("SELECT * FROM workflows WHERE `trigger` = ?"); + $stmt->execute([$trigger]); + $workflows = $stmt->fetchAll(); + + foreach ($workflows as $workflow) { + // 2. For each workflow, fetch its actions + $stmt = $pdo->prepare("SELECT * FROM workflow_actions WHERE workflow_id = ?"); + $stmt->execute([$workflow['id']]); + $actions = $stmt->fetchAll(); + + foreach ($actions as $action) { + // 3. Execute each action + execute_action($action, $data); + } + } +} + +function execute_action($action, $data) { + $config = json_decode($action['config'], true); + + // Replace placeholders in the config with data + array_walk_recursive($config, function(&$value) use ($data) { + if (is_string($value)) { + foreach ($data as $key => $val) { + if (is_string($val) || is_numeric($val)) { + $value = str_replace('{{candidate.' . $key . '}}', $val, $value); + } + } + } + }); + + switch ($action['action_type']) { + case 'send_email': + MailService::sendMail($config['to'], $config['subject'], $config['message']); + break; + case 'create_task': + $pdo = db(); + $stmt = $pdo->prepare("INSERT INTO tasks (name, assignee_id) VALUES (?, ?)"); + $stmt->execute([$config['task_name'], $config['assign_to']]); + break; + case 'send_slack_notification': + $ch = curl_init($config['webhook_url']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['text' => $config['message']])); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_exec($ch); + curl_close($ch); + break; + case 'update_candidate_status': + $pdo = db(); + $stmt = $pdo->prepare("UPDATE candidates SET status = ? WHERE id = ?"); + $stmt->execute([$config['new_status'], $data['candidate.id']]); + break; + } +} +?> \ No newline at end of file diff --git a/workflows.php b/workflows.php new file mode 100644 index 0000000..c3ab1d4 --- /dev/null +++ b/workflows.php @@ -0,0 +1,144 @@ +prepare("INSERT INTO workflows (name, `trigger`) VALUES (?, ?)"); + $stmt->execute([$name, $trigger]); + header("Location: " . $_SERVER['PHP_SELF']); + exit; + } catch (PDOException $e) { + error_log("Error adding workflow: " . $e->getMessage()); + } + } +} + +// Fetch workflows from the database +$stmt = $pdo->query("SELECT * FROM workflows ORDER BY created_at DESC"); +$workflows = $stmt->fetchAll(); + +?> + + + + + + Workflows + + + + +
+ + +
+ +
+
+

Workflows

+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + +
NameTriggerActions
+ + Manage Actions + +
+
+
+
+ + + + + + + \ No newline at end of file