400 lines
20 KiB
PHP
400 lines
20 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db/config.php';
|
|
require_once __DIR__ . '/includes/auth_helper.php';
|
|
Auth::requireLogin();
|
|
|
|
$id = $_GET['id'] ?? null;
|
|
if (!$id) {
|
|
header('Location: employees.php');
|
|
exit;
|
|
}
|
|
|
|
$db = db();
|
|
|
|
// Fetch employee with user info
|
|
$employee = $db->prepare("
|
|
SELECT e.*, u.id as linked_user_id, u.email as user_email, u.last_login_at, u.welcome_email_sent_at
|
|
FROM employees e
|
|
LEFT JOIN users u ON e.user_id = u.id
|
|
WHERE e.id = ?
|
|
");
|
|
$employee->execute([$id]);
|
|
$employee = $employee->fetch();
|
|
|
|
if (!$employee) {
|
|
die("Employee not found.");
|
|
}
|
|
|
|
// Handle Welcome Email Request
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['send_welcome'])) {
|
|
require_once __DIR__ . '/mail/MailService.php';
|
|
|
|
$targetUser = null;
|
|
if ($employee['linked_user_id']) {
|
|
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
|
$stmt->execute([$employee['linked_user_id']]);
|
|
$targetUser = $stmt->fetch();
|
|
} else if ($employee['email']) {
|
|
// Create user if doesn't exist? For now let's assume we invite existing users or those with email
|
|
$stmt = $db->prepare("SELECT * FROM users WHERE email = ?");
|
|
$stmt->execute([$employee['email']]);
|
|
$targetUser = $stmt->fetch();
|
|
}
|
|
|
|
if (!$targetUser && $employee['email']) {
|
|
// Create user if missing
|
|
try {
|
|
$stmt = $db->prepare("INSERT INTO users (tenant_id, name, email, role, require_password_change) VALUES (?, ?, ?, 'staff', 1)");
|
|
$stmt->execute([$employee['tenant_id'], $employee['name'], $employee['email']]);
|
|
$newUserId = $db->lastInsertId();
|
|
|
|
// Link employee to user
|
|
$db->prepare("UPDATE employees SET user_id = ? WHERE id = ?")->execute([$newUserId, $id]);
|
|
|
|
// Fetch the new user
|
|
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
|
|
$stmt->execute([$newUserId]);
|
|
$targetUser = $stmt->fetch();
|
|
} catch (\Exception $e) {
|
|
$error_msg = "Could not create user account: " . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($targetUser) {
|
|
$token = bin2hex(random_bytes(32));
|
|
$expires = date('Y-m-d H:i:s', strtotime('+48 hours'));
|
|
|
|
$db->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)")
|
|
->execute([$targetUser['email'], $token, $expires]);
|
|
|
|
$setupLink = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]/reset_password.php?token=$token";
|
|
$subject = "Welcome to SR&ED Manager - Account Setup";
|
|
$html = "
|
|
<h3>Welcome to SR&ED Manager!</h3>
|
|
<p>Your account has been created. Click the button below to set your password and get started.</p>
|
|
<p><a href='$setupLink' style='padding: 10px 20px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 5px; display: inline-block;'>Set Up Account</a></p>
|
|
<p>This link will expire in 48 hours.</p>
|
|
";
|
|
$text = "Welcome! Set up your account here: $setupLink";
|
|
|
|
if (MailService::sendMail($targetUser['email'], $subject, $html, $text)) {
|
|
$db->prepare("UPDATE users SET welcome_email_sent_at = NOW() WHERE id = ?")
|
|
->execute([$targetUser['id']]);
|
|
$success_msg = "Welcome email sent to " . $targetUser['email'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$pageTitle = "Employee Detail: " . htmlspecialchars($employee['name']);
|
|
include __DIR__ . '/includes/header.php';
|
|
|
|
// Fetch recent sessions
|
|
$sessions = [];
|
|
if ($employee['linked_user_id']) {
|
|
$sessionStmt = $db->prepare("SELECT * FROM user_sessions WHERE user_id = ? ORDER BY login_at DESC LIMIT 5");
|
|
$sessionStmt->execute([$employee['linked_user_id']]);
|
|
$sessions = $sessionStmt->fetchAll();
|
|
}
|
|
|
|
// Fetch recent labour entries
|
|
$stmt = $db->prepare("
|
|
SELECT l.*, p.name as project_name, lt.name as labour_type
|
|
FROM labour_entries l
|
|
JOIN projects p ON l.project_id = p.id
|
|
JOIN labour_types lt ON l.labour_type_id = lt.id
|
|
WHERE l.employee_id = ?
|
|
ORDER BY l.entry_date DESC
|
|
LIMIT 10
|
|
");
|
|
$stmt->execute([$id]);
|
|
$labourEntries = $stmt->fetchAll();
|
|
|
|
// Fetch summary stats
|
|
$stats = $db->prepare("
|
|
SELECT
|
|
(SELECT SUM(hours) FROM labour_entries WHERE employee_id = ?) as total_hours,
|
|
(SELECT COUNT(DISTINCT project_id) FROM labour_entries WHERE employee_id = ?) as project_count,
|
|
(SELECT COUNT(*) FROM attachments a JOIN labour_entries le ON a.entity_id = le.id WHERE a.entity_type = 'labour_entry' AND le.employee_id = ?) as file_count
|
|
");
|
|
$stats->execute([$id, $id, $id]);
|
|
$stats = $stats->fetch();
|
|
|
|
// Fetch recent expenses (linked via attachments uploaded by this employee name)
|
|
$expenseStmt = $db->prepare("
|
|
SELECT ex.*, et.name as expense_type, s.name as supplier_name, p.name as project_name
|
|
FROM expenses ex
|
|
JOIN expense_types et ON ex.expense_type_id = et.id
|
|
LEFT JOIN suppliers s ON ex.supplier_id = s.id
|
|
JOIN projects p ON ex.project_id = p.id
|
|
JOIN attachments a ON a.entity_id = ex.id AND a.entity_type = 'expense'
|
|
WHERE a.uploaded_by = ?
|
|
ORDER BY ex.entry_date DESC
|
|
LIMIT 10
|
|
");
|
|
$expenseStmt->execute([$employee['name']]);
|
|
$expenseEntries = $expenseStmt->fetchAll();
|
|
|
|
// Fetch recent files (uploaded by this employee or linked to their labour)
|
|
$fileStmt = $db->prepare("
|
|
SELECT a.*,
|
|
COALESCE(le.entry_date, ex.entry_date) as entry_date,
|
|
COALESCE(p1.name, p2.name) as project_name
|
|
FROM attachments a
|
|
LEFT JOIN labour_entries le ON a.entity_id = le.id AND a.entity_type = 'labour_entry'
|
|
LEFT JOIN projects p1 ON le.project_id = p1.id
|
|
LEFT JOIN expenses ex ON a.entity_id = ex.id AND a.entity_type = 'expense'
|
|
LEFT JOIN projects p2 ON ex.project_id = p2.id
|
|
WHERE a.uploaded_by = ? OR (a.entity_type = 'labour_entry' AND le.employee_id = ?)
|
|
ORDER BY a.created_at DESC
|
|
LIMIT 10
|
|
");
|
|
$fileStmt->execute([$employee['name'], $id]);
|
|
$recentFiles = $fileStmt->fetchAll();
|
|
|
|
function formatBytes($bytes, $precision = 2) {
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
$bytes = max($bytes, 0);
|
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
|
$pow = min($pow, count($units) - 1);
|
|
$bytes /= pow(1024, $pow);
|
|
return round($bytes, $precision) . ' ' . $units[$pow];
|
|
}
|
|
?>
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-1">
|
|
<li class="breadcrumb-item"><a href="employees.php" class="text-decoration-none">Employees</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($employee['name']) ?></li>
|
|
</ol>
|
|
</nav>
|
|
<h1 class="h3 mb-0"><?= htmlspecialchars($employee['name']) ?></h1>
|
|
<p class="text-muted mb-0"><?= htmlspecialchars($employee['position'] ?? 'Staff') ?> • Joined <?= $employee['start_date'] ? date('M j, Y', strtotime($employee['start_date'])) : 'N/A' ?></p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<?php if (isset($success_msg)): ?>
|
|
<div class="alert alert-success py-1 px-2 mb-0 small"><?= $success_msg ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($employee['email'] || $employee['linked_user_id']): ?>
|
|
<form method="POST" class="d-inline">
|
|
<button type="submit" name="send_welcome" class="btn btn-outline-primary btn-sm">
|
|
<i class="bi bi-envelope-at me-1"></i> <?= $employee['welcome_email_sent_at'] ? 'Resend' : 'Send' ?> Welcome Email
|
|
</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<button class="btn btn-secondary btn-sm"><i class="bi bi-pencil me-1"></i> Edit Employee</button>
|
|
<a href="labour.php?employee_id=<?= $id ?>" class="btn btn-primary btn-sm"><i class="bi bi-plus-lg me-1"></i> Add Labour</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Total Hours</h6>
|
|
<h3 class="mb-0 text-primary"><?= number_format($stats['total_hours'] ?? 0, 1) ?></h3>
|
|
<small class="text-muted">Across all projects</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Projects</h6>
|
|
<h3 class="mb-0 text-success"><?= number_format($stats['project_count'] ?? 0) ?></h3>
|
|
<small class="text-muted">Involved in</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Files Uploaded</h6>
|
|
<h3 class="mb-0 text-info"><?= number_format($stats['file_count'] ?? 0) ?></h3>
|
|
<small class="text-muted">Labour & Expenses</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Email</h6>
|
|
<h3 class="h5 mb-0 text-dark text-truncate"><?= htmlspecialchars($employee['email'] ?? 'N/A') ?></h3>
|
|
<small class="text-muted">Contact Info</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Recent Labour -->
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0 fw-bold">Recent Labour Entries</h6>
|
|
<a href="labour.php?employee_id=<?= $id ?>" class="small text-decoration-none text-primary">View All</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-muted text-uppercase">
|
|
<th class="ps-3">Date</th>
|
|
<th>Project</th>
|
|
<th>Type</th>
|
|
<th class="text-end pe-3">Hours</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($labourEntries)): ?>
|
|
<tr><td colspan="4" class="text-center py-4 text-muted">No entries found.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($labourEntries as $le): ?>
|
|
<tr>
|
|
<td class="ps-3 small"><?= date('M j, Y', strtotime($le['entry_date'])) ?></td>
|
|
<td class="small"><a href="project_detail.php?id=<?= $le['project_id'] ?>" class="text-decoration-none"><?= htmlspecialchars($le['project_name']) ?></a></td>
|
|
<td class="small"><?= htmlspecialchars($le['labour_type']) ?></td>
|
|
<td class="text-end pe-3 fw-bold text-primary"><?= number_format($le['hours'], 1) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Expenses -->
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0 fw-bold">Recent Expenses Captured</h6>
|
|
<a href="expenses.php" class="small text-decoration-none text-primary">View All</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-muted text-uppercase">
|
|
<th class="ps-3">Date</th>
|
|
<th>Project</th>
|
|
<th>Type</th>
|
|
<th class="text-end pe-3">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($expenseEntries)): ?>
|
|
<tr><td colspan="4" class="text-center py-4 text-muted">No expenses captured by this employee.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($expenseEntries as $ee): ?>
|
|
<tr>
|
|
<td class="ps-3 small"><?= date('M j, Y', strtotime($ee['entry_date'])) ?></td>
|
|
<td class="small"><a href="project_detail.php?id=<?= $ee['project_id'] ?>" class="text-decoration-none text-truncate d-inline-block" style="max-width: 120px;"><?= htmlspecialchars($ee['project_name']) ?></a></td>
|
|
<td class="small"><?= htmlspecialchars($ee['expense_type']) ?></td>
|
|
<td class="text-end pe-3 fw-bold text-success">$<?= number_format($ee['amount'], 2) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Files -->
|
|
<div class="col-md-8">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0 fw-bold">Recent Files & Attachments</h6>
|
|
<a href="files.php" class="small text-decoration-none text-primary">View All Files</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 align-middle">
|
|
<thead class="bg-light">
|
|
<tr class="small text-muted text-uppercase">
|
|
<th class="ps-3">Filename</th>
|
|
<th>Linked To</th>
|
|
<th>Size</th>
|
|
<th>Date</th>
|
|
<th class="text-end pe-3">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($recentFiles)): ?>
|
|
<tr><td colspan="5" class="text-center py-4 text-muted">No files found.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($recentFiles as $f): ?>
|
|
<tr>
|
|
<td class="ps-3">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-file-earmark-text me-2 text-primary"></i>
|
|
<span class="small fw-bold text-dark text-truncate d-inline-block" style="max-width: 150px;"><?= htmlspecialchars($f['file_name']) ?></span>
|
|
</div>
|
|
</td>
|
|
<td class="small">
|
|
<span class="badge bg-light text-dark border"><?= ucfirst($f['entity_type']) ?></span>
|
|
</td>
|
|
<td class="small text-muted"><?= formatBytes((int)$f['file_size']) ?></td>
|
|
<td class="small text-muted"><?= date('M j, Y', strtotime($f['created_at'])) ?></td>
|
|
<td class="text-end pe-3">
|
|
<a href="<?= htmlspecialchars($f['file_path']) ?>" target="_blank" class="btn btn-sm btn-primary py-0 px-2">View</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Sessions -->
|
|
<div class="col-md-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white py-3">
|
|
<h6 class="mb-0 fw-bold">Recent Sessions</h6>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="extra-small text-muted text-uppercase">
|
|
<th class="ps-3">Login Time</th>
|
|
<th>IP / Country</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($sessions)): ?>
|
|
<tr><td colspan="2" class="text-center py-4 text-muted small">No session history found.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($sessions as $s): ?>
|
|
<tr>
|
|
<td class="ps-3 small">
|
|
<div class="fw-bold"><?= date('M j, H:i', strtotime($s['login_at'])) ?></div>
|
|
<div class="extra-small text-muted"><?= date('Y', strtotime($s['login_at'])) ?></div>
|
|
</td>
|
|
<td class="small">
|
|
<div><?= htmlspecialchars($s['ip_address']) ?></div>
|
|
<div class="extra-small text-muted"><i class="bi bi-geo-alt me-1"></i><?= htmlspecialchars($s['country'] ?? 'Unknown') ?></div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include __DIR__ . '/includes/footer.php'; ?>
|