Commit 295ff457 authored by Administrator's avatar Administrator

Update 32 files via Son of Anton

parent 3bff7dd8
<?php
declare(strict_types=1);
namespace Modules\Adjustments\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Auth\PermissionEngine;
use Engine\Audit\AuditLogger;
use Engine\Notifications\NotificationManager;
final class AdjustmentController
{
private Connection $db;
private PermissionEngine $perms;
private AuditLogger $audit;
private NotificationManager $notif;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->perms = $c->resolve(PermissionEngine::class);
$this->audit = $c->resolve(AuditLogger::class);
$this->notif = $c->resolve(NotificationManager::class);
}
public function index(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'adjustments.view.any');
$month = $request->query('month', date('Y-m'));
$adjustments = $this->db->fetchAll(
"SELECT ma.*, u.full_name_en as contractor_name, c.full_name_en as created_by_name
FROM manual_adjustments ma
JOIN users u ON u.id = ma.contractor_id
JOIN users c ON c.id = ma.created_by_id
WHERE ma.effective_month = ? AND ma.deleted_at IS NULL ORDER BY ma.created_at DESC",
[$month]
);
return Response::json(['adjustments' => $adjustments, 'month' => $month]);
}
public function create(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'adjustments.create');
$isSA = $user['role'] === 'super_admin';
$status = $isSA ? 'approved' : 'pending_approval';
$id = $this->db->insert('manual_adjustments', [
'contractor_id' => (int)$request->input('contractor_id'),
'type' => $request->input('type'),
'amount' => (float)$request->input('amount'),
'category' => $request->input('category'),
'description' => $request->input('description'),
'effective_month' => $request->input('effective_month', date('Y-m')),
'status' => $status,
'created_by_id' => $user['id'],
'reviewed_by_id' => $isSA ? $user['id'] : null,
'reviewed_at' => $isSA ? date('Y-m-d H:i:s') : null,
]);
if ($isSA) {
$contractor = $this->db->fetchOne("SELECT full_name_en FROM users WHERE id = ?", [(int)$request->input('contractor_id')]);
$this->notif->createImportant((int)$request->input('contractor_id'), 'Salary Adjustment',
"A {$request->input('type')} adjustment of " . number_format((float)$request->input('amount'), 2) . " EGP has been applied.");
} else {
$admins = $this->db->fetchAll("SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1");
foreach ($admins as $a) {
$this->notif->createImportant($a['id'], 'Adjustment Pending Approval',
"Manual adjustment #{$id} requires approval.");
}
}
return Response::json(['success' => true, 'id' => $id]);
}
public function approve(Request $request, string $id): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'adjustments.approve');
$adj = $this->db->fetchOne("SELECT * FROM manual_adjustments WHERE id = ? AND deleted_at IS NULL", [(int)$id]);
if (!$adj) return Response::json(['error' => 'Not found'], 404);
$action = $request->input('action');
$newStatus = $action === 'approve' ? 'approved' : 'rejected';
$this->db->update('manual_adjustments', [
'status' => $newStatus,
'reviewed_by_id' => $user['id'],
'reviewed_at' => date('Y-m-d H:i:s'),
'review_note' => $request->input('review_note'),
], 'id = ?', [(int)$id]);
if ($newStatus === 'approved') {
$this->notif->createImportant($adj['contractor_id'], 'Salary Adjustment Applied',
"A {$adj['type']} adjustment of " . number_format($adj['amount'], 2) . " EGP has been approved.");
}
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/adjustments',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Adjustments\Controllers\AdjustmentController::class, 'index');
$router->post('/', Modules\Adjustments\Controllers\AdjustmentController::class, 'create');
$router->post('/{id}/review', Modules\Adjustments\Controllers\AdjustmentController::class, 'approve');
});
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/boards',
'middleware' => [
Middleware\AuthenticationMiddleware::class,
Middleware\CSRFMiddleware::class,
Middleware\BlockingNotificationMiddleware::class,
]
], function ($router) {
$router->get('/', Modules\Boards\Controllers\BoardController::class, 'index');
$router->post('/', Modules\Boards\Controllers\BoardController::class, 'create');
$router->get('/{boardId}', Modules\Boards\Controllers\BoardController::class, 'show');
$router->post('/{boardId}/update', Modules\Boards\Controllers\BoardController::class, 'update');
$router->post('/{boardId}/archive', Modules\Boards\Controllers\BoardController::class, 'archive');
$router->post('/{boardId}/members', Modules\Boards\Controllers\BoardController::class, 'addMember');
$router->post('/{boardId}/members/{memberId}/remove', Modules\Boards\Controllers\BoardController::class, 'removeMember');
$router->post('/{boardId}/columns', Modules\Boards\Controllers\BoardController::class, 'addColumn');
});
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '',
'middleware' => [
Middleware\AuthenticationMiddleware::class,
Middleware\CSRFMiddleware::class,
Middleware\BlockingNotificationMiddleware::class,
]
], function ($router) {
$router->post('/boards/{boardId}/cards', Modules\Cards\Controllers\CardController::class, 'create');
$router->get('/cards/{cardId}', Modules\Cards\Controllers\CardController::class, 'show');
$router->post('/cards/{cardId}', Modules\Cards\Controllers\CardController::class, 'update');
$router->post('/cards/{cardId}/move', Modules\Cards\Controllers\CardController::class, 'move');
$router->post('/cards/{cardId}/assign', Modules\Cards\Controllers\CardController::class, 'assign');
$router->post('/cards/{cardId}/comments', Modules\Cards\Controllers\CardController::class, 'addComment');
$router->post('/cards/{cardId}/comments/{commentId}/edit', Modules\Cards\Controllers\CardController::class, 'editComment');
$router->post('/cards/{cardId}/bounty', Modules\Cards\Controllers\CardController::class, 'setBounty');
$router->post('/cards/{cardId}/watch', Modules\Cards\Controllers\CardController::class, 'watch');
$router->post('/cards/{cardId}/labels', Modules\Cards\Controllers\CardController::class, 'addLabel');
$router->post('/cards/{cardId}/labels/{labelId}/remove', Modules\Cards\Controllers\CardController::class, 'removeLabel');
$router->post('/cards/{cardId}/checklists', Modules\Cards\Controllers\CardController::class, 'addChecklist');
$router->post('/cards/{cardId}/checklists/{checklistId}/items', Modules\Cards\Controllers\CardController::class, 'addChecklistItem');
$router->post('/cards/{cardId}/checklist-items/{itemId}/toggle', Modules\Cards\Controllers\CardController::class, 'toggleChecklistItem');
$router->post('/cards/{cardId}/archive', Modules\Cards\Controllers\CardController::class, 'archive');
$router->post('/cards/{cardId}/duplicate', Modules\Cards\Controllers\CardController::class, 'duplicate');
});
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/deductions',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Deductions\Controllers\DeductionController::class, 'index');
$router->get('/{deductionId}', Modules\Deductions\Controllers\DeductionController::class, 'show');
$router->post('/', Modules\Deductions\Controllers\DeductionController::class, 'initiate');
$router->post('/{deductionId}/acknowledge', Modules\Deductions\Controllers\DeductionController::class, 'acknowledge');
$router->post('/{deductionId}/respond', Modules\Deductions\Controllers\DeductionController::class, 'respond');
$router->post('/{deductionId}/review-decision', Modules\Deductions\Controllers\DeductionController::class, 'reviewDecision');
$router->post('/{deductionId}/admin-review', Modules\Deductions\Controllers\DeductionController::class, 'adminReview');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Holidays\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Auth\PermissionEngine;
final class HolidayController
{
private Connection $db;
private PermissionEngine $perms;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->perms = $c->resolve(PermissionEngine::class);
}
public function index(Request $request): Response
{
$year = $request->query('year', date('Y'));
$holidays = $this->db->fetchAll(
"SELECT * FROM holidays WHERE YEAR(start_date) = ? OR is_recurring = 1 ORDER BY start_date",
[(int)$year]
);
return Response::json(['holidays' => $holidays]);
}
public function create(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'holidays.manage');
$id = $this->db->insert('holidays', [
'name' => $request->input('name'),
'start_date' => $request->input('start_date'),
'end_date' => $request->input('end_date'),
'is_recurring' => (int)($request->input('is_recurring', 0)),
'notes' => $request->input('notes'),
'created_by_id' => $user['id'],
]);
return Response::json(['success' => true, 'id' => $id]);
}
public function update(Request $request, string $id): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'holidays.manage');
$data = $request->only(['name', 'start_date', 'end_date', 'is_recurring', 'notes']);
$data = array_filter($data, fn($v) => $v !== null);
if (isset($data['is_recurring'])) $data['is_recurring'] = (int)$data['is_recurring'];
$this->db->update('holidays', $data, 'id = ?', [(int)$id]);
return Response::json(['success' => true]);
}
public function delete(Request $request, string $id): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'holidays.manage');
$this->db->delete('holidays', 'id = ?', [(int)$id]);
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/api/holidays',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Holidays\Controllers\HolidayController::class, 'index');
$router->post('/', Modules\Holidays\Controllers\HolidayController::class, 'create');
$router->post('/{id}', Modules\Holidays\Controllers\HolidayController::class, 'update');
$router->post('/{id}/delete', Modules\Holidays\Controllers\HolidayController::class, 'delete');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Labels\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Auth\PermissionEngine;
final class LabelController
{
private Connection $db;
private PermissionEngine $perms;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->perms = $c->resolve(PermissionEngine::class);
}
public function index(Request $request): Response
{
$boardId = $request->query('board_id');
$sql = "SELECT * FROM labels WHERE scope = 'organization'";
$params = [];
if ($boardId) {
$sql .= " OR (scope = 'board' AND board_id = ?)";
$params[] = (int)$boardId;
}
$sql .= " ORDER BY scope DESC, text";
return Response::json(['labels' => $this->db->fetchAll($sql, $params)]);
}
public function create(Request $request): Response
{
$user = $request->user();
$scope = $request->input('scope', 'organization');
$boardId = $request->input('board_id');
if ($scope === 'organization') {
$this->perms->denyUnlessAllowed($user, 'labels.create.org');
} else {
$this->perms->denyUnlessAllowed($user, 'labels.create.board', ['board_id' => (int)$boardId]);
}
$id = $this->db->insert('labels', [
'text' => substr($request->input('text', ''), 0, 20),
'bg_color' => $request->input('bg_color', '#6B7280'),
'text_color' => $request->input('text_color', '#FFFFFF'),
'scope' => $scope,
'board_id' => $scope === 'board' ? (int)$boardId : null,
'created_by_id' => $user['id'],
]);
return Response::json(['success' => true, 'id' => $id]);
}
public function update(Request $request, string $labelId): Response
{
$user = $request->user();
$label = $this->db->fetchOne("SELECT * FROM labels WHERE id = ?", [(int)$labelId]);
if (!$label) return Response::json(['error' => 'Not found'], 404);
$data = $request->only(['text', 'bg_color', 'text_color']);
$data = array_filter($data, fn($v) => $v !== null);
if (isset($data['text'])) $data['text'] = substr($data['text'], 0, 20);
$this->db->update('labels', $data, 'id = ?', [(int)$labelId]);
return Response::json(['success' => true]);
}
public function delete(Request $request, string $labelId): Response
{
$user = $request->user();
$label = $this->db->fetchOne("SELECT * FROM labels WHERE id = ?", [(int)$labelId]);
if (!$label) return Response::json(['error' => 'Not found'], 404);
$this->db->query("DELETE FROM card_labels WHERE label_id = ?", [(int)$labelId]);
$this->db->delete('labels', 'id = ?', [(int)$labelId]);
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/api/labels',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Labels\Controllers\LabelController::class, 'index');
$router->post('/', Modules\Labels\Controllers\LabelController::class, 'create');
$router->post('/{labelId}', Modules\Labels\Controllers\LabelController::class, 'update');
$router->post('/{labelId}/delete', Modules\Labels\Controllers\LabelController::class, 'delete');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Messaging\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Notifications\NotificationManager;
use Engine\Template\TemplateEngine;
final class MessagingController
{
private Connection $db;
private NotificationManager $notif;
private TemplateEngine $templates;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->notif = $c->resolve(NotificationManager::class);
$this->templates = $c->resolve(TemplateEngine::class);
}
public function conversations(Request $request): Response
{
$user = $request->user();
$conversations = $this->db->fetchAll(
"SELECT c.*, cp.last_read_at,
(SELECT content FROM messages WHERE conversation_id = c.id AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1) as last_message,
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id AND m.deleted_at IS NULL AND m.created_at > COALESCE(cp.last_read_at, '1970-01-01')) as unread_count
FROM conversations c
JOIN conversation_participants cp ON cp.conversation_id = c.id AND cp.user_id = ?
ORDER BY c.last_message_at DESC NULLS LAST",
[$user['id']]
);
foreach ($conversations as &$conv) {
$conv['participants'] = $this->db->fetchAll(
"SELECT u.id, u.full_name_en, u.username, u.profile_photo_id
FROM users u JOIN conversation_participants cp ON cp.user_id = u.id
WHERE cp.conversation_id = ? AND u.id != ?",
[$conv['id'], $user['id']]
);
}
if ($request->wantsJson()) {
return Response::json(['conversations' => $conversations]);
}
return Response::html($this->templates->render('messaging/index', [
'user' => $user, 'conversations' => $conversations,
]));
}
public function messages(Request $request, string $conversationId): Response
{
$user = $request->user();
// Verify participant
$participant = $this->db->fetchOne(
"SELECT id FROM conversation_participants WHERE conversation_id = ? AND user_id = ?",
[(int)$conversationId, $user['id']]
);
if (!$participant && $user['role'] !== 'super_admin') {
return Response::json(['error' => 'Not a participant'], 403);
}
$messages = $this->db->fetchAll(
"SELECT m.*, u.full_name_en as sender_name, u.profile_photo_id as sender_photo
FROM messages m JOIN users u ON u.id = m.sender_id
WHERE m.conversation_id = ? AND m.deleted_at IS NULL ORDER BY m.created_at ASC",
[(int)$conversationId]
);
// Mark as read
$this->db->update('conversation_participants', [
'last_read_at' => date('Y-m-d H:i:s'),
], 'conversation_id = ? AND user_id = ?', [(int)$conversationId, $user['id']]);
return Response::json(['messages' => $messages]);
}
public function startConversation(Request $request): Response
{
$user = $request->user();
$recipientIds = $request->input('recipient_ids', []);
if (empty($recipientIds)) {
return Response::json(['error' => 'At least one recipient required'], 422);
}
// Messaging permission enforcement
if ($user['role'] === 'contractor') {
foreach ($recipientIds as $rid) {
$recipient = $this->db->fetchOne("SELECT role FROM users WHERE id = ?", [(int)$rid]);
if (!$recipient || $recipient['role'] === 'contractor') {
return Response::json(['error' => 'Contractors can only message their PL, Admins, and Super Admin.'], 403);
}
}
}
$type = count($recipientIds) > 1 ? 'group' : 'dm';
// Check for existing DM
if ($type === 'dm') {
$existingConv = $this->db->fetchOne(
"SELECT c.id FROM conversations c
JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = ?
JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = ?
WHERE c.type = 'dm'",
[$user['id'], (int)$recipientIds[0]]
);
if ($existingConv) {
return Response::json(['success' => true, 'conversation_id' => $existingConv['id'], 'existing' => true]);
}
}
$convId = $this->db->transaction(function () use ($user, $recipientIds, $type, $request) {
$convId = $this->db->insert('conversations', [
'type' => $type,
'title' => $type === 'group' ? $request->input('title') : null,
'created_by_id' => $user['id'],
]);
$this->db->insert('conversation_participants', [
'conversation_id' => $convId,
'user_id' => $user['id'],
]);
foreach ($recipientIds as $rid) {
$this->db->insert('conversation_participants', [
'conversation_id' => $convId,
'user_id' => (int)$rid,
]);
}
return $convId;
});
return Response::json(['success' => true, 'conversation_id' => $convId]);
}
public function sendMessage(Request $request, string $conversationId): Response
{
$user = $request->user();
$participant = $this->db->fetchOne(
"SELECT id FROM conversation_participants WHERE conversation_id = ? AND user_id = ?",
[(int)$conversationId, $user['id']]
);
if (!$participant) return Response::json(['error' => 'Not a participant'], 403);
$content = trim($request->input('content', ''));
if (!$content) return Response::json(['error' => 'Message cannot be empty'], 422);
$messageId = $this->db->insert('messages', [
'conversation_id' => (int)$conversationId,
'sender_id' => $user['id'],
'content' => $content,
]);
$this->db->update('conversations', [
'last_message_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int)$conversationId]);
// Notify other participants
$others = $this->db->fetchAll(
"SELECT user_id FROM conversation_participants WHERE conversation_id = ? AND user_id != ?",
[(int)$conversationId, $user['id']]
);
foreach ($others as $other) {
$this->notif->createImportant($other['user_id'], 'New Message',
"{$user['full_name_en']} sent you a message.",
"/messages/{$conversationId}", 'conversation', (int)$conversationId);
}
return Response::json(['success' => true, 'id' => $messageId]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/messages',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class, Middleware\BlockingNotificationMiddleware::class]
], function ($router) {
$router->get('/', Modules\Messaging\Controllers\MessagingController::class, 'conversations');
$router->post('/', Modules\Messaging\Controllers\MessagingController::class, 'startConversation');
$router->get('/{conversationId}', Modules\Messaging\Controllers\MessagingController::class, 'messages');
$router->post('/{conversationId}', Modules\Messaging\Controllers\MessagingController::class, 'sendMessage');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Payroll\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Auth\PermissionEngine;
use Engine\Audit\AuditLogger;
use Engine\Notifications\NotificationManager;
final class PayrollController
{
private Connection $db;
private PermissionEngine $perms;
private AuditLogger $audit;
private NotificationManager $notif;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->perms = $c->resolve(PermissionEngine::class);
$this->audit = $c->resolve(AuditLogger::class);
$this->notif = $c->resolve(NotificationManager::class);
}
public function index(Request $request): Response
{
$user = $request->user();
$month = $request->query('month', date('Y-m'));
if ($user['role'] === 'contractor') {
$records = $this->db->fetchAll(
"SELECT * FROM payroll_records WHERE contractor_id = ? ORDER BY month DESC",
[$user['id']]
);
} else {
$this->perms->denyUnlessAllowed($user, 'payroll.view.any');
$records = $this->db->fetchAll(
"SELECT pr.*, u.full_name_en FROM payroll_records pr
JOIN users u ON u.id = pr.contractor_id WHERE pr.month = ? ORDER BY u.full_name_en",
[$month]
);
}
return Response::json(['records' => $records, 'month' => $month]);
}
public function calculate(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'payroll.calculate');
$month = $request->input('month', date('Y-m'));
$contractors = $this->db->fetchAll(
"SELECT * FROM users WHERE role = 'contractor' AND status IN ('active','on_pip','suspended') AND actual_salary IS NOT NULL"
);
$count = 0;
foreach ($contractors as $contractor) {
$exists = $this->db->fetchOne(
"SELECT id FROM payroll_records WHERE contractor_id = ? AND month = ?",
[$contractor['id'], $month]
);
if ($exists) continue;
$actual = (float)$contractor['actual_salary'];
$bounties = (float)$this->db->fetchColumn(
"SELECT COALESCE(SUM(amount), 0) FROM bounty_payouts WHERE recipient_id = ? AND payroll_month = ?",
[$contractor['id'], $month]
);
$posAdj = (float)$this->db->fetchColumn(
"SELECT COALESCE(SUM(amount), 0) FROM manual_adjustments
WHERE contractor_id = ? AND effective_month = ? AND type = 'positive' AND status = 'approved' AND deleted_at IS NULL",
[$contractor['id'], $month]
);
$negAdj = (float)$this->db->fetchColumn(
"SELECT COALESCE(SUM(amount), 0) FROM manual_adjustments
WHERE contractor_id = ? AND effective_month = ? AND type = 'negative' AND status = 'approved' AND deleted_at IS NULL",
[$contractor['id'], $month]
);
$deductionsByCategory = [];
foreach (['A', 'B', 'C', 'D'] as $cat) {
$deductionsByCategory[$cat] = (float)$this->db->fetchColumn(
"SELECT COALESCE(SUM(COALESCE(final_amount, calculated_amount)), 0) FROM deductions
WHERE contractor_id = ? AND payroll_month = ? AND category = ?
AND status IN ('applied','applied_no_response','reduced','accepted') AND deleted_at IS NULL",
[$contractor['id'], $month, $cat]
);
}
$totalDeductions = array_sum($deductionsByCategory);
$net = $actual + $bounties + $posAdj - $totalDeductions - $negAdj;
$this->db->insert('payroll_records', [
'contractor_id' => $contractor['id'],
'month' => $month,
'actual_salary' => $actual,
'total_bounties' => $bounties,
'total_positive_adjustments' => $posAdj,
'total_deductions_a' => $deductionsByCategory['A'],
'total_deductions_b' => $deductionsByCategory['B'],
'total_deductions_c' => $deductionsByCategory['C'],
'total_deductions_d' => $deductionsByCategory['D'],
'total_negative_adjustments' => $negAdj,
'net_payable' => $net,
'status' => 'calculated',
'calculated_at' => date('Y-m-d H:i:s'),
]);
$count++;
}
return Response::json(['success' => true, 'calculated' => $count, 'month' => $month]);
}
public function submit(Request $request, string $payrollId): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'payroll.review');
$this->db->update('payroll_records', [
'status' => 'submitted',
'reviewed_by_id' => $user['id'],
'submitted_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int)$payrollId]);
$admins = $this->db->fetchAll("SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1");
foreach ($admins as $a) {
$this->notif->createImportant($a['id'], 'Payroll Submitted for Approval',
"Payroll record #{$payrollId} submitted for approval.");
}
return Response::json(['success' => true]);
}
public function approve(Request $request, string $payrollId): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'payroll.approve');
$record = $this->db->fetchOne("SELECT * FROM payroll_records WHERE id = ?", [(int)$payrollId]);
if (!$record) return Response::json(['error' => 'Not found'], 404);
$this->db->update('payroll_records', [
'status' => 'approved',
'approved_by_id' => $user['id'],
'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int)$payrollId]);
$this->notif->createImportant($record['contractor_id'], 'Payment Approved',
"Your payment for {$record['month']} has been approved: " . number_format($record['net_payable'], 2) . " EGP.");
return Response::json(['success' => true]);
}
public function reject(Request $request, string $payrollId): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'payroll.approve');
$this->db->update('payroll_records', [
'status' => 'rejected',
'rejected_note' => $request->input('reason', ''),
], 'id = ?', [(int)$payrollId]);
return Response::json(['success' => true]);
}
public function markPaid(Request $request, string $payrollId): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'payroll.approve');
$this->db->update('payroll_records', [
'status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int)$payrollId]);
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/payroll',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Payroll\Controllers\PayrollController::class, 'index');
$router->post('/calculate', Modules\Payroll\Controllers\PayrollController::class, 'calculate');
$router->post('/{payrollId}/submit', Modules\Payroll\Controllers\PayrollController::class, 'submit');
$router->post('/{payrollId}/approve', Modules\Payroll\Controllers\PayrollController::class, 'approve');
$router->post('/{payrollId}/reject', Modules\Payroll\Controllers\PayrollController::class, 'reject');
$router->post('/{payrollId}/paid', Modules\Payroll\Controllers\PayrollController::class, 'markPaid');
});
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/reports',
'middleware' => [
Middleware\AuthenticationMiddleware::class,
Middleware\CSRFMiddleware::class,
Middleware\BlockingNotificationMiddleware::class,
]
], function ($router) {
$router->get('/submit', Modules\Reports\Controllers\ReportController::class, 'submitForm');
$router->post('/submit', Modules\Reports\Controllers\ReportController::class, 'submit');
$router->get('/review', Modules\Reports\Controllers\ReportController::class, 'review');
$router->post('/{reportId}/review', Modules\Reports\Controllers\ReportController::class, 'reviewAction');
$router->post('/bulk-approve', Modules\Reports\Controllers\ReportController::class, 'bulkApprove');
$router->get('/history', Modules\Reports\Controllers\ReportController::class, 'history');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Settings\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Auth\PermissionEngine;
use Engine\Audit\AuditLogger;
final class SettingsController
{
private Connection $db;
private PermissionEngine $perms;
private AuditLogger $audit;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->perms = $c->resolve(PermissionEngine::class);
$this->audit = $c->resolve(AuditLogger::class);
}
public function index(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'settings.manage');
$settings = $this->db->fetchAll("SELECT * FROM system_settings ORDER BY `group`, `key`");
$grouped = [];
foreach ($settings as $s) {
$grouped[$s['group']][] = $s;
}
return Response::json(['settings' => $grouped]);
}
public function update(Request $request): Response
{
$user = $request->user();
$this->perms->denyUnlessAllowed($user, 'settings.manage');
$updates = $request->input('settings', []);
foreach ($updates as $key => $value) {
$existing = $this->db->fetchOne("SELECT * FROM system_settings WHERE `key` = ?", [$key]);
if (!$existing) continue;
$oldValue = $existing['value'];
$this->db->update('system_settings', [
'value' => (string)$value,
'updated_by_id' => $user['id'],
], '`key` = ?', [$key]);
$this->audit->log($user, 'SETTING_CHANGED', 'system_setting', null, 'settings', '/settings',
['key' => $key, 'value' => $oldValue], ['key' => $key, 'value' => (string)$value],
$request->ip(), $request->userAgent());
}
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/settings',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Settings\Controllers\SettingsController::class, 'index');
$router->post('/', Modules\Settings\Controllers\SettingsController::class, 'update');
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Modules\Unavailability\Controllers;
use Engine\Core\Container;
use Engine\Core\Request;
use Engine\Core\Response;
use Engine\Database\Connection;
use Engine\Notifications\NotificationManager;
final class UnavailabilityController
{
private Connection $db;
private NotificationManager $notif;
public function __construct()
{
$c = Container::getInstance();
$this->db = $c->resolve(Connection::class);
$this->notif = $c->resolve(NotificationManager::class);
}
public function index(Request $request): Response
{
$user = $request->user();
$userId = (int)($request->query('user_id', $user['id']));
if ($userId !== $user['id'] && !in_array($user['role'], ['super_admin', 'admin'])) {
return Response::json(['error' => 'Forbidden'], 403);
}
$records = $this->db->fetchAll(
"SELECT * FROM unavailability_records WHERE user_id = ? ORDER BY start_date DESC",
[$userId]
);
return Response::json(['records' => $records]);
}
public function create(Request $request): Response
{
$user = $request->user();
$targetUserId = (int)($request->input('user_id', $user['id']));
if ($targetUserId !== $user['id'] && !in_array($user['role'], ['super_admin', 'admin'])) {
return Response::json(['error' => 'Forbidden'], 403);
}
$id = $this->db->insert('unavailability_records', [
'user_id' => $targetUserId,
'start_date' => $request->input('start_date'),
'end_date' => $request->input('end_date'),
'reason_category' => $request->input('reason_category'),
'notes' => $request->input('notes'),
]);
// Notify PL and Admin
$target = $this->db->fetchOne("SELECT full_name_en, assigned_pl_id FROM users WHERE id = ?", [$targetUserId]);
if ($target['assigned_pl_id']) {
$this->notif->createInformational($target['assigned_pl_id'], 'Unavailability Logged',
"{$target['full_name_en']} logged unavailability: {$request->input('start_date')} to {$request->input('end_date')}.");
}
return Response::json(['success' => true, 'id' => $id]);
}
public function update(Request $request, string $id): Response
{
$user = $request->user();
$record = $this->db->fetchOne("SELECT * FROM unavailability_records WHERE id = ?", [(int)$id]);
if (!$record) return Response::json(['error' => 'Not found'], 404);
if ($record['user_id'] !== $user['id'] && !in_array($user['role'], ['super_admin', 'admin'])) {
return Response::json(['error' => 'Forbidden'], 403);
}
$data = $request->only(['start_date', 'end_date', 'reason_category', 'notes']);
$data = array_filter($data, fn($v) => $v !== null);
$this->db->update('unavailability_records', $data, 'id = ?', [(int)$id]);
return Response::json(['success' => true]);
}
public function delete(Request $request, string $id): Response
{
$user = $request->user();
$record = $this->db->fetchOne("SELECT * FROM unavailability_records WHERE id = ?", [(int)$id]);
if (!$record) return Response::json(['error' => 'Not found'], 404);
if ($record['user_id'] !== $user['id'] && !in_array($user['role'], ['super_admin', 'admin'])) {
return Response::json(['error' => 'Forbidden'], 403);
}
$this->db->delete('unavailability_records', 'id = ?', [(int)$id]);
return Response::json(['success' => true]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/api/unavailability',
'middleware' => [Middleware\AuthenticationMiddleware::class, Middleware\CSRFMiddleware::class]
], function ($router) {
$router->get('/', Modules\Unavailability\Controllers\UnavailabilityController::class, 'index');
$router->post('/', Modules\Unavailability\Controllers\UnavailabilityController::class, 'create');
$router->post('/{id}', Modules\Unavailability\Controllers\UnavailabilityController::class, 'update');
$router->post('/{id}/delete', Modules\Unavailability\Controllers\UnavailabilityController::class, 'delete');
});
\ No newline at end of file
This diff is collapsed.
<?php
declare(strict_types=1);
use Engine\Core\Container;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
$router->group([
'prefix' => '/users',
'middleware' => [
Middleware\AuthenticationMiddleware::class,
Middleware\CSRFMiddleware::class,
Middleware\BlockingNotificationMiddleware::class,
]
], function ($router) {
$router->get('/', Modules\Users\Controllers\UserController::class, 'directory');
$router->get('/{userId}', Modules\Users\Controllers\UserController::class, 'show');
$router->post('/{userId}', Modules\Users\Controllers\UserController::class, 'update');
$router->post('/{userId}/salary', Modules\Users\Controllers\UserController::class, 'setSalary');
$router->post('/{userId}/status', Modules\Users\Controllers\UserController::class, 'changeStatus');
$router->get('/{userId}/notes', Modules\Users\Controllers\UserController::class, 'privateNotes');
$router->post('/{userId}/notes', Modules\Users\Controllers\UserController::class, 'addPrivateNote');
$router->get('/{userId}/sessions', Modules\Users\Controllers\UserController::class, 'sessions');
$router->post('/{userId}/force-logout', Modules\Users\Controllers\UserController::class, 'forceLogout');
$router->get('/{userId}/salary-history', Modules\Users\Controllers\UserController::class, 'salaryHistory');
});
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Boards<?php $__engine->endSection(); ?>
<div class="container">
<div class="page-header">
<h1>📋 Boards</h1>
<?php if (in_array($user['role'], ['super_admin', 'admin'])): ?>
<button class="btn btn-primary" onclick="document.getElementById('create-board-modal').style.display='block'">+ New Board</button>
<?php endif; ?>
</div>
<div class="board-grid">
<?php foreach ($boards as $board): ?>
<a href="/boards/<?= $board['id'] ?>" class="card board-card">
<h3><?= $__engine->e($board['name']) ?></h3>
<p class="text-muted"><?= $__engine->e($board['description'] ?? '') ?></p>
<div class="board-meta">
<span>🔑 <?= $__engine->e($board['board_key']) ?></span>
<span>👥 <?= $board['member_count'] ?> members</span>
<span>🃏 <?= $board['card_count'] ?> cards</span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?><?= $__engine->e($board['name']) ?><?php $__engine->endSection(); ?>
<div class="board-page">
<div class="board-header">
<h1><?= $__engine->e($board['name']) ?></h1>
<span class="board-key"><?= $__engine->e($board['board_key']) ?></span>
<span class="text-muted"><?= count($members) ?> members</span>
</div>
<div class="kanban-board" id="kanban">
<?php foreach ($columns as $column): ?>
<div class="kanban-column" data-column-id="<?= $column['id'] ?>" data-slug="<?= $__engine->e($column['slug']) ?>">
<div class="column-header">
<span class="column-icon"><?= $__engine->e($column['icon'] ?? '') ?></span>
<span class="column-name"><?= $__engine->e($column['name']) ?></span>
<span class="column-count"><?= count($cards_by_column[$column['id']] ?? []) ?></span>
</div>
<div class="column-cards" data-column-id="<?= $column['id'] ?>">
<?php foreach (($cards_by_column[$column['id']] ?? []) as $card): ?>
<div class="kanban-card" data-card-id="<?= $card['id'] ?>" onclick="window.location='/cards/<?= $card['id'] ?>'">
<?php if (!empty($card['labels'])): ?>
<div class="card-labels">
<?php foreach (array_slice($card['labels'], 0, 3) as $label): ?>
<span class="label-pill" style="background:<?= $__engine->e($label['bg_color']) ?>;color:<?= $__engine->e($label['text_color']) ?>"><?= $__engine->e($label['text']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="card-title"><?= $__engine->e($card['title']) ?></div>
<div class="card-key"><?= $__engine->e($card['card_key']) ?></div>
<div class="card-meta">
<?php if ($card['deadline']): ?>
<span class="<?= strtotime($card['deadline']) < time() && !$card['done_at'] ? 'overdue' : '' ?>">
<?= date('M j', strtotime($card['deadline'])) ?>
</span>
<?php endif; ?>
<?php if ($card['bounty_amount']): ?>
<span class="bounty-badge">💰 <?= number_format($card['bounty_amount'], 0) ?></span>
<?php endif; ?>
</div>
<?php if (!empty($card['assignee_ids'])): ?>
<div class="card-assignees">
<span class="assignee-count">👤 <?= count($card['assignee_ids']) ?></span>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?><?= $__engine->e($card['card_key']) ?><?= $__engine->e($card['title']) ?><?php $__engine->endSection(); ?>
<div class="card-detail">
<div class="card-detail-header">
<a href="/boards/<?= $card['board_id'] ?>" class="btn btn-sm btn-secondary"><?= $__engine->e($board['name']) ?></a>
<h1><?= $__engine->e($card['card_key']) ?>: <?= $__engine->e($card['title']) ?></h1>
<span class="badge badge-<?= $__engine->e($column['slug']) ?>"><?= $__engine->e($column['name']) ?></span>
</div>
<div class="card-detail-body">
<div class="card-main">
<?php if ($card['description']): ?>
<div class="card-description">
<h3>Description</h3>
<div class="rich-text"><?= $card['description'] ?></div>
</div>
<?php endif; ?>
<?php foreach ($card['checklists'] as $cl): ?>
<div class="checklist">
<h3>☑️ <?= $__engine->e($cl['name']) ?></h3>
<?php $total = count($cl['items']); $done = count(array_filter($cl['items'], fn($i) => $i['is_checked'])); ?>
<div class="progress-bar"><div style="width:<?= $total > 0 ? round(($done/$total)*100) : 0 ?>%"></div></div>
<p class="text-muted"><?= $done ?>/<?= $total ?> complete</p>
<?php foreach ($cl['items'] as $item): ?>
<label class="checklist-item">
<input type="checkbox" <?= $item['is_checked'] ? 'checked' : '' ?>
onchange="fetch('/cards/<?= $card['id'] ?>/checklist-items/<?= $item['id'] ?>/toggle',{method:'POST',headers:{'X-CSRF-Token':getCsrfToken(),'Accept':'application/json'}})">
<span class="<?= $item['is_checked'] ? 'checked-text' : '' ?>"><?= $__engine->e($item['text']) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<div class="card-comments">
<h3>💬 Comments & Activity</h3>
<?php foreach ($card['comments'] as $comment): ?>
<div class="comment">
<strong><?= $__engine->e($comment['author_name']) ?></strong>
<span class="text-muted"><?= date('M j, H:i', strtotime($comment['created_at'])) ?></span>
<?php if ($comment['edited_at']): ?><span class="text-muted">(edited)</span><?php endif; ?>
<div class="comment-content"><?= nl2br($__engine->e($comment['content'])) ?></div>
</div>
<?php endforeach; ?>
<form method="POST" action="/cards/<?= $card['id'] ?>/comments" class="comment-form">
<input type="hidden" name="_csrf_token" value="<?= $__engine->e($_COOKIE['csrf_token'] ?? '') ?>">
<textarea name="content" placeholder="Add a comment..." rows="3" required></textarea>
<button type="submit" class="btn btn-primary btn-sm">Comment</button>
</form>
</div>
</div>
<div class="card-sidebar">
<div class="sidebar-section">
<h4>Assignees</h4>
<?php foreach ($card['assignees'] as $a): ?>
<div class="assignee-row"><?= $__engine->e($a['full_name_en']) ?></div>
<?php endforeach; ?>
</div>
<div class="sidebar-section">
<h4>Labels</h4>
<?php foreach ($card['labels'] as $l): ?>
<span class="label-pill" style="background:<?= $__engine->e($l['bg_color']) ?>;color:<?= $__engine->e($l['text_color']) ?>"><?= $__engine->e($l['text']) ?></span>
<?php endforeach; ?>
</div>
<?php if ($card['deadline']): ?>
<div class="sidebar-section">
<h4>⏰ Deadline</h4>
<p class="<?= strtotime($card['deadline']) < time() && !$card['done_at'] ? 'overdue' : '' ?>">
<?= date('M j, Y H:i', strtotime($card['deadline'])) ?>
</p>
</div>
<?php endif; ?>
<?php if ($card['bounty_amount']): ?>
<div class="sidebar-section">
<h4>💰 Bounty</h4>
<p class="bounty-amount"><?= number_format($card['bounty_amount'], 2) ?> EGP</p>
</div>
<?php endif; ?>
<?php if ($card['priority'] !== 'none'): ?>
<div class="sidebar-section">
<h4>Priority</h4>
<p><?= ucfirst($card['priority']) ?></p>
</div>
<?php endif; ?>
<div class="sidebar-section">
<h4>Info</h4>
<p>Created: <?= date('M j, Y', strtotime($card['created_at'])) ?></p>
<?php if ($card['done_at']): ?>
<p>Completed: <?= date('M j, Y', strtotime($card['done_at'])) ?></p>
<?php endif; ?>
</div>
</div>
</div>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Messages<?php $__engine->endSection(); ?>
<div class="container">
<div class="page-header">
<h1>💬 Messages</h1>
<button class="btn btn-primary" id="new-conversation-btn">+ New Conversation</button>
</div>
<div class="conversation-list">
<?php if (empty($conversations)): ?>
<div class="card"><p class="text-muted">No conversations yet.</p></div>
<?php endif; ?>
<?php foreach ($conversations as $conv): ?>
<a href="/messages/<?= $conv['id'] ?>" class="card conversation-card" style="display:block;margin-bottom:8px;text-decoration:none">
<div style="display:flex;justify-content:space-between">
<div>
<strong>
<?php foreach ($conv['participants'] as $p): ?>
<?= $__engine->e($p['full_name_en']) ?><?php if ($p !== end($conv['participants'])) echo ', '; ?>
<?php endforeach; ?>
</strong>
<?php if ($conv['last_message']): ?>
<p class="text-muted" style="margin:4px 0 0"><?= $__engine->e(substr($conv['last_message'], 0, 80)) ?></p>
<?php endif; ?>
</div>
<div style="text-align:right">
<?php if ($conv['last_message_at']): ?>
<small class="text-muted"><?= date('M j, H:i', strtotime($conv['last_message_at'])) ?></small>
<?php endif; ?>
<?php if ($conv['unread_count'] > 0): ?>
<span class="badge" style="background:var(--primary);color:white;position:static;width:auto;height:auto;padding:2px 8px;font-size:0.75em"><?= $conv['unread_count'] ?></span>
<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Review Reports<?php $__engine->endSection(); ?>
<div class="container">
<div class="page-header">
<h1>📋 Report Review — <?= $__engine->e($date) ?></h1>
</div>
<?php if (empty($reports)): ?>
<div class="card"><p class="text-muted">No reports to review for this date.</p></div>
<?php endif; ?>
<?php foreach ($reports as $report): ?>
<div class="card" style="margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center">
<h3><?= $__engine->e($report['full_name_en']) ?></h3>
<span class="badge badge-<?= $report['status'] === 'submitted' ? 'warning' : ($report['status'] === 'approved' ? 'success' : 'danger') ?>">
<?= ucfirst(str_replace('_', ' ', $report['status'])) ?>
</span>
</div>
<?php foreach ($report['tasks'] as $task): ?>
<div class="task-entry-review" style="padding:8px;border:1px solid var(--border);border-radius:var(--radius);margin:8px 0">
<?php if ($task['card_key']): ?>
<strong><?= $__engine->e($task['card_key']) ?>: <?= $__engine->e($task['card_title']) ?></strong><br>
<?php endif; ?>
<p><?= nl2br($__engine->e($task['work_description'])) ?></p>
<small class="text-muted"><?= round($task['time_spent_minutes'] / 60, 1) ?>h · <?= ucfirst(str_replace('_', ' ', $task['task_status'])) ?></small>
</div>
<?php endforeach; ?>
<?php if ($report['blockers']): ?>
<p><strong>Blockers:</strong> <?= nl2br($__engine->e($report['blockers'])) ?></p>
<?php endif; ?>
<p class="text-muted">Total: <?= $report['total_hours'] ?>h · Submitted: <?= date('H:i', strtotime($report['submitted_at'])) ?>
· <?= $report['is_on_time'] ? '✅ On time' : '⏰ Late' ?></p>
<?php if (in_array($report['status'], ['submitted', 'late'])): ?>
<div style="display:flex;gap:8px;margin-top:12px">
<form method="POST" action="/reports/<?= $report['id'] ?>/review" style="display:inline">
<input type="hidden" name="_csrf_token" value="<?= $__engine->e($_COOKIE['csrf_token'] ?? '') ?>">
<input type="hidden" name="action" value="approve">
<button type="submit" class="btn btn-success btn-sm">✅ Approve</button>
</form>
<form method="POST" action="/reports/<?= $report['id'] ?>/review" style="display:inline">
<input type="hidden" name="_csrf_token" value="<?= $__engine->e($_COOKIE['csrf_token'] ?? '') ?>">
<input type="hidden" name="action" value="flag_vague">
<button type="submit" class="btn btn-warning btn-sm">⚠️ Flag Vague</button>
</form>
<form method="POST" action="/reports/<?= $report['id'] ?>/review" style="display:inline">
<input type="hidden" name="_csrf_token" value="<?= $__engine->e($_COOKIE['csrf_token'] ?? '') ?>">
<input type="hidden" name="action" value="request_revision">
<button type="submit" class="btn btn-danger btn-sm">🔄 Request Revision</button>
</form>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Submit Daily Report<?php $__engine->endSection(); ?>
<div class="container">
<div class="card">
<h2>📋 Daily Report — <?= date('l, F j, Y') ?></h2>
<?php if ($report && in_array($report['status'], ['submitted','approved','approved_auto','late'])): ?>
<div class="alert alert-success">You've already submitted your report for today.</div>
<?php else: ?>
<form method="POST" action="/reports/submit" id="report-form">
<input type="hidden" name="_csrf_token" value="<?= $__engine->e($_COOKIE['csrf_token'] ?? '') ?>">
<input type="hidden" name="report_date" value="<?= $__engine->e($report_date) ?>">
<div id="task-entries">
<h3>Tasks Worked On</h3>
<div class="task-entry" data-index="0">
<div class="form-group">
<label>Card (optional)</label>
<select name="tasks[0][card_id]">
<option value="">— No card linked —</option>
<?php foreach ($my_cards as $card): ?>
<option value="<?= $card['id'] ?>"><?= $__engine->e($card['card_key'] . ': ' . $card['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Work Description (min 50 chars)</label>
<textarea name="tasks[0][work_description]" required minlength="50" rows="3"></textarea>
</div>
<div class="form-group" style="display:flex;gap:12px">
<div style="flex:1">
<label>Time Spent (minutes)</label>
<input type="number" name="tasks[0][time_spent_minutes]" required min="15" step="15" value="60">
</div>
<div style="flex:1">
<label>Status</label>
<select name="tasks[0][task_status]">
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="blocked">Blocked</option>
</select>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm" onclick="addTaskEntry()">+ Add Another Task</button>
<hr style="margin:20px 0">
<div class="form-group">
<label>Blockers (required if any task is blocked)</label>
<textarea name="blockers" rows="2"></textarea>
</div>
<div class="form-group">
<label>Additional Notes</label>
<textarea name="additional_notes" rows="2"></textarea>
</div>
<div class="form-group">
<label>How are you feeling?</label>
<div style="display:flex;gap:12px">
<label><input type="radio" name="mood" value="frustrated"> 😤 Frustrated</label>
<label><input type="radio" name="mood" value="neutral"> 😐 Neutral</label>
<label><input type="radio" name="mood" value="good" checked> 😊 Good</label>
<label><input type="radio" name="mood" value="on_fire"> 🔥 On Fire</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block">Submit Report</button>
</form>
<?php endif; ?>
</div>
</div>
<?php $__engine->section('scripts'); ?>
<script>
let taskIndex = 1;
function addTaskEntry() {
const container = document.getElementById('task-entries');
const first = container.querySelector('.task-entry');
const clone = first.cloneNode(true);
clone.dataset.index = taskIndex;
clone.querySelectorAll('[name]').forEach(el => {
el.name = el.name.replace(/\[\d+\]/, `[${taskIndex}]`);
if (el.tagName === 'TEXTAREA') el.value = '';
if (el.tagName === 'SELECT') el.selectedIndex = 0;
if (el.type === 'number') el.value = '60';
});
container.appendChild(clone);
taskIndex++;
}
</script>
<?php $__engine->endSection(); ?>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Team Directory<?php $__engine->endSection(); ?>
<div class="container">
<h1>👥 Team Directory</h1>
<form method="GET" action="/users" style="margin:16px 0;display:flex;gap:8px;flex-wrap:wrap">
<input type="text" name="search" value="<?= $__engine->e($search) ?>" placeholder="Search by name..." class="form-control" style="flex:1;min-width:200px">
<select name="role" class="form-control" style="width:auto">
<option value="">All Roles</option>
<option value="contractor" <?= $filter_role === 'contractor' ? 'selected' : '' ?>>Contractor</option>
<option value="project_leader" <?= $filter_role === 'project_leader' ? 'selected' : '' ?>>Project Leader</option>
<option value="admin" <?= $filter_role === 'admin' ? 'selected' : '' ?>>Admin</option>
</select>
<select name="status" class="form-control" style="width:auto">
<option value="">All Statuses</option>
<option value="active" <?= $filter_status === 'active' ? 'selected' : '' ?>>Active</option>
<option value="onboarding" <?= $filter_status === 'onboarding' ? 'selected' : '' ?>>Onboarding</option>
<option value="on_pip" <?= $filter_status === 'on_pip' ? 'selected' : '' ?>>On PIP</option>
</select>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<div class="directory-grid">
<?php foreach ($users as $u): ?>
<a href="/users/<?= $u['id'] ?>" class="card directory-card">
<div class="directory-avatar">👤</div>
<h3><?= $__engine->e($u['full_name_en']) ?></h3>
<p class="text-muted">@<?= $__engine->e($u['username']) ?></p>
<span class="role-badge"><?= ucfirst(str_replace('_', ' ', $u['role'])) ?></span>
<span class="badge badge-<?= $u['status'] === 'active' ? 'success' : 'warning' ?>"><?= ucfirst($u['status']) ?></span>
<?php if (!empty($u['boards'])): ?>
<div class="directory-boards">
<?php foreach (array_slice($u['boards'], 0, 3) as $b): ?>
<span class="tag"><?= $__engine->e($b['name']) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php if ($last_page > 1): ?>
<div style="text-align:center;margin:20px">
<?php for ($p = 1; $p <= $last_page; $p++): ?>
<a href="?page=<?= $p ?>&search=<?= urlencode($search) ?>" class="btn btn-sm <?= $p === $page ? 'btn-primary' : 'btn-secondary' ?>"><?= $p ?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?><?= $__engine->e($profile['full_name_en']) ?> — Profile<?php $__engine->endSection(); ?>
<div class="container">
<div class="card">
<div style="display:flex;gap:20px;align-items:center;margin-bottom:20px">
<div class="profile-avatar" style="width:80px;height:80px;background:var(--primary);border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:2em">
<?= strtoupper(substr($profile['full_name_en'], 0, 1)) ?>
</div>
<div>
<h1 style="margin:0"><?= $__engine->e($profile['full_name_en']) ?></h1>
<p class="text-muted">@<?= $__engine->e($profile['username']) ?> · <?= ucfirst(str_replace('_', ' ', $profile['role'])) ?></p>
<span class="badge badge-<?= $profile['status'] === 'active' ? 'success' : 'warning' ?>"><?= ucfirst($profile['status']) ?></span>
<?php if ($profile['contractor_type']): ?>
<span class="badge"><?= ucfirst(str_replace('_', ' ', $profile['contractor_type'])) ?></span>
<?php endif; ?>
</div>
</div>
<?php if (isset($profile['actual_salary'])): ?>
<div class="stat-row">
<span>Actual Salary</span>
<strong>EGP <?= number_format($profile['actual_salary'], 2) ?></strong>
</div>
<?php endif; ?>
<?php if (isset($profile['base_salary'])): ?>
<div class="stat-row">
<span>Base Salary</span>
<strong>EGP <?= number_format($profile['base_salary'], 2) ?></strong>
</div>
<?php endif; ?>
<?php if (!empty($profile['boards'])): ?>
<h3 style="margin-top:20px">Boards</h3>
<?php foreach ($profile['boards'] as $b): ?>
<a href="/boards/<?= $b['id'] ?>" class="tag"><?= $__engine->e($b['name']) ?> (<?= $b['role_on_board'] ?>)</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if (isset($profile['phone_primary'])): ?>
<h3 style="margin-top:20px">Contact</h3>
<p>📱 <?= $__engine->e($profile['phone_primary']) ?></p>
<?php if (isset($profile['address'])): ?>
<p>📍 <?= $__engine->e($profile['address']) ?></p>
<?php endif; ?>
<?php endif; ?>
<?php if ($profile['activation_date']): ?>
<p class="text-muted">Active since: <?= date('M j, Y', strtotime($profile['activation_date'])) ?></p>
<?php endif; ?>
</div>
</div>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment