Nov 19,2025

This commit is contained in:
Flatlogic Bot 2025-11-20 02:21:39 +00:00
parent 577ca93381
commit c13312a14c
42 changed files with 3239 additions and 149 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

311
ai/LocalAIApi.php Normal file
View File

@ -0,0 +1,311 @@
<?php
// LocalAIApi — proxy client for the Responses API.
// Usage:
// require_once __DIR__ . '/ai/LocalAIApi.php';
// $response = LocalAIApi::createResponse([
// 'input' => [
// ['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<string,mixed>|null */
private static ?array $configCache = null;
/**
* Signature compatible with the OpenAI Responses API.
*
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
* @param array<string,mixed> $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<string,mixed> $params
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
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<string,mixed> $payload JSON payload.
* @param array<string,mixed> $options Additional request options.
* @return array<string,mixed>
*/
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<string,mixed> $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<string,mixed> $response
* @return array<string,mixed>|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<string,mixed>
*/
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');
}

52
ai/config.php Normal file
View File

@ -0,0 +1,52 @@
<?php
// OpenAI proxy configuration (workspace scope).
// Reads values from environment variables or executor/.env.
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
if (
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
($projectId === false || $projectId === null || $projectId === '')
) {
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$key, $value] = array_map('trim', explode('=', $line, 2));
if ($key === '') {
continue;
}
$value = trim($value, "\"' ");
if (getenv($key) === false || getenv($key) === '') {
putenv("{$key}={$value}");
}
}
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
}
}
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
$projectId = ($projectId === false) ? null : $projectId;
$baseUrl = 'https://flatlogic.com';
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
return [
'base_url' => $baseUrl,
'responses_path' => $responsesPath,
'project_id' => $projectId,
'project_uuid' => $projectUuid,
'project_header' => 'project-uuid',
'default_model' => 'gpt-5',
'timeout' => 30,
'verify_tls' => true,
];

67
api/chat.php Normal file
View File

@ -0,0 +1,67 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php';
require_once __DIR__ . '/../db/config.php';
// Get the user's message from the POST request
$input = json_decode(file_get_contents('php://input'), true);
$userMessage = $input['message'] ?? '';
if (empty($userMessage)) {
echo json_encode(['error' => '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 = <<<PROMPT
You are an expert HR assistant for a company called FinMox.
Your role is to answer questions about candidates and tasks based on the data provided below.
Be concise and professional. If you don't have the answer, say so.
Here is the current data from the system:
{$context}
PROMPT;
$messages = [
['role' => '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.']);
}
?>

271
assets/css/custom.css Normal file
View File

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

74
assets/js/main.js Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

86
auth.php Normal file
View File

@ -0,0 +1,86 @@
<?php
session_start();
require_once 'db/config.php';
function register_user($username, $password, $email) {
$pdo = db();
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// Get the default role ID (e.g., the role with the name 'Admin')
$stmt = $pdo->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;
}
?>

164
chat.php Normal file
View File

@ -0,0 +1,164 @@
<?php
require_once 'auth.php';
if (!is_logged_in()) {
header('Location: login.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO & Meta Tags -->
<title>AI Chat - FinMox</title>
<meta name="description" content="FinMox Flow - a multi-tenant SaaS platform for HR and Operations teams. Built with Flatlogic Generator.">
<meta name="keywords" content="finmox, hr, operations, saas, candidate tracking, onboarding, automations, ai copilot, flatlogic">
<!-- Social Media Meta Tags -->
<meta property="og:title" content="FinMox Flow">
<meta property="og:description" content="A multi-tenant SaaS platform for HR and Operations teams.">
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<!-- Stylesheets -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<style>
#chat-container {
max-width: 800px;
margin: 2rem auto;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
height: 70vh;
display: flex;
flex-direction: column;
}
#chat-messages {
flex-grow: 1;
overflow-y: auto;
padding: 1rem;
border-bottom: 1px solid #ddd;
}
.message {
margin-bottom: 1rem;
}
.message .sender {
font-weight: bold;
}
.message .content {
padding: 0.5rem 1rem;
border-radius: 8px;
display: inline-block;
}
.user-message .content {
background-color: #e9f5ff;
}
.ai-message .content {
background-color: #f8f9fa;
}
</style>
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="main-content">
<div id="chat-container">
<div id="chat-messages"></div>
<div class="input-group mt-3">
<input type="text" id="user-input" class="form-control" placeholder="Ask the AI Assistant...">
<button id="send-btn" class="btn btn-primary">Send</button>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const chatMessages = document.getElementById('chat-messages');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
function addMessage(sender, content) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', sender + '-message');
const senderDiv = document.createElement('div');
senderDiv.classList.add('sender');
senderDiv.textContent = sender === 'user' ? 'You' : 'AI Assistant';
const contentDiv = document.createElement('div');
contentDiv.classList.add('content');
contentDiv.textContent = content;
messageDiv.appendChild(senderDiv);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
async function sendMessage() {
const message = userInput.value.trim();
if (!message) return;
addMessage('user', message);
userInput.value = '';
sendBtn.disabled = true;
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const aiReply = data.reply || data.error || 'No reply from AI.';
addMessage('ai', aiReply);
} catch (error) {
console.error('Error:', error);
addMessage('ai', 'Sorry, something went wrong. Please check the console.');
} finally {
sendBtn.disabled = false;
}
}
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>

460
dashboard.php Normal file
View File

@ -0,0 +1,460 @@
<?php
require_once 'auth.php';
// Check if user is logged in
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
// Fetch data for analytics
$pdo = db();
// Candidate stats
$stmt = $pdo->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);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analytics Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid mt-4">
<h2 class="mb-4">Analytics Dashboard</h2>
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab" aria-controls="overview" aria-selected="true">Overview</button>
</li>
<?php if (hasPermission('view_candidates')) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link" id="candidates-tab" data-bs-toggle="tab" data-bs-target="#candidates" type="button" role="tab" aria-controls="candidates" aria-selected="false">Candidates</button>
</li>
<?php } ?>
<?php if (hasPermission('view_tasks')) { ?>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tasks-tab" data-bs-toggle="tab" data-bs-target="#tasks" type="button" role="tab" aria-controls="tasks" aria-selected="false">Tasks</button>
</li>
<?php } ?>
</ul>
<!-- Tab content -->
<div class="tab-content" id="myTabContent">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview" role="tabpanel" aria-labelledby="overview-tab">
<!-- Key Metrics -->
<div class="row mb-4 mt-4">
<?php if (hasPermission('view_candidates')) { ?>
<div class="col-md-4">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title">Total Candidates</h5>
<p class="card-text fs-4" id="total-candidates"><?php echo $total_candidates; ?></p>
</div>
</div>
</div>
<?php } ?>
<?php if (hasPermission('view_tasks')) { ?>
<div class="col-md-4">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title">Total Tasks</h5>
<p class="card-text fs-4" id="total-tasks"><?php echo $total_tasks; ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title">Completed Tasks</h5>
<p class="card-text fs-4" id="completed-tasks"><?php echo $completed_tasks; ?></p>
</div>
</div>
</div>
<?php } ?>
</div>
<!-- Charts -->
<div class="row">
<?php if (hasPermission('view_candidates')) { ?>
<div class="col-md-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Candidates by Status</h5>
<canvas id="candidates-by-status-chart"></canvas>
</div>
</div>
</div>
<?php } ?>
<?php if (hasPermission('view_tasks')) { ?>
<div class="col-md-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Tasks by Status</h5>
<canvas id="tasks-by-status-chart"></canvas>
</div>
</div>
</div>
<?php } ?>
<?php if (hasPermission('view_candidates')) { ?>
<div class="col-md-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Candidates per Day</h5>
<canvas id="candidates-per-day-chart"></canvas>
</div>
</div>
</div>
<?php } ?>
</div>
<div class="row">
<?php if (hasPermission('view_tasks')) { ?>
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Tasks Created per Day</h5>
<canvas id="tasks-per-day-chart"></canvas>
</div>
</div>
</div>
<?php } ?>
<?php if (hasPermission('view_tasks')) { ?>
<div class="col-md-6 mb-4">
<div class="card shadow-sm h-100">
<div class="card-body">
<h5 class="card-title">Tasks Completed per Day</h5>
<canvas id="tasks-completed-per-day-chart"></canvas>
</div>
</div>
</div>
<?php } ?>
</div>
</div>
<!-- Candidates Tab -->
<?php if (hasPermission('view_candidates')) { ?>
<div class="tab-pane fade" id="candidates" role="tabpanel" aria-labelledby="candidates-tab">
<div class="row mt-4">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title">Recent Candidates</h5>
<a href="edit_candidate.php" class="btn btn-primary">Add Candidate</a>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($candidates as $candidate) { ?>
<tr>
<td><?php echo htmlspecialchars($candidate['name']); ?></td>
<td><?php echo htmlspecialchars($candidate['email']); ?></td>
<td><span class="badge bg-secondary"><?php echo htmlspecialchars($candidate['status']); ?></span></td>
<td>
<a href="edit_candidate.php?id=<?php echo $candidate['id']; ?>" class="btn btn-sm btn-outline-primary">Edit</a>
<a href="delete_candidate.php?id=<?php echo $candidate['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')">Delete</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<nav>
<ul class="pagination">
<?php for ($i = 1; $i <= $total_candidate_pages; $i++) { ?>
<li class="page-item <?php if ($i == $page) echo 'active'; ?>"><a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a></li>
<?php } ?>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<?php } ?>
<!-- Tasks Tab -->
<?php if (hasPermission('view_tasks')) { ?>
<div class="tab-pane fade" id="tasks" role="tabpanel" aria-labelledby="tasks-tab">
<div class="row mt-4">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title">Recent Tasks</h5>
<a href="edit_task.php" class="btn btn-primary">Add Task</a>
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Assigned To</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($tasks as $task) { ?>
<tr>
<td><?php echo htmlspecialchars($task['title']); ?></td>
<td><span class="badge bg-info"><?php echo htmlspecialchars($task['status']); ?></span></td>
<td><?php echo htmlspecialchars($task['assigned_to'] ?? 'N/A'); ?></td>
<td>
<a href="edit_task.php?id=<?php echo $task['id']; ?>" class="btn btn-sm btn-outline-primary">Edit</a>
<a href="delete_task.php?id=<?php echo $task['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')">Delete</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<nav>
<ul class="pagination">
<?php for ($i = 1; $i <= $total_task_pages; $i++) { ?>
<li class="page-item <?php if ($i == $page) echo 'active'; ?>"><a class="page-link" href="?page=<?php echo $i; ?>"><?php echo $i; ?></a></li>
<?php } ?>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<?php } ?>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Candidates by Status Chart
const candidateCtx = document.getElementById('candidates-by-status-chart').getContext('2d');
new Chart(candidateCtx, {
type: 'doughnut',
data: {
labels: <?php echo $candidate_status_labels; ?>,
datasets: [{
label: 'Candidates',
data: <?php echo $candidate_status_data; ?>,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)',
'rgba(255, 159, 64, 0.7)'
],
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
// Tasks by Status Chart
const taskCtx = document.getElementById('tasks-by-status-chart').getContext('2d');
new Chart(taskCtx, {
type: 'bar',
data: {
labels: <?php echo $task_status_labels; ?>,
datasets: [{
label: 'Tasks',
data: <?php echo $task_status_data; ?>,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
],
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Candidates per Day Chart
const candidatesPerDayCtx = document.getElementById('candidates-per-day-chart').getContext('2d');
new Chart(candidatesPerDayCtx, {
type: 'line',
data: {
labels: <?php echo $candidates_per_day_labels; ?>,
datasets: [{
label: 'Candidates',
data: <?php echo $candidates_per_day_data; ?>,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 2,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Tasks per Day Chart
const tasksPerDayCtx = document.getElementById('tasks-per-day-chart').getContext('2d');
new Chart(tasksPerDayCtx, {
type: 'line',
data: {
labels: <?php echo $tasks_per_day_labels; ?>,
datasets: [{
label: 'Tasks',
data: <?php echo $tasks_per_day_data; ?>,
backgroundColor: 'rgba(239, 68, 68, 0.2)',
borderColor: 'rgba(239, 68, 68, 1)',
borderWidth: 2,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Tasks Completed per Day Chart
const tasksCompletedPerDayCtx = document.getElementById('tasks-completed-per-day-chart').getContext('2d');
new Chart(tasksCompletedPerDayCtx, {
type: 'line',
data: {
labels: <?php echo $tasks_completed_per_day_labels; ?>,
datasets: [{
label: 'Tasks Completed',
data: <?php echo $tasks_completed_per_day_data; ?>,
backgroundColor: 'rgba(16, 185, 129, 0.2)',
borderColor: 'rgba(16, 185, 129, 1)',
borderWidth: 2,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
});
</script>
</body>
</html>

View File

@ -6,12 +6,38 @@ define('DB_USER', 'app_31009');
define('DB_PASS', '2c66b530-2a65-423a-a106-6760b49ad1a2'); define('DB_PASS', '2c66b530-2a65-423a-a106-6760b49ad1a2');
function db() { function db() {
static $pdo; static $pdo;
if (!$pdo) { if ($pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ return $pdo;
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, }
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]); try {
} $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
return $pdo; 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;
} }

21
db/migrate.php Normal file
View File

@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/config.php';
function run_migrations() {
$pdo = db();
$migrationsDir = __DIR__ . '/migrations';
$migrationFiles = glob($migrationsDir . '/*.sql');
foreach ($migrationFiles as $file) {
try {
$sql = file_get_contents($file);
$pdo->exec($sql);
echo "Migration successful: " . basename($file) . "\n";
} catch (PDOException $e) {
echo "Migration failed for " . basename($file) . ": " . $e->getMessage() . "\n";
}
}
}
run_migrations();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN role_id INT, ADD FOREIGN KEY (role_id) REFERENCES roles(id);

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
ALTER TABLE `tasks` ADD `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1 @@
INSERT INTO permissions (name) VALUES ('delete_candidates'), ('delete_tasks');

20
delete_candidate.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once 'auth.php';
if (!is_logged_in() || !hasPermission('delete_candidates')) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
if (isset($_GET['id'])) {
$id = $_GET['id'];
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM candidates WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
}
header('Location: dashboard.php');
exit;

20
delete_task.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once 'auth.php';
if (!is_logged_in() || !hasPermission('delete_tasks')) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
if (isset($_GET['id'])) {
$id = $_GET['id'];
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM tasks WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
}
header('Location: dashboard.php');
exit;

126
edit_candidate.php Normal file
View File

@ -0,0 +1,126 @@
<?php
require_once 'auth.php';
require_once 'db/config.php';
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('manage_candidates')) {
header('Location: index.php');
exit;
}
$pdo = db();
$candidate_id = $_GET['id'] ?? null;
if (!$candidate_id) {
header('Location: index.php');
exit;
}
// Fetch candidate data
$stmt = $pdo->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());
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Candidate</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<h2 class="mb-4">Edit Candidate</h2>
<div class="card">
<div class="card-body">
<form method="POST" action="edit_candidate.php?id=<?php echo $candidate_id; ?>">
<input type="hidden" name="update_candidate" value="1">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($candidate['name']); ?>" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" value="<?php echo htmlspecialchars($candidate['email']); ?>" required>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone</label>
<input type="text" class="form-control" id="phone" name="phone" value="<?php echo htmlspecialchars($candidate['phone']); ?>">
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="Applied" <?php if ($candidate['status'] === 'Applied') echo 'selected'; ?>>Applied</option>
<option value="Interviewing" <?php if ($candidate['status'] === 'Interviewing') echo 'selected'; ?>>Interviewing</option>
<option value="Offered" <?php if ($candidate['status'] === 'Offered') echo 'selected'; ?>>Offered</option>
<option value="Hired" <?php if ($candidate['status'] === 'Hired') echo 'selected'; ?>>Hired</option>
<option value="Rejected" <?php if ($candidate['status'] === 'Rejected') echo 'selected'; ?>>Rejected</option>
</select>
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"><?php echo htmlspecialchars($candidate['notes']); ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Update Candidate</button>
<a href="index.php" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

142
edit_role.php Normal file
View File

@ -0,0 +1,142 @@
<?php
require_once 'auth.php';
require_once 'db/config.php';
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('manage_roles')) {
header('Location: index.php');
exit;
}
if (!isset($_GET['role_id'])) {
header('Location: roles.php');
exit;
}
$role_id = $_GET['role_id'];
$pdo = db();
// Fetch role details
$stmt = $pdo->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());
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Role</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<h2 class="mb-4">Edit Role: <?php echo htmlspecialchars($role['name']); ?></h2>
<div class="card">
<div class="card-body">
<form method="POST">
<input type="hidden" name="save_permissions" value="1">
<div class="mb-3">
<label class="form-label">Permissions</label>
<?php foreach ($available_permissions as $permission): ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="<?php echo $permission; ?>" id="perm_<?php echo $permission; ?>" <?php echo in_array($permission, $current_permissions) ? 'checked' : ''; ?>>
<label class="form-check-label" for="perm_<?php echo $permission; ?>">
<?php echo ucfirst(str_replace('_', ' ', $permission)); ?>
</label>
</div>
<?php endforeach; ?>
</div>
<button type="submit" class="btn btn-primary">Save Permissions</button>
<a href="roles.php" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

133
edit_task.php Normal file
View File

@ -0,0 +1,133 @@
<?php
require_once 'auth.php';
require_once 'db/config.php';
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('manage_tasks')) {
header('Location: index.php');
exit;
}
$pdo = db();
$task_id = $_GET['id'] ?? null;
if (!$task_id) {
header('Location: index.php');
exit;
}
// Fetch task data
$stmt = $pdo->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());
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Task</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<h2 class="mb-4">Edit Task</h2>
<div class="card">
<div class="card-body">
<form method="POST" action="edit_task.php?id=<?php echo $task_id; ?>">
<input type="hidden" name="update_task" value="1">
<div class="mb-3">
<label for="task_name" class="form-label">Task Name</label>
<input type="text" class="form-control" id="task_name" name="task_name" value="<?php echo htmlspecialchars($task['task_name']); ?>" required>
</div>
<div class="mb-3">
<label for="candidate_id" class="form-label">Assign to Candidate</label>
<select class="form-select" id="candidate_id" name="candidate_id" required>
<option value="" disabled>Select a candidate</option>
<?php foreach ($candidates as $candidate): ?>
<option value="<?php echo $candidate['id']; ?>" <?php if ($task['candidate_id'] == $candidate['id']) echo 'selected'; ?>><?php echo htmlspecialchars($candidate['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="date" class="form-control" id="due_date" name="due_date" value="<?php echo htmlspecialchars($task['due_date']); ?>">
</div>
<div class="mb-3">
<label for="task_status" class="form-label">Status</label>
<select class="form-select" id="task_status" name="status">
<option value="To Do" <?php if ($task['status'] === 'To Do') echo 'selected'; ?>>To Do</option>
<option value="In Progress" <?php if ($task['status'] === 'In Progress') echo 'selected'; ?>>In Progress</option>
<option value="Done" <?php if ($task['status'] === 'Done') echo 'selected'; ?>>Done</option>
</select>
</div>
<div class="mb-3">
<label for="task_description" class="form-label">Description</label>
<textarea class="form-control" id="task_description" name="description" rows="3"><?php echo htmlspecialchars($task['description']); ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Update Task</button>
<a href="index.php" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

574
index.php
View File

@ -1,150 +1,442 @@
<?php <?php
declare(strict_types=1); require_once 'auth.php';
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; // Check if user is logged in
$now = date('Y-m-d H:i:s'); if (!is_logged_in()) {
header('Location: login.php');
exit;
}
require_once __DIR__ . '/db/config.php';
// Handle delete task
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_task']) && hasPermission('manage_tasks')) {
$task_id = $_POST['delete_task_id'] ?? null;
if (!empty($task_id)) {
try {
$pdo = db();
$stmt = $pdo->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 '';
}
}
?> ?>
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title>
<?php <!-- SEO & Meta Tags -->
// Read project preview data from environment <title>FinMox Flow</title>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <meta name="description" content="FinMox Flow - a multi-tenant SaaS platform for HR and Operations teams. Built with Flatlogic Generator.">
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <meta name="keywords" content="finmox, hr, operations, saas, candidate tracking, onboarding, automations, ai copilot, flatlogic">
?>
<?php if ($projectDescription): ?> <!-- Social Media Meta Tags -->
<!-- Meta description --> <meta property="og:title" content="FinMox Flow">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <meta property="og:description" content="A multi-tenant SaaS platform for HR and Operations teams.">
<!-- Open Graph meta tags --> <meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <meta name="twitter:card" content="summary_large_image">
<!-- Twitter meta tags --> <meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?> <!-- Stylesheets -->
<?php if ($projectImageUrl): ?> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Open Graph image --> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main>
<div class="card"> <header class="header d-flex justify-content-between align-items-center">
<h1>Analyzing your requirements and generating your website…</h1> <div class="logo">FinMox<span class="dot">.</span></div>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <nav class="d-flex align-items-center">
<span class="sr-only">Loading…</span> <a href="index.php" class="btn btn-outline-primary me-2">Home</a>
</div> <a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> <a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<p class="hint">This page will update automatically as the plan is implemented.</p> <a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p> <div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="main-content">
<div class="container-fluid">
<?php if (hasPermission('view_candidates')): ?>
<div class="page-header">
<h1 class="page-title">Candidates</h1>
<?php if (hasPermission('manage_candidates')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCandidateModal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
</svg>
Add Candidate
</button>
<?php endif; ?>
</div>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th class="ps-4">Name</th>
<th>Phone</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($candidates as $candidate): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div>
<div class="candidate-name"><?php echo htmlspecialchars($candidate['name']); ?></div>
<div class="candidate-email"><?php echo htmlspecialchars($candidate['email']); ?></div>
</div>
</div>
</td>
<td><?php echo htmlspecialchars($candidate['phone'] ?? 'N/A'); ?></td>
<td>
<span class="status-badge <?php echo getStatusClass($candidate['status']); ?>">
<?php echo htmlspecialchars($candidate['status']); ?>
</span>
</td>
<td class="text-end pe-4">
<a href="edit_candidate.php?id=<?php echo $candidate['id']; ?>" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>" style="display: inline;">
<input type="hidden" name="delete_candidate_id" value="<?php echo $candidate['id']; ?>">
<button type="submit" name="delete_candidate" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this candidate?');">Delete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<?php if (hasPermission('view_tasks')): ?>
<div class="page-header mt-5">
<h1 class="page-title">Tasks</h1>
<?php if (hasPermission('manage_tasks')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTaskModal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
</svg>
Add Task
</button>
<?php endif; ?>
</div>
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th class="ps-4">Task Name</th>
<th>Assigned To</th>
<th>Due Date</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($tasks as $task): ?>
<tr>
<td class="ps-4"><?php echo htmlspecialchars($task['task_name']); ?></td>
<td><?php echo htmlspecialchars($task['candidate_name'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($task['due_date'] ?? 'N/A'); ?></td>
<td>
<span class="status-badge <?php echo getStatusClass($task['status']); ?>">
<?php echo htmlspecialchars($task['status']); ?>
</span>
</td>
<td class="text-end pe-4">
<a href="edit_task.php?id=<?php echo $task['id']; ?>" class="btn btn-sm btn-outline-primary">Edit</a>
<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>" style="display: inline;">
<input type="hidden" name="delete_task_id" value="<?php echo $task['id']; ?>">
<button type="submit" name="delete_task" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this task?');">Delete</button>
</form>
<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>" style="display: inline;">
<input type="hidden" name="task_id" value="<?php echo $task['id']; ?>">
<button type="submit" name="complete_task" class="btn btn-sm btn-success">Complete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</div>
</main>
<!-- Add Candidate Modal -->
<div class="modal fade" id="addCandidateModal" tabindex="-1" aria-labelledby="addCandidateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addCandidateModalLabel">Add New Candidate</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>">
<input type="hidden" name="add_candidate" value="1">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone</label>
<input type="text" class="form-control" id="phone" name="phone">
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="Applied" selected>Applied</option>
<option value="Interviewing">Interviewing</option>
<option value="Offered">Offered</option>
<option value="Hired">Hired</option>
<option value="Rejected">Rejected</option>
</select>
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Candidate</button>
</div>
</form>
</div>
</div>
</div> </div>
</main> </div>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <!-- Add Task Modal -->
</footer> <div class="modal fade" id="addTaskModal" tabindex="-1" aria-labelledby="addTaskModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTaskModalLabel">Add New Task</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>">
<input type="hidden" name="add_task" value="1">
<div class="mb-3">
<label for="task_name" class="form-label">Task Name</label>
<input type="text" class="form-control" id="task_name" name="task_name" required>
</div>
<div class="mb-3">
<label for="candidate_id" class="form-label">Assign to Candidate</label>
<select class="form-select" id="candidate_id" name="candidate_id" required>
<option value="" disabled selected>Select a candidate</option>
<?php foreach ($candidates as $candidate): ?>
<option value="<?php echo $candidate['id']; ?>"><?php echo htmlspecialchars($candidate['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="date" class="form-control" id="due_date" name="due_date">
</div>
<div class="mb-3">
<label for="task_status" class="form-label">Status</label>
<select class="form-select" id="task_status" name="status">
<option value="To Do" selected>To Do</option>
<option value="In Progress">In Progress</option>
<option value="Done">Done</option>
</select>
</div>
<div class="mb-3">
<label for="task_description" class="form-label">Description</label>
<textarea class="form-control" id="task_description" name="description" rows="3"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Task</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<!-- AI Chat Interface -->
<div id="chat-container" class="chat-container">
<div id="chat-header" class="chat-header">
<span>AI Assistant</span>
<button id="close-chat" class="btn-close btn-sm" aria-label="Close"></button>
</div>
<div id="chat-body" class="chat-body">
<!-- Chat messages will be appended here -->
</div>
<div id="chat-input-container" class="chat-input-container">
<input type="text" id="chat-input" class="form-control" placeholder="Ask a question...">
<button id="send-chat" class="btn btn-primary">Send</button>
</div>
</div>
<button id="chat-toggle" class="chat-toggle-button">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-chat-dots" viewBox="0 0 16 16">
<path d="M5 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
<path d="M2.165 15.803l-1.57-1.57a1 1 0 0 1 0-1.414L2.165 10.5l1.57 1.57-1.57 1.57zm1.57-1.57l-1.57-1.57a1 1 0 0 1 0-1.414l1.57-1.57 1.57 1.57-1.57 1.57zM12.235 2.165l1.57 1.57a1 1 0 0 1 0 1.414l-1.57 1.57-1.57-1.57 1.57-1.57z"/>
<path d="M15.657 2.343a1 1 0 0 1 0 1.414l-1.57 1.57-1.57-1.57 1.57-1.57a1 1 0 0 1 1.414 0z"/>
<path d="M.5 1a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H2.707l-2.147 2.146a.5.5 0 0 0 .708.708L3.293 11H15a2 2 0 0 0 2-2V1a2 2 0 0 0-2-2H1.5A1.5 1.5 0 0 0 0 1.5v12.793a.5.5 0 0 0 .854.353L.5 1z"/>
</svg>
</button>
</body> </body>
</html> </html>

68
login.php Normal file
View File

@ -0,0 +1,68 @@
<?php
require_once 'auth.php';
if (is_logged_in()) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'Please fill in all fields.';
} else {
if (login_user($username, $password)) {
header('Location: index.php');
exit;
} else {
$error = 'Invalid username or password.';
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - FinMox</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Login</h3>
</div>
<div class="card-body">
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form action="login.php" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
<div class="card-footer text-center">
<p>Don't have an account? <a href="register.php">Register here</a>.</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

7
logout.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require_once 'auth.php';
logout_user();
header('Location: login.php');
exit;
?>

63
register.php Normal file
View File

@ -0,0 +1,63 @@
<?php
require_once 'auth.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error = 'Please fill in all fields.';
} else {
if (register_user($username, $password)) {
header('Location: login.php');
exit;
} else {
$error = 'Username already exists.';
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - FinMox</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Register</h3>
</div>
<div class="card-body">
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form action="register.php" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
<div class="card-footer text-center">
<p>Already have an account? <a href="login.php">Login here</a>.</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

128
roles.php Normal file
View File

@ -0,0 +1,128 @@
<?php
require_once 'auth.php';
require_once 'db/config.php';
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('manage_roles')) {
// Redirect to a generic "access denied" page or the home page
header('Location: index.php');
exit;
}
$pdo = db();
// Handle form submission for new role
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_role'])) {
$role_name = $_POST['role_name'] ?? '';
if (!empty($role_name)) {
try {
$stmt = $pdo->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();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Role Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Role Management</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addRoleModal">Add Role</button>
</div>
<div class="card">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Role Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($roles as $role): ?>
<tr>
<td><?php echo htmlspecialchars($role['name']); ?></td>
<td>
<a href="edit_role.php?role_id=<?php echo $role['id']; ?>" class="btn btn-sm btn-outline-primary">Edit</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</main>
<!-- Add Role Modal -->
<div class="modal fade" id="addRoleModal" tabindex="-1" aria-labelledby="addRoleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addRoleModalLabel">Add New Role</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST">
<input type="hidden" name="add_role" value="1">
<div class="mb-3">
<label for="role_name" class="form-label">Role Name</label>
<input type="text" class="form-control" id="role_name" name="role_name" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Role</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

230
workflow_actions.php Normal file
View File

@ -0,0 +1,230 @@
<?php
require_once 'auth.php';
// Check if user is logged in
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('view_workflows')) {
header('Location: index.php');
exit;
}
require_once 'db/config.php';
if (!isset($_GET['workflow_id'])) {
header('Location: workflows.php');
exit;
}
$workflow_id = $_GET['workflow_id'];
$pdo = db();
// Fetch workflow details
$stmt = $pdo->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();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflow Actions</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Actions for "<?php echo htmlspecialchars($workflow['name']); ?>"</h2>
<?php if (hasPermission('manage_workflows')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addActionModal">Add Action</button>
<?php endif; ?>
</div>
<div class="card">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Action Type</th>
<th>Configuration</th>
</tr>
</thead>
<tbody>
<?php foreach ($actions as $action): ?>
<tr>
<td><?php echo htmlspecialchars($action['action_type']); ?></td>
<td><pre><?php echo htmlspecialchars(json_encode(json_decode($action['config']), JSON_PRETTY_PRINT)); ?></pre></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</main>
<!-- Add Action Modal -->
<div class="modal fade" id="addActionModal" tabindex="-1" aria-labelledby="addActionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addActionModalLabel">Add New Action</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST">
<input type="hidden" name="add_action" value="1">
<div class="mb-3">
<label for="action_type" class="form-label">Action Type</label>
<select class="form-select" id="action_type" name="action_type" required onchange="showActionConfig(this.value)">
<option value="" disabled selected>Select an action</option>
<option value="send_email">Send Email</option>
<option value="create_task">Create Task</option>
<option value="send_slack_notification">Send Slack Notification</option>
<option value="update_candidate_status">Update Candidate Status</option>
</select>
</div>
<div id="send_email_config" style="display: none;">
<div class="mb-3">
<label for="to" class="form-label">To</label>
<input type="email" class="form-control" id="to" name="to">
</div>
<div class="mb-3">
<label for="subject" class="form-label">Subject</label>
<input type="text" class="form-control" id="subject" name="subject">
</div>
<div class="mb-3">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" id="message" name="message" rows="3"></textarea>
</div>
</div>
<div id="create_task_config" style="display: none;">
<div class="mb-3">
<label for="task_name" class="form-label">Task Name</label>
<input type="text" class="form-control" id="task_name" name="task_name">
</div>
<div class="mb-3">
<label for="assign_to" class="form-label">Assign To</label>
<input type="text" class="form-control" id="assign_to" name="assign_to" placeholder="Candidate ID or '{{candidate.id}}'">
</div>
</div>
<div id="send_slack_notification_config" style="display: none;">
<div class="mb-3">
<label for="webhook_url" class="form-label">Slack Webhook URL</label>
<input type="text" class="form-control" id="webhook_url" name="webhook_url">
</div>
<div class="mb-3">
<label for="slack_message" class="form-label">Message</label>
<textarea class="form-control" id="slack_message" name="slack_message" rows="3"></textarea>
</div>
</div>
<div id="update_candidate_status_config" style="display: none;">
<div class="mb-3">
<label for="new_status" class="form-label">New Status</label>
<select class="form-select" id="new_status" name="new_status">
<option value="Applied">Applied</option>
<option value="Interviewing">Interviewing</option>
<option value="Offered">Offered</option>
<option value="Hired">Hired</option>
<option value="Rejected">Rejected</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Action</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
function showActionConfig(actionType) {
document.getElementById('send_email_config').style.display = 'none';
document.getElementById('create_task_config').style.display = 'none';
document.getElementById('send_slack_notification_config').style.display = 'none';
document.getElementById('update_candidate_status_config').style.display = 'none';
if (actionType) {
document.getElementById(actionType + '_config').style.display = 'block';
}
}
</script>
</body>
</html>

65
workflow_engine.php Normal file
View File

@ -0,0 +1,65 @@
<?php
require_once 'db/config.php';
require_once 'mail/MailService.php';
function trigger_workflow($trigger, $data) {
$pdo = db();
// 1. Fetch all workflows for the given trigger
$stmt = $pdo->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;
}
}
?>

144
workflows.php Normal file
View File

@ -0,0 +1,144 @@
<?php
require_once 'auth.php';
// Check if user is logged in
if (!is_logged_in()) {
header('Location: login.php');
exit;
}
if (!hasPermission('view_workflows')) {
header('Location: index.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
// Handle form submission for new workflow
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_workflow']) && hasPermission('manage_workflows')) {
$name = $_POST['name'] ?? '';
$trigger = $_POST['trigger'] ?? '';
if (!empty($name) && !empty($trigger)) {
try {
$stmt = $pdo->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();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflows</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body>
<header class="header d-flex justify-content-between align-items-center">
<div class="logo">FinMox<span class="dot">.</span></div>
<nav class="d-flex align-items-center">
<a href="index.php" class="btn btn-outline-primary me-2">Home</a>
<a href="chat.php" class="btn btn-outline-primary me-2">Chat</a>
<a href="dashboard.php" class="btn btn-outline-primary me-2">Dashboard</a>
<a href="workflows.php" class="btn btn-outline-primary me-3">Workflows</a>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo htmlspecialchars($_SESSION['username']); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<?php if (hasPermission('manage_roles')): ?>
<li><a class="dropdown-item" href="roles.php">Manage Roles</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</div>
</nav>
</header>
<main class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Workflows</h2>
<?php if (hasPermission('manage_workflows')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addWorkflowModal">Add Workflow</button>
<?php endif; ?>
</div>
<div class="card">
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Trigger</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($workflows as $workflow): ?>
<tr>
<td><?php echo htmlspecialchars($workflow['name']); ?></td>
<td><?php echo htmlspecialchars($workflow['trigger']); ?></td>
<td>
<?php if (hasPermission('manage_workflows')): ?>
<a href="workflow_actions.php?workflow_id=<?php echo $workflow['id']; ?>" class="btn btn-sm btn-outline-primary">Manage Actions</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</main>
<!-- Add Workflow Modal -->
<div class="modal fade" id="addWorkflowModal" tabindex="-1" aria-labelledby="addWorkflowModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addWorkflowModalLabel">Add New Workflow</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST">
<input type="hidden" name="add_workflow" value="1">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="trigger" class="form-label">Trigger</label>
<select class="form-select" id="trigger" name="trigger" required>
<option value="" disabled selected>Select a trigger</option>
<option value="candidate_created">New Candidate is Created</option>
<option value="task_completed">Task is Completed</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Workflow</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>