Nov 19,2025
This commit is contained in:
parent
577ca93381
commit
c13312a14c
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
311
ai/LocalAIApi.php
Normal file
311
ai/LocalAIApi.php
Normal 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
52
ai/config.php
Normal 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
67
api/chat.php
Normal 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
271
assets/css/custom.css
Normal 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
74
assets/js/main.js
Normal 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
|
||||
};
|
||||
});
|
||||
BIN
assets/pasted-20251111-052937-ae05f6a6.jpg
Normal file
BIN
assets/pasted-20251111-052937-ae05f6a6.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/pasted-20251111-062128-0e3cd006.jpg
Normal file
BIN
assets/pasted-20251111-062128-0e3cd006.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/vm-shot-2025-11-11T05-29-19-670Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-11T05-29-19-670Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/vm-shot-2025-11-11T06-19-43-142Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-11T06-19-43-142Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
86
auth.php
Normal file
86
auth.php
Normal 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
164
chat.php
Normal 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
460
dashboard.php
Normal 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>
|
||||
@ -6,12 +6,38 @@ define('DB_USER', 'app_31009');
|
||||
define('DB_PASS', '2c66b530-2a65-423a-a106-6760b49ad1a2');
|
||||
|
||||
function db() {
|
||||
static $pdo;
|
||||
if (!$pdo) {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
static $pdo;
|
||||
if ($pdo) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() === 1049) { // SQLSTATE[HY000] [1049] Unknown database
|
||||
try {
|
||||
$tempPdo = new PDO('mysql:host='.DB_HOST, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
$tempPdo->exec('CREATE DATABASE IF NOT EXISTS `'.DB_NAME.'`');
|
||||
|
||||
// Now, reconnect to the newly created database
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
// If database creation also fails, re-throw the exception
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
// For any other PDO exception, re-throw it
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
21
db/migrate.php
Normal file
21
db/migrate.php
Normal 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();
|
||||
|
||||
10
db/migrations/001_create_candidates_table.sql
Normal file
10
db/migrations/001_create_candidates_table.sql
Normal 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
|
||||
);
|
||||
10
db/migrations/002_create_tasks_table.sql
Normal file
10
db/migrations/002_create_tasks_table.sql
Normal 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
|
||||
);
|
||||
42
db/migrations/003_create_roles_and_permissions_tables.sql
Normal file
42
db/migrations/003_create_roles_and_permissions_tables.sql
Normal 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);
|
||||
9
db/migrations/004_create_users_table.sql
Normal file
9
db/migrations/004_create_users_table.sql
Normal 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)
|
||||
);
|
||||
6
db/migrations/005_create_workflows_table.sql
Normal file
6
db/migrations/005_create_workflows_table.sql
Normal 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
|
||||
);
|
||||
8
db/migrations/006_create_workflow_actions_table.sql
Normal file
8
db/migrations/006_create_workflow_actions_table.sql
Normal 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
|
||||
);
|
||||
1
db/migrations/007_add_role_id_to_users.sql
Normal file
1
db/migrations/007_add_role_id_to_users.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN role_id INT, ADD FOREIGN KEY (role_id) REFERENCES roles(id);
|
||||
8
db/migrations/008_create_tasks_table.sql
Normal file
8
db/migrations/008_create_tasks_table.sql
Normal 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)
|
||||
);
|
||||
6
db/migrations/009_add_candidates_permissions.sql
Normal file
6
db/migrations/009_add_candidates_permissions.sql
Normal 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');
|
||||
11
db/migrations/010_add_tasks_permissions.sql
Normal file
11
db/migrations/010_add_tasks_permissions.sql
Normal 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');
|
||||
7
db/migrations/011_create_workflow_logs_table.sql
Normal file
7
db/migrations/011_create_workflow_logs_table.sql
Normal 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)
|
||||
);
|
||||
1
db/migrations/012_add_updated_at_to_tasks.sql
Normal file
1
db/migrations/012_add_updated_at_to_tasks.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE `tasks` ADD `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
1
db/migrations/013_add_delete_permissions.sql
Normal file
1
db/migrations/013_add_delete_permissions.sql
Normal file
@ -0,0 +1 @@
|
||||
INSERT INTO permissions (name) VALUES ('delete_candidates'), ('delete_tasks');
|
||||
20
delete_candidate.php
Normal file
20
delete_candidate.php
Normal 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
20
delete_task.php
Normal 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
126
edit_candidate.php
Normal 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
142
edit_role.php
Normal 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
133
edit_task.php
Normal 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>
|
||||
572
index.php
572
index.php
@ -1,150 +1,442 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once 'auth.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
// Check if user is logged in
|
||||
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">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<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>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO & Meta Tags -->
|
||||
<title>FinMox Flow</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(); ?>">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
|
||||
<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 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>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Add Task Modal -->
|
||||
<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>
|
||||
</html>
|
||||
68
login.php
Normal file
68
login.php
Normal 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
7
logout.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require_once 'auth.php';
|
||||
|
||||
logout_user();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
?>
|
||||
63
register.php
Normal file
63
register.php
Normal 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
128
roles.php
Normal 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
230
workflow_actions.php
Normal 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
65
workflow_engine.php
Normal 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
144
workflows.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user