Compare commits

...

19 Commits

Author SHA1 Message Date
Flatlogic Bot
9ff74420bd login update 2026-02-15 18:07:08 +00:00
Flatlogic Bot
36b8744143 Password changes 2026-02-15 17:59:20 +00:00
Flatlogic Bot
c8a94f29c5 company settings update 2026-02-15 17:29:16 +00:00
Flatlogic Bot
a2a8ca4262 import updated 2026-02-15 16:55:38 +00:00
Flatlogic Bot
5ba9886549 Import functions 2026-02-15 16:51:04 +00:00
Flatlogic Bot
0f6b05982a thumbnail improvement 2026-02-15 16:33:12 +00:00
Flatlogic Bot
cc5d6146bf file upload changes 2026-02-15 16:28:14 +00:00
Flatlogic Bot
b56a9a8592 Autosave: 20260215-155851 2026-02-15 15:58:51 +00:00
Flatlogic Bot
037f8c27be update to filtering 2026-02-15 15:54:39 +00:00
Flatlogic Bot
353a0f1f3b employee detail page 2026-02-15 15:19:05 +00:00
Flatlogic Bot
cb637ac2fc labour overhaul 2026-02-15 02:14:10 +00:00
Flatlogic Bot
33ad9419e5 reorg 2026-02-15 01:43:55 +00:00
Flatlogic Bot
87b38e5dfa Autosave: 20260215-013327 2026-02-15 01:33:27 +00:00
Flatlogic Bot
2c1612942a menu 2026-02-15 01:22:25 +00:00
Flatlogic Bot
bb0b464262 report update 2026-02-15 01:10:00 +00:00
Flatlogic Bot
444cd73cc6 reports 2026-02-15 01:02:26 +00:00
Flatlogic Bot
10ea0441d5 Autosave: 20260215-003353 2026-02-15 00:33:53 +00:00
Flatlogic Bot
f03a7a8de5 employees 2026-02-15 00:26:46 +00:00
Flatlogic Bot
a2a711f887 added labour 2026-02-15 00:18:02 +00:00
52 changed files with 7064 additions and 145 deletions

57
api/export_expenses.php Normal file
View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
$tenant_id = 1;
$filter_project = (int)($_GET['project_id'] ?? 0);
$filter_supplier = (int)($_GET['supplier_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$where = ["e.tenant_id = ?"];
$params = [$tenant_id];
if ($filter_project) {
$where[] = "e.project_id = ?";
$params[] = $filter_project;
}
if ($filter_supplier) {
$where[] = "e.supplier_id = ?";
$params[] = $filter_supplier;
}
if ($filter_start) {
$where[] = "e.entry_date >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "e.entry_date <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$stmt = db()->prepare("
SELECT e.entry_date, s.name as supplier_name, p.name as project_name, e.amount, e.allocation_percent, et.name as expense_type, e.notes
FROM expenses e
JOIN projects p ON e.project_id = p.id
JOIN suppliers s ON e.supplier_id = s.id
LEFT JOIN expense_types et ON e.expense_type_id = et.id
WHERE $where_clause
ORDER BY e.entry_date DESC
");
$stmt->execute($params);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$filename_suffix = date('Y-m-d_H-i-s');
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=expenses_export_' . $filename_suffix . '.csv');
$output = fopen('php://output', 'w');
fputcsv($output, ['Date', 'Supplier', 'Project', 'Amount', 'Allocation %', 'Expense Type', 'Notes']);
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;

79
api/export_labour.php Normal file
View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
$tenant_id = 1;
$filter_project = $_GET['project_id'] ?? null;
$filter_projects = $_GET['projects'] ?? []; // Array from reports.php
$filter_employee = (int)($_GET['employee_id'] ?? 0);
$filter_type = (int)($_GET['labour_type_id'] ?? ($_GET['activity_type_id'] ?? 0));
$filter_evidence = (int)($_GET['evidence_type_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$filter_team = (int)($_GET['team_id'] ?? 0);
$where = ["le.tenant_id = ?"];
$params = [$tenant_id];
if ($filter_project) {
$where[] = "le.project_id = ?";
$params[] = (int)$filter_project;
}
if (!empty($filter_projects)) {
$placeholders = implode(',', array_fill(0, count($filter_projects), '?'));
$where[] = "le.project_id IN ($placeholders)";
foreach ($filter_projects as $pid) $params[] = (int)$pid;
}
if ($filter_employee) {
$where[] = "le.employee_id = ?";
$params[] = $filter_employee;
}
if ($filter_team) {
$where[] = "le.employee_id IN (SELECT employee_id FROM employee_teams WHERE team_id = ?)";
$params[] = $filter_team;
}
if ($filter_type) {
$where[] = "le.labour_type_id = ?";
$params[] = $filter_type;
}
if ($filter_evidence) {
$where[] = "le.evidence_type_id = ?";
$params[] = $filter_evidence;
}
if ($filter_start) {
$where[] = "le.entry_date >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "le.entry_date <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$stmt = db()->prepare("
SELECT le.entry_date, e.name as employee_name, p.name as project_name, le.hours, lt.name as labour_type, et.name as evidence_type, le.notes
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
JOIN employees e ON le.employee_id = e.id
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
LEFT JOIN evidence_types et ON le.evidence_type_id = et.id
WHERE $where_clause
ORDER BY le.entry_date DESC
");
$stmt->execute($params);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$filename_suffix = date('Y-m-d_H-i-s');
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=labour_export_' . $filename_suffix . '.csv');
$output = fopen('php://output', 'w');
fputcsv($output, ['Date', 'Employee', 'Project', 'Hours', 'Labour Type', 'Evidence Type', 'Notes']);
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;

26
api/get_labour_stats.php Normal file
View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$employee_id = (int)($_GET['employee_id'] ?? 0);
$start_date = $_GET['start_date'] ?? '';
$end_date = $_GET['end_date'] ?? '';
if (!$employee_id || !$start_date || !$end_date) {
echo json_encode([]);
exit;
}
try {
$db = db();
$stmt = $db->prepare("
SELECT entry_date, SUM(hours) as total_hours
FROM labour_entries
WHERE employee_id = ? AND entry_date BETWEEN ? AND ?
GROUP BY entry_date
");
$stmt->execute([$employee_id, $start_date, $end_date]);
echo json_encode($stmt->fetchAll());
} catch (Exception $e) {
echo json_encode(['error' => $e->getMessage()]);
}

49
api/search.php Normal file
View File

@ -0,0 +1,49 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode([]);
exit;
}
$results = [];
try {
$db = db();
// Search Projects
$stmt = $db->prepare("SELECT id, name, code, is_archived FROM projects WHERE name LIKE ? OR code LIKE ? LIMIT 5");
$stmt->execute(['%' . $q . '%', '%' . $q . '%']);
foreach ($stmt->fetchAll() as $row) {
$label = $row['name'] . ' (' . $row['code'] . ')';
if ($row['is_archived']) {
$label .= ' [ARCHIVED]';
}
$results[] = [
'type' => 'Project',
'id' => $row['id'],
'label' => $label,
'url' => 'project_detail.php?id=' . $row['id']
];
}
// Search Employees
$stmt = $db->prepare("SELECT id, name, position FROM employees WHERE name LIKE ? OR first_name LIKE ? OR last_name LIKE ? LIMIT 5");
$stmt->execute(['%' . $q . '%', '%' . $q . '%', '%' . $q . '%']);
foreach ($stmt->fetchAll() as $row) {
$results[] = [
'type' => 'Employee',
'id' => $row['id'],
'label' => $row['name'] . ' - ' . ($row['position'] ?? 'Staff'),
'url' => 'employee_detail.php?id=' . $row['id']
];
}
} catch (Exception $e) {
// Silence error for now
}
echo json_encode($results);

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

@ -0,0 +1,252 @@
body {
background-color: #f8fafc;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #0f172a;
font-size: 0.875rem;
}
.navbar {
background-color: #ffffff;
border-bottom: 1px solid #e2e8f0;
padding: 0.75rem 1.5rem;
}
.navbar-brand {
font-weight: 700;
color: #0f172a;
letter-spacing: -0.025em;
}
.nav-link {
color: #475569;
font-weight: 500;
}
.nav-link:hover {
color: #3b82f6;
}
.dropdown-menu {
border: 1px solid #e2e8f0;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-radius: 0.375rem;
padding: 0.5rem;
}
.dropdown-item {
border-radius: 0.25rem;
padding: 0.5rem 0.75rem;
color: #475569;
}
.dropdown-item:hover {
background-color: #f1f5f9;
color: #3b82f6;
}
.card {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.card-header {
background-color: #ffffff;
border-bottom: 1px solid #e2e8f0;
padding: 1rem 1.5rem;
font-weight: 600;
}
.btn-primary {
background-color: #3b82f6;
border-color: #3b82f6;
color: #ffffff !important;
font-weight: 500;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
.btn-primary.active,
.btn-primary:focus-visible {
background-color: #2563eb;
border-color: #2563eb;
color: #ffffff !important;
box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.5);
outline: none;
}
.nav-link.active {
color: #3b82f6 !important;
}
.navbar-nav .nav-link.active {
border-bottom: 2px solid #3b82f6;
}
.nav-pills .nav-link.active,
.nav-pills .nav-link.active:hover,
.nav-pills .nav-link.active:focus,
.nav-pills .nav-link.active:active {
background-color: #3b82f6 !important;
color: #ffffff !important;
border-bottom: none !important;
box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25);
}
.btn-check:checked + .btn-outline-primary,
.btn-check:active + .btn-outline-primary,
.btn-outline-primary:active,
.btn-outline-primary.active,
.btn-outline-primary.dropdown-toggle.show {
color: #ffffff !important;
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.table {
margin-bottom: 0;
}
.table th {
background-color: #f8fafc;
color: #64748b;
text-transform: uppercase;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
padding: 0.75rem 1.5rem;
border-top: none;
}
.table td {
padding: 1rem 1.5rem;
vertical-align: middle;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.status-active { background-color: #dcfce7; color: #166534; }
.status-on-hold { background-color: #fef9c3; color: #854d0e; }
.status-completed { background-color: #dbeafe; color: #1e40af; }
.activity-feed {
list-style: none;
padding: 0;
}
.activity-item {
padding: 0.75rem 0;
border-bottom: 1px solid #f1f5f9;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-time {
font-size: 0.75rem;
color: #94a3b8;
}
/* Filter Sidebar Styles */
.filter-sidebar {
transition: all 0.3s ease;
width: 300px;
position: relative;
}
.filter-sidebar.collapsed {
width: 0;
overflow: hidden;
padding: 0;
margin: 0;
border: none;
opacity: 0;
}
.filter-toggle-btn {
position: absolute;
left: -40px;
top: 10px;
z-index: 10;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 4px 0 0 4px;
padding: 5px 10px;
cursor: pointer;
box-shadow: -2px 0 5px rgba(0,0,0,0.05);
}
.filter-card {
position: sticky;
top: 90px;
}
.extra-small {
font-size: 0.7rem;
}
/* Calendar Styles */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: #e2e8f0;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
overflow: hidden;
}
.calendar-day {
background-color: #ffffff;
min-height: 100px;
padding: 0.75rem;
position: relative;
transition: background-color 0.2s;
}
.calendar-day:hover {
background-color: #f8fafc;
}
.calendar-header-day {
background-color: #f1f5f9;
text-align: center;
padding: 0.75rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
color: #64748b;
letter-spacing: 0.05em;
}
.day-number {
font-weight: 600;
color: #94a3b8;
font-size: 0.875rem;
}
.day-hours {
position: absolute;
bottom: 0.75rem;
right: 0.75rem;
font-size: 1.125rem;
font-weight: 700;
color: #3b82f6;
}
.other-month {
background-color: #f8fafc;
}
.other-month .day-number {
color: #e2e8f0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

266
company_settings.php Normal file
View File

@ -0,0 +1,266 @@
<?php
/**
* Company Preferences - Identity & Settings
*/
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$success = false;
$error = '';
// Canadian Provinces
$provinces = [
'AB' => 'Alberta', 'BC' => 'British Columbia', 'MB' => 'Manitoba',
'NB' => 'New Brunswick', 'NL' => 'Newfoundland and Labrador', 'NS' => 'Nova Scotia',
'ON' => 'Ontario', 'PE' => 'Prince Edward Island', 'QC' => 'Quebec',
'SK' => 'Saskatchewan', 'NT' => 'Northwest Territories', 'NU' => 'Nunavut', 'YT' => 'Yukon'
];
// Business Sectors
$sectors = [
'Telecommunications', 'Information Technology', 'Professional Services',
'Manufacturing', 'Construction', 'Retail', 'Healthcare', 'Energy', 'Other'
];
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$company_name = $_POST['company_name'] ?? '';
$address_1 = $_POST['address_1'] ?? '';
$address_2 = $_POST['address_2'] ?? '';
$city = $_POST['city'] ?? '';
$province = $_POST['province'] ?? '';
$postal_code = $_POST['postal_code'] ?? '';
$phone = $_POST['phone'] ?? '';
$phone_2 = $_POST['phone_2'] ?? '';
$email = $_POST['email'] ?? '';
$website = $_POST['website'] ?? '';
$fiscal_year_end = $_POST['fiscal_year_end'] ?: null;
$business_number = $_POST['business_number'] ?? '';
$timezone = $_POST['timezone'] ?? '';
$sector = $_POST['sector'] ?? '';
$notifications_enabled = isset($_POST['notifications_enabled']) ? 1 : 0;
// Handle Logo Upload
$logo_path = $_POST['current_logo'] ?? '';
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$upload_dir = __DIR__ . '/assets/images/logo/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0775, true);
}
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$filename = 'company_logo_' . time() . '.' . $ext;
$target = $upload_dir . $filename;
if (move_uploaded_file($_FILES['logo']['tmp_name'], $target)) {
$logo_path = 'assets/images/logo/' . $filename;
}
}
try {
$stmt = db()->prepare("
UPDATE company_settings
SET company_name = ?, address_1 = ?, address_2 = ?, city = ?, province = ?,
postal_code = ?, phone = ?, phone_2 = ?, email = ?, website = ?,
fiscal_year_end = ?, business_number = ?, timezone = ?, sector = ?,
notifications_enabled = ?, logo_path = ?
WHERE id = 1
");
$stmt->execute([
$company_name, $address_1, $address_2, $city, $province,
$postal_code, $phone, $phone_2, $email, $website,
$fiscal_year_end, $business_number, $timezone, $sector,
$notifications_enabled, $logo_path
]);
$success = true;
} catch (PDOException $e) {
$error = "Error updating settings: " . $e->getMessage();
}
}
// Fetch Current Settings
$settings = db()->query("SELECT * FROM company_settings WHERE id = 1")->fetch();
$pageTitle = "SR&ED Manager - Company Preferences";
include __DIR__ . '/includes/header.php';
?>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Company Preferences</h2>
<p class="text-muted">Manage your business identity and application-wide settings.</p>
</div>
<a href="settings.php" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i> Back to Settings
</a>
</div>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> Settings updated successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= htmlspecialchars($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="row g-4">
<input type="hidden" name="current_logo" value="<?= htmlspecialchars($settings['logo_path'] ?? '') ?>">
<!-- Identity Card -->
<div class="col-12">
<div class="card border-0 shadow-sm overflow-hidden">
<div class="card-header bg-white py-3 border-bottom">
<h5 class="mb-0 fw-bold">Company Identity</h5>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label small fw-bold">Company Legal Name</label>
<input type="text" name="company_name" class="form-control" value="<?= htmlspecialchars($settings['company_name'] ?? '') ?>" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Sector</label>
<select name="sector" class="form-select">
<option value="">Select Sector...</option>
<?php foreach ($sectors as $s): ?>
<option value="<?= $s ?>" <?= ($settings['sector'] ?? '') === $s ? 'selected' : '' ?>><?= $s ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Business Number (CRA)</label>
<input type="text" name="business_number" class="form-control" value="<?= htmlspecialchars($settings['business_number'] ?? '') ?>" placeholder="e.g. 123456789 RT 0001">
</div>
</div>
</div>
<div class="col-md-4 text-center">
<label class="form-label small fw-bold d-block">Company Logo</label>
<div class="mb-2">
<?php if (!empty($settings['logo_path'])): ?>
<img src="<?= htmlspecialchars($settings['logo_path']) ?>" alt="Logo" class="img-thumbnail mb-2" style="max-height: 120px;">
<?php else: ?>
<div class="bg-light rounded d-flex align-items-center justify-content-center mx-auto mb-2" style="width: 120px; height: 120px;">
<i class="bi bi-image text-muted fs-1"></i>
</div>
<?php endif; ?>
</div>
<input type="file" name="logo" class="form-control form-control-sm" accept="image/*">
<p class="extra-small text-muted mt-2">Recommended: PNG or SVG with transparent background.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Contact & Address Card -->
<div class="col-md-7">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 border-bottom">
<h5 class="mb-0 fw-bold">Address & Contact Info</h5>
</div>
<div class="card-body p-4">
<div class="mb-3">
<label class="form-label small fw-bold">Address Line 1</label>
<input type="text" name="address_1" class="form-control" value="<?= htmlspecialchars($settings['address_1'] ?? '') ?>" placeholder="Street address, P.O. box, company name, c/o">
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Address Line 2 (Optional)</label>
<input type="text" name="address_2" class="form-control" value="<?= htmlspecialchars($settings['address_2'] ?? '') ?>" placeholder="Apartment, suite, unit, building, floor, etc.">
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-bold">City</label>
<input type="text" name="city" class="form-control" value="<?= htmlspecialchars($settings['city'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Province</label>
<select name="province" class="form-select">
<option value="">--</option>
<?php foreach ($provinces as $code => $name): ?>
<option value="<?= $code ?>" <?= ($settings['province'] ?? '') === $code ? 'selected' : '' ?>><?= $code ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Postal Code</label>
<input type="text" name="postal_code" class="form-control" value="<?= htmlspecialchars($settings['postal_code'] ?? '') ?>">
</div>
</div>
<hr class="my-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold">Primary Phone</label>
<input type="tel" name="phone" class="form-control" value="<?= htmlspecialchars($settings['phone'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Secondary Phone</label>
<input type="tel" name="phone_2" class="form-control" value="<?= htmlspecialchars($settings['phone_2'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Email Address</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($settings['email'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold">Website</label>
<input type="url" name="website" class="form-control" value="<?= htmlspecialchars($settings['website'] ?? '') ?>" placeholder="https://">
</div>
</div>
</div>
</div>
</div>
<!-- Operations Card -->
<div class="col-md-5">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3 border-bottom">
<h5 class="mb-0 fw-bold">Operations</h5>
</div>
<div class="card-body p-4">
<div class="mb-4">
<label class="form-label small fw-bold text-primary">Fiscal Year End</label>
<input type="date" name="fiscal_year_end" class="form-control border-primary shadow-sm" value="<?= htmlspecialchars($settings['fiscal_year_end'] ?? '') ?>">
<p class="extra-small text-muted mt-2">Important: This date is used for tax reporting and financial summaries.</p>
</div>
<div class="mb-4">
<label class="form-label small fw-bold">Application Timezone</label>
<select name="timezone" class="form-select">
<option value="America/Toronto" <?= ($settings['timezone'] ?? '') === 'America/Toronto' ? 'selected' : '' ?>>Eastern Time (ET)</option>
<option value="America/Winnipeg" <?= ($settings['timezone'] ?? '') === 'America/Winnipeg' ? 'selected' : '' ?>>Central Time (CT)</option>
<option value="America/Edmonton" <?= ($settings['timezone'] ?? '') === 'America/Edmonton' ? 'selected' : '' ?>>Mountain Time (MT)</option>
<option value="America/Vancouver" <?= ($settings['timezone'] ?? '') === 'America/Vancouver' ? 'selected' : '' ?>>Pacific Time (PT)</option>
<option value="America/Halifax" <?= ($settings['timezone'] ?? '') === 'America/Halifax' ? 'selected' : '' ?>>Atlantic Time (AT)</option>
<option value="America/St_Johns" <?= ($settings['timezone'] ?? '') === 'America/St_Johns' ? 'selected' : '' ?>>Newfoundland Time (NT)</option>
</select>
</div>
<div class="form-check form-switch p-3 bg-light rounded shadow-sm border">
<input class="form-check-input ms-0 me-2" type="checkbox" name="notifications_enabled" id="notifSwitch" <?= ($settings['notifications_enabled'] ?? 1) ? 'checked' : '' ?>>
<label class="form-check-label fw-bold small" for="notifSwitch">Enable Company-wide Notifications</label>
<p class="extra-small text-muted mb-0 mt-1">Global toggle for email alerts and system notifications.</p>
</div>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg shadow-sm py-3 fw-bold rounded-3">
<i class="bi bi-save me-2"></i> Save All Changes
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

View File

@ -0,0 +1,43 @@
-- Initial Schema for SR&ED Manager
CREATE TABLE IF NOT EXISTS tenants (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
role ENUM('global_admin', 'tenant_admin', 'staff') DEFAULT 'staff',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS projects (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
code VARCHAR(50) NOT NULL,
status ENUM('active', 'completed', 'on_hold') DEFAULT 'active',
start_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS activity_log (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
user_id INT,
action VARCHAR(255) NOT NULL,
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Seed Initial Demo Data
INSERT IGNORE INTO tenants (id, name) VALUES (1, 'Acme Research Corp');
INSERT IGNORE INTO users (id, tenant_id, name, email, role) VALUES (1, 1, 'John Manager', 'john@acme.com', 'tenant_admin');
INSERT IGNORE INTO projects (tenant_id, name, code, start_date) VALUES (1, 'Project Alpha: Quantum AI', 'PA-001', '2025-01-01');
INSERT IGNORE INTO activity_log (tenant_id, user_id, action, details) VALUES (1, 1, 'System Setup', 'Initial tenant environment created.');

View File

@ -0,0 +1,74 @@
-- Migration for Labour Module and Centralized Attachments
-- Table for Employees
CREATE TABLE IF NOT EXISTS employees (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
position VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Table for Labour Types (e.g., Experimental Development, Technical Support)
CREATE TABLE IF NOT EXISTS labour_types (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Table for Objective Evidence Types (e.g., Logbooks, Test Results, Design Docs)
CREATE TABLE IF NOT EXISTS evidence_types (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Table for Labour Entries
CREATE TABLE IF NOT EXISTS labour_entries (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
project_id INT NOT NULL,
employee_id INT NOT NULL,
entry_date DATE NOT NULL,
hours DECIMAL(10, 2) NOT NULL,
labour_type_id INT,
evidence_type_id INT,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (labour_type_id) REFERENCES labour_types(id) ON DELETE SET NULL,
FOREIGN KEY (evidence_type_id) REFERENCES evidence_types(id) ON DELETE SET NULL
);
-- Centralized Attachments Table
CREATE TABLE IF NOT EXISTS attachments (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
entity_type VARCHAR(50) NOT NULL, -- 'labour_entry', 'project', 'expense', etc.
entity_id INT NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(255) NOT NULL,
file_size INT,
mime_type VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Seed some initial settings for the demo tenant
INSERT IGNORE INTO employees (tenant_id, name, position) VALUES (1, 'Alice Smith', 'Lead Researcher');
INSERT IGNORE INTO employees (tenant_id, name, position) VALUES (1, 'Bob Jones', 'Developer');
INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Experimental Development');
INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Technical Support');
INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Technical Planning');
INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Logbooks');
INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Test Results');
INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Design Documents');
INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Source Code Commits');

View File

@ -0,0 +1,48 @@
-- Table for Suppliers/Contractors
CREATE TABLE IF NOT EXISTS suppliers (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
type ENUM('supplier', 'contractor') DEFAULT 'supplier',
contact_info TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id)
);
-- Table for Expense Types (Company editable list)
CREATE TABLE IF NOT EXISTS expense_types (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id)
);
-- Table for Expenses
CREATE TABLE IF NOT EXISTS expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
project_id INT NOT NULL,
supplier_id INT NOT NULL,
expense_type_id INT NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
allocation_percent DECIMAL(5, 2) DEFAULT 100.00,
entry_date DATE NOT NULL,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id),
FOREIGN KEY (supplier_id) REFERENCES suppliers(id),
FOREIGN KEY (expense_type_id) REFERENCES expense_types(id),
INDEX (tenant_id)
);
-- Insert some default data for the demo tenant (tenant_id = 1)
INSERT INTO suppliers (tenant_id, name, type, contact_info) VALUES
(1, 'Cloud Services Inc.', 'supplier', 'billing@cloudservices.com'),
(1, 'John Doe Consulting', 'contractor', 'john@doe.com');
INSERT INTO expense_types (tenant_id, name) VALUES
(1, 'Materials'),
(1, 'Subcontractors'),
(1, 'Software Licenses'),
(1, 'Overhead');

View File

@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS company_settings (
id INT PRIMARY KEY DEFAULT 1,
company_name VARCHAR(255),
address_1 VARCHAR(255),
address_2 VARCHAR(255),
city VARCHAR(100),
province VARCHAR(100),
postal_code VARCHAR(20),
phone VARCHAR(20),
phone_2 VARCHAR(20),
email VARCHAR(255),
website VARCHAR(255),
fiscal_year_end DATE,
business_number VARCHAR(100),
timezone VARCHAR(100),
sector VARCHAR(100),
notifications_enabled TINYINT(1) DEFAULT 1,
logo_path VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT single_row CHECK (id = 1)
) ENGINE=InnoDB;
-- Initialize with default values if not exists
INSERT IGNORE INTO company_settings (id, company_name) VALUES (1, 'My ERP Company');

View File

@ -0,0 +1,50 @@
-- Migration for Enhanced Employee Management
-- 1. Alter employees table to add required fields
ALTER TABLE employees ADD COLUMN first_name VARCHAR(100) AFTER tenant_id;
ALTER TABLE employees ADD COLUMN last_name VARCHAR(100) AFTER first_name;
ALTER TABLE employees ADD COLUMN start_date DATE AFTER position;
ALTER TABLE employees ADD COLUMN is_limited BOOLEAN DEFAULT TRUE AFTER start_date;
ALTER TABLE employees ADD COLUMN user_id INT NULL AFTER is_limited;
ALTER TABLE employees ADD CONSTRAINT fk_employee_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
-- Migrate existing names to first/last (rough split)
UPDATE employees SET first_name = SUBSTRING_INDEX(name, ' ', 1), last_name = SUBSTRING_INDEX(name, ' ', -1) WHERE name LIKE '% %';
UPDATE employees SET first_name = name, last_name = '' WHERE name NOT LIKE '% %';
-- 2. Wage History Table
CREATE TABLE IF NOT EXISTS employee_wages (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
employee_id INT NOT NULL,
hourly_rate DECIMAL(10, 2) NOT NULL,
effective_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
);
-- 3. Teams Table
CREATE TABLE IF NOT EXISTS teams (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- 4. Employee-Team Link (Many-to-Many)
CREATE TABLE IF NOT EXISTS employee_teams (
employee_id INT NOT NULL,
team_id INT NOT NULL,
tenant_id INT NOT NULL,
PRIMARY KEY (employee_id, team_id),
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
);
-- Seed some teams
INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'Engineering');
INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'R&D');
INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'Product Management');

View File

@ -0,0 +1,16 @@
-- Add hourly_rate to labour_entries to capture wage at time of entry
ALTER TABLE labour_entries ADD COLUMN hourly_rate DECIMAL(10, 2) DEFAULT NULL AFTER hours;
-- Backfill hourly_rate from employee_wages
UPDATE labour_entries le
JOIN (
SELECT ew1.employee_id, ew1.hourly_rate, ew1.effective_date
FROM employee_wages ew1
WHERE ew1.effective_date = (
SELECT MAX(ew2.effective_date)
FROM employee_wages ew2
WHERE ew2.employee_id = ew1.employee_id
)
) latest_wage ON le.employee_id = latest_wage.employee_id
SET le.hourly_rate = latest_wage.hourly_rate
WHERE le.hourly_rate IS NULL;

View File

@ -0,0 +1,7 @@
-- Add new fields to projects table
ALTER TABLE projects
ADD COLUMN owner_id INT NULL AFTER tenant_id,
ADD COLUMN estimated_completion_date DATE NULL AFTER start_date,
ADD COLUMN type ENUM('Internal', 'SRED') NOT NULL DEFAULT 'Internal' AFTER code,
ADD COLUMN estimated_hours DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER type,
ADD CONSTRAINT fk_project_owner FOREIGN KEY (owner_id) REFERENCES employees(id) ON DELETE SET NULL;

View File

@ -0,0 +1,27 @@
-- Create password_resets table
CREATE TABLE IF NOT EXISTS password_resets (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (email),
INDEX (token)
);
-- Create user_sessions table
CREATE TABLE IF NOT EXISTS user_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
ip_address VARCHAR(45) NOT NULL,
country VARCHAR(100) DEFAULT NULL,
user_agent TEXT DEFAULT NULL,
login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Add tracking columns to users
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at DATETIME DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_ip VARCHAR(45) DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS welcome_email_sent_at DATETIME DEFAULT NULL;

View File

@ -0,0 +1,15 @@
-- Create clients table
CREATE TABLE IF NOT EXISTS clients (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
phone VARCHAR(50),
address TEXT,
city VARCHAR(100),
province_state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

399
employee_detail.php Normal file
View File

@ -0,0 +1,399 @@
<?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'; ?>

262
employees.php Normal file
View File

@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Add Employee
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
$first_name = $_POST['first_name'] ?? '';
$last_name = $_POST['last_name'] ?? '';
$email = $_POST['email'] ?? '';
$position = $_POST['position'] ?? '';
$start_date = $_POST['start_date'] ?? date('Y-m-d');
$is_limited = isset($_POST['is_limited']) ? 1 : 0;
$phone = $_POST['phone'] ?? '';
$password = $_POST['password'] ?? '';
$force_password_change = isset($_POST['force_password_change']) ? 1 : 0;
$initial_wage = (float)($_POST['initial_wage'] ?? 0);
$team_ids = $_POST['teams'] ?? [];
if ($first_name && $last_name) {
$user_id = null;
if (!$is_limited && $email) {
$hashed_password = $password ? password_hash($password, PASSWORD_DEFAULT) : null;
$stmt = db()->prepare("INSERT INTO users (tenant_id, name, email, phone, password, require_password_change, role)
VALUES (?, ?, ?, ?, ?, ?, 'staff')
ON DUPLICATE KEY UPDATE
phone = VALUES(phone),
password = COALESCE(VALUES(password), password),
require_password_change = VALUES(require_password_change)");
$stmt->execute([$tenant_id, "$first_name $last_name", $email, $phone, $hashed_password, $force_password_change]);
$stmt = db()->prepare("SELECT id FROM users WHERE email = ? AND tenant_id = ?");
$stmt->execute([$email, $tenant_id]);
$user_id = (int)($stmt->fetchColumn() ?: null);
}
$stmt = db()->prepare("INSERT INTO employees (tenant_id, first_name, last_name, email, phone, position, start_date, is_limited, user_id, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $first_name, $last_name, $email, $phone, $position, $start_date, $is_limited, $user_id, "$first_name $last_name"]);
$employee_id = (int)db()->lastInsertId();
if ($initial_wage > 0) {
$stmt = db()->prepare("INSERT INTO employee_wages (tenant_id, employee_id, hourly_rate, effective_date) VALUES (?, ?, ?, ?)");
$stmt->execute([$tenant_id, $employee_id, $initial_wage, $start_date]);
}
if (!empty($team_ids)) {
foreach ($team_ids as $tid) {
$stmt = db()->prepare("INSERT INTO employee_teams (tenant_id, employee_id, team_id) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, $employee_id, $tid]);
}
}
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Employee Created', "Added employee: $first_name $last_name"]);
header("Location: employees.php?success=1");
exit;
}
}
// Fetch Data
$stmt = db()->prepare("SELECT pref_key, pref_value FROM system_preferences WHERE tenant_id = ?");
$stmt->execute([$tenant_id]);
$prefs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$employees = db()->prepare("
SELECT e.*,
(SELECT hourly_rate FROM employee_wages WHERE employee_id = e.id ORDER BY effective_date DESC LIMIT 1) as current_wage
FROM employees e
WHERE e.tenant_id = ?
ORDER BY e.first_name, e.last_name
");
$employees->execute([$tenant_id]);
$employeeList = $employees->fetchAll();
$teams = db()->prepare("SELECT * FROM teams WHERE tenant_id = ? ORDER BY name");
$teams->execute([$tenant_id]);
$teamList = $teams->fetchAll();
$pageTitle = "SR&ED Manager - Employees";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Employees</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addEmployeeModal">+ New Employee</button>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Employee record successfully created.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Name</th>
<th>Position</th>
<th>Teams</th>
<th>Wage</th>
<th>Access</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($employeeList)): ?>
<tr><td colspan="6" class="text-center py-5 text-muted">No employees found.</td></tr>
<?php endif; ?>
<?php foreach ($employeeList as $e): ?>
<tr>
<td>
<a href="employee_detail.php?id=<?= $e['id'] ?>" class="text-decoration-none fw-bold text-dark">
<?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?>
</a>
</td>
<td class="small text-muted"><?= htmlspecialchars($e['position']) ?></td>
<td>
<?php
$e_teams = db()->prepare("SELECT t.name FROM teams t JOIN employee_teams et ON t.id = et.team_id WHERE et.employee_id = ?");
$e_teams->execute([$e['id']]);
$t_names = $e_teams->fetchAll(PDO::FETCH_COLUMN);
foreach ($t_names as $tn) {
echo '<span class="badge bg-light text-dark border me-1">' . htmlspecialchars($tn) . '</span>';
}
?>
</td>
<td><span class="fw-bold text-success">$<?= number_format((float)($e['current_wage'] ?? 0), 2) ?>/h</span></td>
<td><span class="badge <?= $e['is_limited'] ? 'bg-secondary' : 'bg-primary' ?>"><?= $e['is_limited'] ? 'Limited' : 'Regular' ?></span></td>
<td class="text-end">
<a href="employee_detail.php?id=<?= $e['id'] ?>" class="btn btn-sm btn-primary me-1">View</a>
<button class="btn btn-sm btn-secondary">Edit</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="addEmployeeModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add New Employee</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">First Name</label>
<input type="text" name="first_name" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Last Name</label>
<input type="text" name="last_name" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Email</label>
<input type="email" name="email" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Telephone (for 2FA)</label>
<input type="text" name="phone" class="form-control" placeholder="+1234567890">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Position</label>
<input type="text" name="position" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Hourly Wage ($)</label>
<input type="number" name="initial_wage" class="form-control" step="0.01">
</div>
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold d-block">Teams</label>
<div class="row px-2">
<?php foreach ($teamList as $t): ?>
<div class="col-md-4 form-check">
<input class="form-check-input" type="checkbox" name="teams[]" value="<?= $t['id'] ?>" id="teamCheck<?= $t['id'] ?>">
<label class="form-check-label small" for="teamCheck<?= $t['id'] ?>"><?= htmlspecialchars($t['name']) ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="col-12 mb-3">
<div class="form-check form-switch p-3 border rounded">
<input class="form-check-input ms-0 me-2" type="checkbox" name="is_limited" id="limitedCheck" checked onchange="togglePasswordFields()">
<label class="form-check-label small fw-bold" for="limitedCheck">Limited Web Reporting (Cannot Login)</label>
</div>
</div>
<div id="passwordSection" style="display: none;">
<div class="row p-3 border rounded bg-light mx-1 mb-3">
<div class="col-md-8 mb-3">
<label class="form-label small fw-bold">Password</label>
<div class="input-group">
<input type="text" name="password" id="employeePassword" class="form-control" placeholder="Set or generate password">
<button type="button" class="btn btn-outline-secondary" onclick="generatePassword()">Generate</button>
</div>
</div>
<div class="col-md-4 mb-3 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="force_password_change" id="forceChange" checked>
<label class="form-check-label small fw-bold" for="forceChange">Require change</label>
</div>
</div>
<div class="col-12">
<div class="extra-small text-muted">Passwords must meet complexity requirements defined in System Preferences.</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="add_employee" class="btn btn-primary px-4">Create Employee</button>
</div>
</form>
</div>
</div>
</div>
<script>
function togglePasswordFields() {
const isLimited = document.getElementById('limitedCheck').checked;
document.getElementById('passwordSection').style.display = isLimited ? 'none' : 'block';
if (!isLimited) {
document.getElementById('employeePassword').required = true;
} else {
document.getElementById('employeePassword').required = false;
document.getElementById('employeePassword').value = '';
}
}
function generatePassword() {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
let retVal = "";
for (let i = 0, n = charset.length; i < 12; ++i) {
retVal += charset.charAt(Math.floor(Math.random() * n));
}
document.getElementById('employeePassword').value = retVal;
}
// Initialize on load
document.addEventListener('DOMContentLoaded', togglePasswordFields);
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

165
expense_files.php Normal file
View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Filters
$project_filter = $_GET['project_id'] ?? '';
$start_date = $_GET['start_date'] ?? '';
$end_date = $_GET['end_date'] ?? '';
$include_archived = isset($_GET['include_archived']) && $_GET['include_archived'] === '1';
// Get Projects for filter
$projects_sql = "SELECT id, name FROM projects WHERE tenant_id = ?";
if (!$include_archived) {
$projects_sql .= " AND is_archived = 0";
}
$projects_sql .= " ORDER BY name ASC";
$projects_stmt = db()->prepare($projects_sql);
$projects_stmt->execute([$tenant_id]);
$all_projects = $projects_stmt->fetchAll();
$where_clauses = ["a.tenant_id = ?", "a.entity_type = 'expense'"];
$params = [$tenant_id];
if (!$include_archived) {
$where_clauses[] = "p.is_archived = 0";
}
if ($project_filter) {
$where_clauses[] = "ex.project_id = ?";
$params[] = $project_filter;
}
if ($start_date) {
$where_clauses[] = "ex.entry_date >= ?";
$params[] = $start_date;
}
if ($end_date) {
$where_clauses[] = "ex.entry_date <= ?";
$params[] = $end_date;
}
$where_sql = implode(" AND ", $where_clauses);
// Fetch Expense Files
$stmt = db()->prepare("
SELECT a.*, ex.entry_date, s.name as supplier_name, p.name as project_name
FROM attachments a
JOIN expenses ex ON a.entity_id = ex.id
JOIN suppliers s ON ex.supplier_id = s.id
JOIN projects p ON ex.project_id = p.id
WHERE $where_sql
ORDER BY a.created_at DESC
");
$stmt->execute($params);
$files = $stmt->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];
}
$pageTitle = "SR&ED Manager - Expense Files";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Expense Files</h2>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select">
<option value="">All Projects</option>
<?php foreach ($all_projects as $p): ?>
<option value="<?= $p['id'] ?>" <?= $project_filter == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($start_date) ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">End Date</label>
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($end_date) ?>">
</div>
<div class="col-md-2 d-flex align-items-center mt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="include_archived" id="includeArchived" value="1" <?= $include_archived ? 'checked' : '' ?> onchange="this.form.submit()">
<label class="form-check-label small" for="includeArchived">Include Archived</label>
</div>
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="d-grid w-100 gap-2 d-md-flex">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="expense_files.php" class="btn btn-outline-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Filename</th>
<th>Size</th>
<th>Uploaded By</th>
<th>Created At</th>
<th>Linked Entry</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($files)): ?>
<tr><td colspan="6" class="text-center py-5 text-muted">No expense files found.</td></tr>
<?php endif; ?>
<?php foreach ($files as $f): ?>
<tr>
<td>
<i class="bi bi-file-earmark-pdf me-2 text-danger"></i>
<strong><?= htmlspecialchars($f['file_name']) ?></strong>
</td>
<td><small class="text-muted"><?= formatBytes((int)$f['file_size']) ?></small></td>
<td>
<div class="d-flex align-items-center">
<div class="bg-light rounded-circle p-1 me-2" style="width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-person small"></i>
</div>
<small><?= htmlspecialchars($f['uploaded_by'] ?? 'System') ?></small>
</div>
</td>
<td class="small text-muted"><?= date('M j, Y H:i', strtotime($f['created_at'])) ?></td>
<td>
<div class="small">
<span class="fw-bold text-dark"><?= $f['entry_date'] ?></span><br>
<span class="text-muted"><?= htmlspecialchars($f['supplier_name']) ?> - <?= htmlspecialchars($f['project_name']) ?></span>
</div>
</td>
<td class="text-end">
<a href="<?= htmlspecialchars($f['file_path']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

285
expenses.php Normal file
View File

@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Add Expense
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
$project_id = (int)($_POST['project_id'] ?? 0);
$supplier_id = (int)($_POST['supplier_id'] ?? 0);
$expense_type_id = (int)($_POST['expense_type_id'] ?? 0);
$amount = (float)($_POST['amount'] ?? 0);
$allocation = (float)($_POST['allocation_percent'] ?? 100);
$entry_date = $_POST['entry_date'] ?? date('Y-m-d');
$notes = $_POST['notes'] ?? '';
if ($project_id && $supplier_id && $amount > 0) {
$stmt = db()->prepare("INSERT INTO expenses (tenant_id, project_id, supplier_id, expense_type_id, amount, allocation_percent, entry_date, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $project_id, $supplier_id, $expense_type_id, $amount, $allocation, $entry_date, $notes]);
$expense_id = (int)db()->lastInsertId();
// Handle File Uploads
if (!empty($_FILES['attachments']['name'][0])) {
foreach ($_FILES['attachments']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['attachments']['error'][$key] === UPLOAD_ERR_OK) {
$file_name = $_FILES['attachments']['name'][$key];
$file_size = $_FILES['attachments']['size'][$key];
$mime_type = $_FILES['attachments']['type'][$key];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
$new_file_name = uniqid() . '.' . $file_ext;
$file_path = 'uploads/' . $new_file_name;
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
if (move_uploaded_file($tmp_name, $file_path)) {
$thumbnail_path = null;
if (isImage($mime_type)) {
$thumb_name = 'thumb_' . $new_file_name;
$thumb_path = 'uploads/' . $thumb_name;
if (createThumbnail($file_path, $thumb_path)) {
$thumbnail_path = $thumb_path;
}
}
$stmt = db()->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, thumbnail_path, file_size, mime_type, uploaded_by) VALUES (?, 'expense', ?, ?, ?, ?, ?, ?, 'John Manager')");
$stmt->execute([$tenant_id, $expense_id, $file_name, $file_path, $thumbnail_path, $file_size, $mime_type]);
}
}
}
}
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Expense Logged', "Logged \$" . number_format($amount, 2) . " expense for project ID $project_id"]);
header("Location: expenses.php?success=1");
exit;
}
}
// Fetch Data
$filter_project = (int)($_GET['project_id'] ?? 0);
$filter_supplier = (int)($_GET['supplier_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$where = ["e.tenant_id = ?"];
$params = [$tenant_id];
if ($filter_project) {
$where[] = "e.project_id = ?";
$params[] = $filter_project;
}
if ($filter_supplier) {
$where[] = "e.supplier_id = ?";
$params[] = $filter_supplier;
}
if ($filter_start) {
$where[] = "e.entry_date >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "e.entry_date <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$expenseEntries = db()->prepare("
SELECT e.*, p.name as project_name, s.name as supplier_name, et.name as expense_type
FROM expenses e
JOIN projects p ON e.project_id = p.id
JOIN suppliers s ON e.supplier_id = s.id
LEFT JOIN expense_types et ON e.expense_type_id = et.id
WHERE $where_clause
ORDER BY e.entry_date DESC, e.created_at DESC
");
$expenseEntries->execute($params);
$expenseList = $expenseEntries->fetchAll();
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
$projects->execute([$tenant_id]);
$projectList = $projects->fetchAll();
$suppliers = db()->prepare("SELECT * FROM suppliers WHERE tenant_id = ? ORDER BY name");
$suppliers->execute([$tenant_id]);
$supplierList = $suppliers->fetchAll();
$expenseTypes = db()->prepare("SELECT * FROM expense_types WHERE tenant_id = ? ORDER BY name");
$expenseTypes->execute([$tenant_id]);
$expenseTypeList = $expenseTypes->fetchAll();
$pageTitle = "SR&ED Manager - Expenses";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Expenses</h2>
<div>
<a href="api/export_expenses.php?<?= http_build_query($_GET) ?>" class="btn btn-primary me-2"><i class="bi bi-file-earmark-excel me-1"></i> Export to Excel</a>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addExpenseModal">+ Add Expense</button>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select form-select-sm">
<option value="">All Projects</option>
<?php
$allProjects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name");
$allProjects->execute([$tenant_id]);
foreach ($allProjects->fetchAll() as $p): ?>
<option value="<?= $p['id'] ?>" <?= $filter_project == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Supplier</label>
<select name="supplier_id" class="form-select form-select-sm">
<option value="">All Suppliers</option>
<?php foreach ($supplierList as $s): ?>
<option value="<?= $s['id'] ?>" <?= $filter_supplier == $s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">From</label>
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_start) ?>">
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">To</label>
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_end) ?>">
</div>
<div class="col-md-2">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-sm btn-primary w-100">Filter</button>
<a href="expenses.php" class="btn btn-sm btn-secondary w-100">Reset</a>
</div>
</div>
</form>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Expense successfully logged.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Date</th>
<th>Supplier</th>
<th>Project</th>
<th>Amount</th>
<th>Allocation</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenseList)): ?>
<tr><td colspan="6" class="text-center py-5 text-muted">No expenses found.</td></tr>
<?php endif; ?>
<?php foreach ($expenseList as $ex): ?>
<tr>
<td class="text-muted"><?= $ex['entry_date'] ?></td>
<td>
<strong><?= htmlspecialchars($ex['supplier_name']) ?></strong><br>
<small class="text-muted"><?= htmlspecialchars($ex['expense_type'] ?? '') ?></small>
</td>
<td><?= htmlspecialchars($ex['project_name']) ?></td>
<td><span class="fw-bold">$<?= number_format((float)$ex['amount'], 2) ?></span></td>
<td>
<div class="progress mb-1" style="height: 6px; width: 100px;">
<div class="progress-bar bg-info" role="progressbar" style="width: <?= $ex['allocation_percent'] ?>%"></div>
</div>
<small class="extra-small text-muted"><?= (float)$ex['allocation_percent'] ?>% SR&ED</small>
</td>
<td class="text-end">
<button class="btn btn-sm btn-secondary">Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="addExpenseModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add Expense</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body">
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select" required>
<option value="">Select Project...</option>
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Supplier</label>
<select name="supplier_id" class="form-select" required>
<option value="">Select Supplier...</option>
<?php foreach ($supplierList as $s): ?>
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Cost Type</label>
<select name="expense_type_id" class="form-select">
<?php foreach ($expenseTypeList as $et): ?>
<option value="<?= $et['id'] ?>"><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold">Amount ($)</label>
<input type="number" name="amount" class="form-control" step="0.01" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold">Allocation (%)</label>
<input type="number" name="allocation_percent" class="form-control" value="100">
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold">Date</label>
<input type="date" name="entry_date" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-12 mb-3">
<label class="form-label small fw-bold">Receipts</label>
<input type="file" name="attachments[]" class="form-control" multiple>
</div>
<div class="col-12">
<label class="form-label small fw-bold">Notes</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="add_expense" class="btn btn-primary px-4">Log Expense</button>
</div>
</form>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

266
export_pdf.php Normal file
View File

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
require_once '/usr/share/php/dompdf/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
// Simulate Tenant Context
$tenant_id = (int)$_SESSION['tenant_id'];
$report_type = $_GET['report_type'] ?? 'labour_export';
// Filter Data (Same as reports.php)
$f_employee = $_GET['employee_id'] ?? '';
$f_team = $_GET['team_id'] ?? '';
$f_start = $_GET['start_date'] ?? date('Y-m-01');
$f_end = $_GET['end_date'] ?? date('Y-m-t');
$f_projects = $_GET['projects'] ?? [];
$f_activity = $_GET['activity_type_id'] ?? '';
// Handle Results for Labour Export
$results = [];
if ($report_type === 'labour_export') {
$query = "
SELECT le.*, p.name as project_name, e.name as employee_name, lt.name as labour_type
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
JOIN employees e ON le.employee_id = e.id
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
WHERE le.tenant_id = ? AND le.entry_date BETWEEN ? AND ?
";
$params = [$tenant_id, $f_start, $f_end];
if ($f_employee) {
$query .= " AND le.employee_id = ?";
$params[] = $f_employee;
}
if ($f_team) {
$query .= " AND le.employee_id IN (SELECT employee_id FROM employee_teams WHERE team_id = ?)";
$params[] = $f_team;
}
if (!empty($f_projects)) {
$placeholders = implode(',', array_fill(0, count($f_projects), '?'));
$query .= " AND le.project_id IN ($placeholders)";
foreach ($f_projects as $pid) $params[] = $pid;
}
if ($f_activity) {
$query .= " AND le.labour_type_id = ?";
$params[] = $f_activity;
}
$query .= " ORDER BY le.entry_date ASC";
$stmt = db()->prepare($query);
$stmt->execute($params);
$results = $stmt->fetchAll();
}
// Handle Calendar Report
$calendarData = [];
$cal_month = $_GET['month'] ?? date('Y-m');
if ($report_type === 'calendar') {
$stmt = db()->prepare("
SELECT entry_date, SUM(hours) as total_hours
FROM labour_entries
WHERE tenant_id = ? AND DATE_FORMAT(entry_date, '%Y-%m') = ?
GROUP BY entry_date
");
$stmt->execute([$tenant_id, $cal_month]);
$calendarData = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
}
// Fetch filter names for header (Labour Export only)
$employee_name = 'All Employees';
if ($report_type === 'labour_export') {
if ($f_employee) {
$stmt = db()->prepare("SELECT name FROM employees WHERE id = ?");
$stmt->execute([$f_employee]);
$employee_name = $stmt->fetchColumn() ?: 'All Employees';
}
$project_names = 'All Projects';
if (!empty($f_projects)) {
$placeholders = implode(',', array_fill(0, count($f_projects), '?'));
$stmt = db()->prepare("SELECT name FROM projects WHERE id IN ($placeholders)");
$stmt->execute($f_projects);
$p_list = $stmt->fetchAll(PDO::FETCH_COLUMN);
$project_names = implode(', ', $p_list);
}
}
// Generate HTML
$html = '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; color: #333; }
.header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #3b82f6; padding-bottom: 10px; }
.header h1 { margin: 0; color: #0f172a; font-size: 20px; }
.filters { margin-bottom: 20px; background: #f8fafc; padding: 10px; border-radius: 5px; border: 1px solid #e2e8f0; }
.filters table { width: 100%; }
.label { font-weight: bold; color: #64748b; font-size: 10px; }
table.data { width: 100%; border-collapse: collapse; margin-top: 10px; }
table.data th { background: #f1f5f9; color: #475569; text-align: left; padding: 8px; border: 1px solid #e2e8f0; text-transform: uppercase; font-size: 9px; }
table.data td { padding: 8px; border: 1px solid #e2e8f0; }
.text-end { text-align: right; }
.footer { margin-top: 30px; text-align: center; font-size: 9px; color: #94a3b8; }
.total-row { background: #f8fafc; font-weight: bold; }
.badge { background: #f1f5f9; padding: 2px 5px; border-radius: 3px; border: 1px solid #e2e8f0; font-size: 9px; }
/* Calendar Styles for PDF */
.calendar { width: 100%; border-collapse: collapse; table-layout: fixed; }
.calendar th { background: #f1f5f9; padding: 10px; border: 1px solid #e2e8f0; text-transform: uppercase; font-size: 10px; color: #64748b; }
.calendar td { height: 70px; vertical-align: top; padding: 5px; border: 1px solid #e2e8f0; background: #fff; }
.day-num { font-weight: bold; color: #94a3b8; margin-bottom: 5px; display: block; }
.day-hours { color: #3b82f6; font-weight: bold; font-size: 14px; text-align: right; display: block; margin-top: 10px; }
.other-month { background: #f8fafc; }
</style>
</head>
<body>
<div class="header">
<h1>' . ($report_type === 'calendar' ? 'Monthly Labour Calendar' : 'SR&ED Labour Report') . '</h1>
<p style="margin-top: 5px; color: #64748b;">Generated on ' . date('F j, Y, g:i a') . '</p>
</div>';
if ($report_type === 'labour_export') {
$html .= '
<div class="filters">
<table cellspacing="0" cellpadding="0">
<tr>
<td class="label" width="15%">Period:</td>
<td width="35%">' . date('M j, Y', strtotime($f_start)) . ' to ' . date('M j, Y', strtotime($f_end)) . '</td>
<td class="label" width="15%">Employee:</td>
<td width="35%">' . htmlspecialchars($employee_name) . '</td>
</tr>
<tr>
<td class="label">Projects:</td>
<td colspan="3">' . htmlspecialchars($project_names) . '</td>
</tr>
</table>
</div>
<table class="data">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Project</th>
<th>Activity</th>
<th class="text-end">Hours</th>
</tr>
</thead>
<tbody>';
$total_hours = 0;
foreach ($results as $row) {
$total_hours += (float)$row['hours'];
$html .= '
<tr>
<td>' . $row['entry_date'] . '</td>
<td><strong>' . htmlspecialchars($row['employee_name']) . '</strong></td>
<td>' . htmlspecialchars($row['project_name']) . '</td>
<td><span class="badge">' . htmlspecialchars($row['labour_type'] ?? 'Uncategorized') . '</span></td>
<td class="text-end">' . number_format((float)$row['hours'], 2) . '</td>
</tr>';
}
if (empty($results)) {
$html .= '<tr><td colspan="5" style="text-align:center; padding: 20px;">No records found.</td></tr>';
}
$html .= '
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="4" class="text-end">Total Hours:</td>
<td class="text-end" style="color: #3b82f6;">' . number_format($total_hours, 2) . '</td>
</tr>
</tfoot>
</table>';
} else {
// Calendar View
$html .= '
<div class="filters" style="text-align: center; font-size: 14px; font-weight: bold; color: #0f172a;">
' . date('F Y', strtotime($cal_month)) . '
</div>
<table class="calendar">
<thead>
<tr>
<th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th>
</tr>
</thead>
<tbody>';
$first_day = date('Y-m-01', strtotime($cal_month));
$days_in_month = (int)date('t', strtotime($cal_month));
$start_day_of_week = (int)date('w', strtotime($first_day));
$cal_day_count = 0;
$html .= '<tr>';
// Fill empty days before first of month
for ($i = 0; $i < $start_day_of_week; $i++) {
$html .= '<td class="other-month"></td>';
$cal_day_count++;
}
// Days of month
for ($day = 1; $day <= $days_in_month; $day++) {
if ($cal_day_count > 0 && ($cal_day_count % 7) == 0) {
$html .= '</tr><tr>';
}
$current_date = date('Y-m-', strtotime($cal_month)) . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
$hours = $calendarData[$current_date] ?? 0;
$html .= '<td>';
$html .= '<span class="day-num">' . $day . '</span>';
if ($hours > 0) {
$html .= '<span class="day-hours">' . number_format((float)$hours, 1) . 'h</span>';
}
$html .= '</td>';
$cal_day_count++;
}
// Fill remaining days to complete grid
while ($cal_day_count % 7 != 0) {
$html .= '<td class="other-month"></td>';
$cal_day_count++;
}
$html .= '</tr>';
$html .= '
</tbody>
</table>';
}
$html .= '
<div class="footer">
<p>This report is for internal ERP and SR&ED documentation purposes.</p>
</div>
</body>
</html>';
// Setup Dompdf
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
// Output the PDF
$prefix = $report_type === 'calendar' ? 'Monthly_Calendar_' : 'Labour_Report_';
$filename = $prefix . date('Ymd_His') . '.pdf';
$dompdf->stream($filename, ['Attachment' => true]);

188
files.php Normal file
View File

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Filters
$project_filter = $_GET['project_id'] ?? '';
$start_date = $_GET['start_date'] ?? '';
$end_date = $_GET['end_date'] ?? '';
$include_archived = isset($_GET['include_archived']) && $_GET['include_archived'] === '1';
// Get Projects for filter
$projects_sql = "SELECT id, name FROM projects WHERE tenant_id = ?";
if (!$include_archived) {
$projects_sql .= " AND is_archived = 0";
}
$projects_sql .= " ORDER BY name ASC";
$projects_stmt = db()->prepare($projects_sql);
$projects_stmt->execute([$tenant_id]);
$all_projects = $projects_stmt->fetchAll();
$where_clauses = ["a.tenant_id = ?"];
$params = [$tenant_id];
if (!$include_archived) {
$where_clauses[] = "(lp.is_archived = 0 OR ep.is_archived = 0 OR (a.entity_type NOT IN ('labour_entry', 'expense')))";
}
if ($project_filter) {
$where_clauses[] = "(le.project_id = ? OR ex.project_id = ?)";
$params[] = $project_filter;
$params[] = $project_filter;
}
if ($start_date) {
$where_clauses[] = "(le.entry_date >= ? OR ex.entry_date >= ?)";
$params[] = $start_date;
$params[] = $start_date;
}
if ($end_date) {
$where_clauses[] = "(le.entry_date <= ? OR ex.entry_date <= ?)";
$params[] = $end_date;
$params[] = $end_date;
}
$where_sql = implode(" AND ", $where_clauses);
// Fetch All Files with their related context using LEFT JOINs for better filtering
$stmt = db()->prepare("
SELECT a.*,
le.entry_date as labour_date, e.name as employee_name, lp.name as labour_project,
ex.entry_date as expense_date, s.name as supplier_name, ep.name as expense_project
FROM attachments a
LEFT JOIN labour_entries le ON a.entity_id = le.id AND a.entity_type = 'labour_entry'
LEFT JOIN employees e ON le.employee_id = e.id
LEFT JOIN projects lp ON le.project_id = lp.id
LEFT JOIN expenses ex ON a.entity_id = ex.id AND a.entity_type = 'expense'
LEFT JOIN suppliers s ON ex.supplier_id = s.id
LEFT JOIN projects ep ON ex.project_id = ep.id
WHERE $where_sql
ORDER BY a.created_at DESC
");
$stmt->execute($params);
$files = $stmt->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];
}
$pageTitle = "SR&ED Manager - All Files Report";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">System Files Report</h2>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select">
<option value="">All Projects</option>
<?php foreach ($all_projects as $p): ?>
<option value="<?= $p['id'] ?>" <?= $project_filter == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($start_date) ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">End Date</label>
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($end_date) ?>">
</div>
<div class="col-md-2 d-flex align-items-center mt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="include_archived" id="includeArchived" value="1" <?= $include_archived ? 'checked' : '' ?> onchange="this.form.submit()">
<label class="form-check-label small" for="includeArchived">Include Archived</label>
</div>
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="d-grid w-100 gap-2 d-md-flex">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="files.php" class="btn btn-outline-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Type</th>
<th>Filename</th>
<th>Size</th>
<th>Uploaded By</th>
<th>Created At</th>
<th>Linked Entry</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($files)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted">No files found in the system.</td></tr>
<?php endif; ?>
<?php foreach ($files as $f): ?>
<tr>
<td>
<?php if ($f['entity_type'] === 'labour_entry'): ?>
<span class="badge bg-soft-primary text-primary border">Labour</span>
<?php elseif ($f['entity_type'] === 'expense'): ?>
<span class="badge bg-soft-success text-success border">Expense</span>
<?php else: ?>
<span class="badge bg-soft-secondary text-secondary border"><?= ucfirst($f['entity_type']) ?></span>
<?php endif; ?>
</td>
<td>
<strong><?= htmlspecialchars($f['file_name']) ?></strong>
</td>
<td><small class="text-muted"><?= formatBytes((int)$f['file_size']) ?></small></td>
<td><small><?= htmlspecialchars($f['uploaded_by'] ?? 'System') ?></small></td>
<td class="small text-muted"><?= date('M j, Y', strtotime($f['created_at'])) ?></td>
<td>
<small class="text-muted text-truncate d-inline-block" style="max-width: 300px;">
<?php
if ($f['entity_type'] === 'labour_entry') {
echo htmlspecialchars(($f['labour_date'] ?? '') . ' - ' . ($f['employee_name'] ?? '') . ' - ' . ($f['labour_project'] ?? ''));
} elseif ($f['entity_type'] === 'expense') {
echo htmlspecialchars(($f['expense_date'] ?? '') . ' - ' . ($f['supplier_name'] ?? '') . ' - ' . ($f['expense_project'] ?? ''));
} else {
echo 'N/A';
}
?>
</small>
</td>
<td class="text-end">
<a href="<?= htmlspecialchars($f['file_path']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<style>
.bg-soft-primary { background-color: rgba(59, 130, 246, 0.1); }
.bg-soft-success { background-color: rgba(34, 197, 94, 0.1); }
.bg-soft-secondary { background-color: rgba(107, 114, 128, 0.1); }
</style>
<?php include __DIR__ . '/includes/footer.php'; ?>

88
forgot_password.php Normal file
View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
require_once __DIR__ . '/mail/MailService.php';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$ip = Auth::getIpAddress();
// Record attempt for security tracking
Auth::recordResetAttempt($email, $ip);
$stmt = db()->prepare("SELECT id FROM users WHERE email = ? LIMIT 1");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user) {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt = db()->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$email, $token, $expires]);
// Send Email
$resetLink = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]/reset_password.php?token=$token";
$subject = "Password Reset Request";
$html = "
<h3>Password Reset Request</h3>
<p>We received a request to reset your password for SR&ED Manager.</p>
<p>Click the link below to set a new password. This link will expire in 1 hour.</p>
<p><a href='$resetLink' style='padding: 10px 20px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 5px; display: inline-block;'>Reset Password</a></p>
<p>If you did not request this, please ignore this email.</p>
";
$text = "Reset your password by clicking this link: $resetLink";
MailService::sendMail($email, $subject, $html, $text);
}
// Always show success to prevent email enumeration
$success = true;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Forgot Password - SR&ED Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
</style>
</head>
<body>
<div class="card login-card p-4">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary">RESET PASSWORD</h3>
<p class="text-muted small">Enter your email to receive a reset link</p>
</div>
<?php if ($success): ?>
<div class="alert alert-success small">
If an account exists with that email, you will receive a password reset link shortly.
</div>
<div class="text-center">
<a href="login.php" class="btn btn-outline-secondary w-100">Back to Login</a>
</div>
<?php else: ?>
<form method="POST">
<div class="mb-3">
<label class="form-label small fw-bold">Email Address</label>
<input type="email" name="email" class="form-control" placeholder="name@company.com" required autofocus>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">Send Reset Link</button>
<div class="text-center">
<a href="login.php" class="small text-decoration-none">Back to Login</a>
</div>
</form>
<?php endif; ?>
</div>
</body>
</html>

189
import_expenses.php Normal file
View File

@ -0,0 +1,189 @@
<?php
require_once 'db/config.php';
$pageTitle = 'Import Expenses';
$message = '';
$error = '';
$results = [];
// Helper to get maps for lookups
function getLookupMaps() {
$db = db();
$maps = [
'projects' => [],
'suppliers' => [],
'types' => []
];
$s = $db->query("SELECT id, project_code FROM projects");
while($r = $s->fetch(PDO::FETCH_ASSOC)) $maps['projects'][strtolower($r['project_code'])] = $r['id'];
$s = $db->query("SELECT id, name FROM suppliers");
while($r = $s->fetch(PDO::FETCH_ASSOC)) $maps['suppliers'][strtolower($r['name'])] = $r['id'];
$s = $db->query("SELECT id, name FROM expense_types");
while($r = $s->fetch(PDO::FETCH_ASSOC)) $maps['types'][strtolower($r['name'])] = $r['id'];
return $maps;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$error = 'File upload failed.';
} else {
$handle = fopen($file['tmp_name'], 'r');
$header = fgetcsv($handle);
$expected = ['project_code', 'supplier_name', 'expense_type', 'amount', 'date', 'notes'];
if (!$header || count(array_intersect($expected, $header)) < 3) {
$error = 'Invalid CSV format. Please ensure project_code, supplier_name, and expense_type are present.';
} else {
$columnMap = array_flip($header);
$maps = getLookupMaps();
$rowCount = 0;
$importCount = 0;
$skippedCount = 0;
db()->beginTransaction();
try {
$stmt = db()->prepare("INSERT INTO expenses (tenant_id, project_id, supplier_id, expense_type_id, amount, allocation_percent, entry_date, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
while (($row = fgetcsv($handle)) !== false) {
$rowCount++;
$data = [];
foreach ($expected as $col) {
$data[$col] = isset($columnMap[$col]) && isset($row[$columnMap[$col]]) ? trim($row[$columnMap[$col]]) : '';
}
$projId = $maps['projects'][strtolower($data['project_code'])] ?? null;
$suppId = $maps['suppliers'][strtolower($data['supplier_name'])] ?? null;
$typeId = $maps['types'][strtolower($data['expense_type'])] ?? null;
if (!$projId) {
$results[] = "Row $rowCount: Skipped (Unknown Project Code: " . htmlspecialchars($data['project_code']) . ")";
$skippedCount++;
continue;
}
if (!$suppId) {
$results[] = "Row $rowCount: Skipped (Unknown Supplier: " . htmlspecialchars($data['supplier_name']) . ")";
$skippedCount++;
continue;
}
if (!$typeId) {
$results[] = "Row $rowCount: Skipped (Unknown Expense Type: " . htmlspecialchars($data['expense_type']) . ")";
$skippedCount++;
continue;
}
$amount = floatval($data['amount']);
$date = empty($data['date']) ? date('Y-m-d') : $data['date'];
$stmt->execute([
1, // tenant_id
$projId,
$suppId,
$typeId,
$amount,
100.00, // Default allocation_percent
$date,
$data['notes']
]);
$importCount++;
}
db()->commit();
$message = "Import completed. $importCount expenses imported, $skippedCount skipped.";
} catch (Exception $e) {
db()->rollBack();
$error = 'Database error: ' . $e->getMessage();
}
}
fclose($handle);
}
}
include 'includes/header.php';
?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-receipt me-2"></i>Import Expenses</h2>
<a href="samples/expenses_template.csv" class="btn btn-outline-primary btn-sm">
<i class="bi bi-download me-1"></i>Download Template
</a>
</div>
<?php if ($message): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $message ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<form action="import_expenses.php" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label for="csv_file" class="form-label fw-bold">Select CSV File</label>
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
<div class="form-text mt-2">
Max file size: 2MB. Only .csv files are allowed.
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="bi bi-upload me-2"></i>Upload and Import
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Instructions</h5>
</div>
<div class="card-body">
<ol class="small text-muted">
<li>Download the CSV template.</li>
<li>Ensure <strong>project_code</strong>, <strong>supplier_name</strong>, and <strong>expense_type</strong> match existing records in the system.</li>
<li><strong>amount</strong> should be a numeric value.</li>
<li><strong>date</strong> should be in YYYY-MM-DD format (defaults to today if empty).</li>
</ol>
<div class="alert alert-warning py-2 small mb-0">
<i class="bi bi-exclamation-triangle me-1"></i> Rows with unknown projects, suppliers, or types will be skipped.
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($results)): ?>
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Logs</h5>
</div>
<div class="card-body">
<div class="list-group list-group-flush small">
<?php foreach ($results as $res): ?>
<div class="list-group-item px-0 border-0 py-1">
<i class="bi bi-dot me-1"></i><?= $res ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php include 'includes/footer.php'; ?>

185
import_labour.php Normal file
View File

@ -0,0 +1,185 @@
<?php
require_once 'db/config.php';
$pageTitle = 'Import Labour Activities';
$message = '';
$error = '';
$results = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$error = 'File upload failed.';
} else {
$handle = fopen($file['tmp_name'], 'r');
$header = fgetcsv($handle);
$expected = ['project_code', 'employee_email', 'date', 'hours', 'labour_type', 'evidence_type', 'notes'];
if (!$header || count(array_intersect($expected, $header)) < 1) {
$error = 'Invalid CSV format. Please use the provided template.';
} else {
$columnMap = array_flip($header);
// Pre-fetch lookups
$projects = [];
$stmt = db()->query("SELECT id, code FROM projects WHERE tenant_id = 1");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $projects[$row['code']] = $row['id'];
$employees = [];
$stmt = db()->query("SELECT id, email FROM employees WHERE tenant_id = 1");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $employees[$row['email']] = $row['id'];
$labourTypes = [];
$stmt = db()->query("SELECT id, name FROM labour_types");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $labourTypes[strtolower($row['name'])] = $row['id'];
$evidenceTypes = [];
$stmt = db()->query("SELECT id, name FROM evidence_types");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $evidenceTypes[strtolower($row['name'])] = $row['id'];
$rowCount = 0;
$importCount = 0;
$skippedCount = 0;
db()->beginTransaction();
try {
$stmt = db()->prepare("INSERT INTO labour_entries (tenant_id, project_id, employee_id, entry_date, hours, labour_type_id, evidence_type_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
while (($row = fgetcsv($handle)) !== false) {
$rowCount++;
$data = [];
foreach ($expected as $col) {
$data[$col] = isset($columnMap[$col]) && isset($row[$columnMap[$col]]) ? trim($row[$columnMap[$col]]) : '';
}
$rowErrors = [];
$projectId = $projects[$data['project_code']] ?? null;
$employeeId = $employees[$data['employee_email']] ?? null;
$labourTypeId = $labourTypes[strtolower($data['labour_type'])] ?? null;
$evidenceTypeId = $evidenceTypes[strtolower($data['evidence_type'])] ?? 5; // Default to 'None'
if (!$projectId) $rowErrors[] = "Invalid Project Code: " . $data['project_code'];
if (!$employeeId) $rowErrors[] = "Invalid Employee Email: " . $data['employee_email'];
if (!$labourTypeId) $rowErrors[] = "Invalid Labour Type: " . $data['labour_type'];
if (empty($data['date']) || !strtotime($data['date'])) $rowErrors[] = "Invalid Date: " . $data['date'];
if (!is_numeric($data['hours'])) $rowErrors[] = "Invalid Hours: " . $data['hours'];
if (!empty($rowErrors)) {
$skippedCount++;
$results[] = "Row $rowCount: Skipped (" . implode(', ', $rowErrors) . ")";
continue;
}
$stmt->execute([
1,
$projectId,
$employeeId,
date('Y-m-d', strtotime($data['date'])),
$data['hours'],
$labourTypeId,
$evidenceTypeId,
$data['notes']
]);
$importCount++;
}
db()->commit();
$message = "Import completed successfully. $importCount activities imported, $skippedCount skipped.";
} catch (Exception $e) {
db()->rollBack();
$error = 'Database error: ' . $e->getMessage();
}
}
fclose($handle);
}
}
include 'includes/header.php';
?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-clock-history me-2"></i>Import Labour Activities</h2>
<a href="samples/labour_template.csv" class="btn btn-outline-primary btn-sm">
<i class="bi bi-download me-1"></i>Download Template
</a>
</div>
<?php if ($message): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $message ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<form action="import_labour.php" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label for="csv_file" class="form-label fw-bold">Select CSV File</label>
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
<div class="form-text mt-2">
Max file size: 5MB. Only .csv files are allowed.
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="bi bi-upload me-2"></i>Upload and Import
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Instructions</h5>
</div>
<div class="card-body">
<ol class="small text-muted">
<li>Download the CSV template using the button above.</li>
<li>Ensure <strong>Project Code</strong> and <strong>Employee Email</strong> match existing records.</li>
<li><strong>Date</strong> should be in YYYY-MM-DD format.</li>
<li><strong>Labour Type</strong> must match one of:
<ul class="mb-0">
<li>Experimental Development</li>
<li>Technical Support</li>
<li>Technical Planning</li>
</ul>
</li>
<li>Upload and review the logs for any skipped rows.</li>
</ol>
</div>
</div>
</div>
</div>
<?php if (!empty($results)): ?>
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Logs</h5>
</div>
<div class="card-body">
<div class="list-group list-group-flush small">
<?php foreach ($results as $res): ?>
<div class="list-group-item px-0 border-0 py-1">
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i><?= $res ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php include 'includes/footer.php'; ?>

152
import_suppliers.php Normal file
View File

@ -0,0 +1,152 @@
<?php
require_once 'db/config.php';
$pageTitle = 'Import Suppliers';
$message = '';
$error = '';
$results = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$error = 'File upload failed.';
} else {
$handle = fopen($file['tmp_name'], 'r');
$header = fgetcsv($handle);
// Expected columns: name, type, contact_info
$expected = ['name', 'type', 'contact_info'];
if (!$header || count(array_intersect($expected, $header)) < 1) {
$error = 'Invalid CSV format. Please use the provided template.';
} else {
$columnMap = array_flip($header);
$rowCount = 0;
$importCount = 0;
$skippedCount = 0;
db()->beginTransaction();
try {
$stmt = db()->prepare("INSERT INTO suppliers (tenant_id, name, type, contact_info) VALUES (?, ?, ?, ?)");
while (($row = fgetcsv($handle)) !== false) {
$rowCount++;
$data = [];
foreach ($expected as $col) {
$data[$col] = isset($columnMap[$col]) && isset($row[$columnMap[$col]]) ? trim($row[$columnMap[$col]]) : '';
}
if (empty($data['name'])) {
$skippedCount++;
$results[] = "Row $rowCount: Skipped (Missing Name)";
continue;
}
// Default type if invalid
if (!in_array($data['type'], ['supplier', 'contractor'])) {
$data['type'] = 'supplier';
}
$stmt->execute([
1, // Hardcoded tenant_id
$data['name'],
$data['type'],
$data['contact_info']
]);
$importCount++;
}
db()->commit();
$message = "Import completed successfully. $importCount suppliers imported, $skippedCount skipped.";
} catch (Exception $e) {
db()->rollBack();
$error = 'Database error: ' . $e->getMessage();
}
}
fclose($handle);
}
}
include 'includes/header.php';
?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-truck me-2"></i>Import Suppliers</h2>
<a href="samples/suppliers_template.csv" class="btn btn-outline-primary btn-sm">
<i class="bi bi-download me-1"></i>Download Template
</a>
</div>
<?php if ($message): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $message ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-6">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<form action="import_suppliers.php" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label for="csv_file" class="form-label fw-bold">Select CSV File</label>
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
<div class="form-text mt-2">
Max file size: 2MB. Only .csv files are allowed.
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="bi bi-upload me-2"></i>Upload and Import
</button>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Instructions</h5>
</div>
<div class="card-body">
<ol class="small text-muted">
<li>Download the CSV template using the button above.</li>
<li>Fill in your supplier data. The <strong>name</strong> field is required.</li>
<li>The <strong>type</strong> field should be either 'supplier' or 'contractor'.</li>
<li>Save the file as a CSV and upload it.</li>
</ol>
<div class="alert alert-info py-2 small mb-0">
<i class="bi bi-info-circle me-1"></i> Existing suppliers with the same name will be added as new entries.
</div>
</div>
</div>
</div>
</div>
<?php if (!empty($results)): ?>
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Import Logs</h5>
</div>
<div class="card-body">
<div class="list-group list-group-flush small">
<?php foreach ($results as $res): ?>
<div class="list-group-item px-0 border-0 py-1">
<i class="bi bi-dot me-1"></i><?= $res ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php include 'includes/footer.php'; ?>

124
includes/auth_helper.php Normal file
View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/**
* Configure session for iframed environments (like Flatlogic preview)
*/
function start_secure_session() {
if (session_status() === PHP_SESSION_NONE) {
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['SERVER_PORT'] == 443)
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
// Use SameSite=None only if secure, otherwise use Lax
$samesite = $secure ? 'None' : 'Lax';
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $secure,
'httponly' => true,
'samesite' => $samesite
]);
if (!session_start()) {
error_log("Failed to start session");
}
}
}
start_secure_session();
/**
* Authentication Helper
*/
class Auth {
public static function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
public static function requireLogin(): void {
if (!self::isLoggedIn()) {
header('Location: login.php');
exit;
}
}
public static function login(int $userId, int $tenantId, string $role): void {
if (session_status() === PHP_SESSION_NONE) {
start_secure_session();
}
$_SESSION['user_id'] = $userId;
$_SESSION['tenant_id'] = $tenantId;
$_SESSION['role'] = $role;
// Important: Save session before geolocation which might be slow
session_write_close();
// Tracking
$ip = self::getIpAddress();
$country = self::getCountryFromIp($ip);
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
try {
// Re-open to update tracking info in DB if we want,
// but we can just use a fresh DB connection
$stmt = db()->prepare("INSERT INTO user_sessions (user_id, ip_address, country, user_agent) VALUES (?, ?, ?, ?)");
$stmt->execute([$userId, $ip, $country, $userAgent]);
$stmt = db()->prepare("UPDATE users SET last_login_at = NOW(), last_login_ip = ? WHERE id = ?");
$stmt->execute([$ip, $userId]);
} catch (\Throwable $e) {
error_log("Auth::login tracking error: " . $e->getMessage());
}
}
public static function logout(): void {
if (session_status() === PHP_SESSION_NONE) {
start_secure_session();
}
$_SESSION = [];
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
session_destroy();
header('Location: login.php', true, 302);
exit;
}
public static function getIpAddress(): string {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} else {
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}
public static function getCountryFromIp(string $ip): ?string {
if ($ip === '127.0.0.1' || $ip === '::1') return 'Localhost';
try {
$ctx = stream_context_create(['http' => ['timeout' => 2]]);
$resp = @file_get_contents("http://ip-api.com/json/{$ip}?fields=country", false, $ctx);
if ($resp) {
$data = json_decode($resp, true);
return $data['country'] ?? 'Unknown';
}
} catch (\Throwable $e) {
}
return 'Unknown';
}
public static function recordResetAttempt(string $email, string $ip): void {
try {
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([0, 'Password Reset Attempt', "Email: $email, IP: $ip"]);
} catch (\Throwable $e) {}
}
}

35
includes/footer.php Normal file
View File

@ -0,0 +1,35 @@
<footer class="py-4 mt-auto border-top bg-white">
<div class="container-fluid">
<div class="d-flex align-items-center justify-content-between small text-muted">
<div>&copy; <?= date('Y') ?> SR&ED Manager ERP</div>
<div>
<a href="#" class="text-decoration-none text-muted me-3">Privacy Policy</a>
<a href="#" class="text-decoration-none text-muted">Terms of Service</a>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Smooth scroll for anchors
document.querySelectorAll('a[href^="index.php#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const url = new URL(this.href);
if (window.location.pathname.endsWith('index.php') || window.location.pathname === '/') {
e.preventDefault();
const targetId = url.hash.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 80,
behavior: 'smooth'
});
}
}
});
});
</script>
</body>
</html>

159
includes/header.php Normal file
View File

@ -0,0 +1,159 @@
<?php
/**
* Global Header with Static Navigation
*/
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $pageTitle ?? 'SR&ED Manager' ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="assets/css/custom.css?v=<?= time() ?>" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container-fluid py-3 bg-white">
<a class="navbar-brand fs-3" href="index.php">SR&ED MANAGER</a>
</div>
<nav class="navbar navbar-expand-lg sticky-top border-top">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'index.php' ? 'active' : '' ?>" href="index.php">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'projects.php' ? 'active' : '' ?>" href="projects.php">Projects</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['labour.php', 'labour_files.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
Labour
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-menu-item dropdown-item" href="labour.php">Labour Tracking</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="labour_files.php">Labour Files</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['expenses.php', 'expense_files.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
Expenses
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-menu-item dropdown-item" href="expenses.php">Expense Logs</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="expense_files.php">Expense Files</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'employees.php' ? 'active' : '' ?>" href="employees.php">Employees</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['reports.php', 'files.php', 'reports_media.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
Reports
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-menu-item dropdown-item" href="reports.php">Summary Reports</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="sred_claim_report_selector.php">SRED Claim Report</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="reports_media.php">Media Gallery</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="files.php">Files</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['settings.php', 'company_settings.php', 'system_preferences.php', 'import_suppliers.php', 'import_expenses.php', 'import_labour.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
Settings
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-menu-item dropdown-item" href="settings.php">Datasets</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="company_settings.php">Company Preferences</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="system_preferences.php">System Preferences</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-menu-item dropdown-item" href="import_suppliers.php">Import Suppliers</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="import_expenses.php">Import Expenses</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="import_labour.php">Import Labour</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center">
<div class="position-relative me-3" style="width: 250px;">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
<input type="text" id="globalSearch" class="form-control border-start-0 ps-0" placeholder="Search projects or employees..." autocomplete="off">
</div>
<div id="searchResults" class="dropdown-menu w-100 mt-1 shadow-sm" style="display: none; max-height: 300px; overflow-y: auto;"></div>
</div>
<div class="dropdown">
<button class="btn btn-light btn-sm border dropdown-toggle d-flex align-items-center" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-2"></i>
<div class="text-start me-2">
<div class="extra-small fw-bold lh-1"><?= htmlspecialchars($_SESSION['role'] ?? 'User') ?></div>
<div class="extra-small text-muted lh-1">Acme Research</div>
</div>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
<li><h6 class="dropdown-header small">Account Settings</h6></li>
<li><a class="dropdown-item small" href="#"><i class="bi bi-person me-2"></i>My Profile</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item small text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i>Logout</a></li>
</ul>
</div>
</div>
</div>
</div>
</nav>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('globalSearch');
const searchResults = document.getElementById('searchResults');
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
searchResults.style.display = 'none';
return;
}
debounceTimer = setTimeout(() => {
fetch(`api/search.php?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
searchResults.innerHTML = '';
if (data.length > 0) {
data.forEach(item => {
const div = document.createElement('a');
div.href = item.url;
div.className = 'dropdown-item d-flex justify-content-between align-items-center';
div.innerHTML = `
<span>${item.label}</span>
<span class="badge bg-light text-muted extra-small">${item.type}</span>
`;
searchResults.appendChild(div);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div class="dropdown-item text-muted small">No results found</div>';
searchResults.style.display = 'block';
}
});
}, 300);
});
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
});
</script>

107
includes/media_helper.php Normal file
View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* Creates a thumbnail for an image.
*
* @param string $sourcePath Path to the source image.
* @param string $targetPath Path where the thumbnail should be saved.
* @param int $maxWidth Maximum width of the thumbnail.
* @param int $maxHeight Maximum height of the thumbnail.
* @return bool True on success, false on failure.
*/
function createThumbnail(string $sourcePath, string $targetPath, int $maxWidth = 400, int $maxHeight = 400): bool {
if (!file_exists($sourcePath)) {
return false;
}
$imageInfo = getimagesize($sourcePath);
if ($imageInfo === false) {
return false;
}
list($width, $height, $type) = $imageInfo;
$sourceImage = null;
switch ($type) {
case IMAGETYPE_JPEG:
$sourceImage = @imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$sourceImage = @imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$sourceImage = @imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$sourceImage = @imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
if (!$sourceImage) {
return false;
}
// Calculate dimensions
$ratio = min($maxWidth / $width, $maxHeight / $height);
if ($ratio >= 1) {
$newWidth = $width;
$newHeight = $height;
} else {
$newWidth = (int)($width * $ratio);
$newHeight = (int)($height * $ratio);
}
$thumbnail = imagecreatetruecolor($newWidth, $newHeight);
// Handle transparency for PNG/GIF/WebP
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF || $type == IMAGETYPE_WEBP) {
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
imagefilledrectangle($thumbnail, 0, 0, $newWidth, $newHeight, $transparent);
}
imagecopyresampled($thumbnail, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
if (!is_dir(dirname($targetPath))) {
mkdir(dirname($targetPath), 0775, true);
}
$success = false;
switch ($type) {
case IMAGETYPE_JPEG:
$success = imagejpeg($thumbnail, $targetPath, 85);
break;
case IMAGETYPE_PNG:
$success = imagepng($thumbnail, $targetPath);
break;
case IMAGETYPE_GIF:
$success = imagegif($thumbnail, $targetPath);
break;
case IMAGETYPE_WEBP:
$success = imagewebp($thumbnail, $targetPath);
break;
}
imagedestroy($sourceImage);
imagedestroy($thumbnail);
return $success;
}
/**
* Checks if a mime type is an image.
*/
function isImage(string $mimeType): bool {
return str_starts_with($mimeType, 'image/');
}
/**
* Checks if a mime type is a video.
*/
function isVideo(string $mimeType): bool {
return str_starts_with($mimeType, 'video/');
}

439
index.php
View File

@ -1,150 +1,301 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$tenant_id = (int)$_SESSION['tenant_id'];
// Fetch Highlights Data
$projectHighlights = db()->prepare("
SELECT p.*,
CONCAT(e.first_name, ' ', e.last_name) as owner_name,
COALESCE((SELECT SUM(hours) FROM labour_entries WHERE project_id = p.id), 0) as total_hours
FROM projects p
LEFT JOIN employees e ON p.owner_id = e.id
WHERE p.tenant_id = ?
ORDER BY p.created_at DESC
LIMIT 5
");
$projectHighlights->execute([$tenant_id]);
$projectList = $projectHighlights->fetchAll();
$labourHighlights = db()->prepare("
SELECT le.*, p.name as project_name, e.name as employee_name
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
JOIN employees e ON le.employee_id = e.id
WHERE le.tenant_id = ?
ORDER BY le.entry_date DESC, le.created_at DESC
LIMIT 5
");
$labourHighlights->execute([$tenant_id]);
$labourList = $labourHighlights->fetchAll();
$expenseHighlights = db()->prepare("
SELECT e.*, p.name as project_name, s.name as supplier_name
FROM expenses e
JOIN projects p ON e.project_id = p.id
JOIN suppliers s ON e.supplier_id = s.id
WHERE e.tenant_id = ?
ORDER BY e.entry_date DESC, e.created_at DESC
LIMIT 5
");
$expenseHighlights->execute([$tenant_id]);
$expenseList = $expenseHighlights->fetchAll();
$employeeHighlights = db()->prepare("
SELECT e.*
FROM employees e
WHERE e.tenant_id = ?
ORDER BY e.created_at DESC
LIMIT 5
");
$employeeHighlights->execute([$tenant_id]);
$employeeList = $employeeHighlights->fetchAll();
// Chart Data
$chart_days = isset($_GET['chart_days']) ? (int)$_GET['chart_days'] : 7;
if (!in_array($chart_days, [7, 14, 30])) $chart_days = 7;
$chartDataQuery = db()->prepare("
SELECT entry_date, SUM(hours) as total_hours
FROM labour_entries
WHERE tenant_id = ? AND entry_date > DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
GROUP BY entry_date
ORDER BY entry_date ASC
");
$chartDataQuery->execute([$tenant_id, $chart_days]);
$rawChartData = $chartDataQuery->fetchAll(PDO::FETCH_KEY_PAIR);
$chartLabels = [];
$chartValues = [];
for ($i = $chart_days - 1; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-$i days"));
$chartLabels[] = date('M d', strtotime($date));
$chartValues[] = (float)($rawChartData[$date] ?? 0);
}
$activities = db()->prepare("SELECT * FROM activity_log WHERE tenant_id = ? ORDER BY created_at DESC LIMIT 10");
$activities->execute([$tenant_id]);
$activityList = $activities->fetchAll();
$pageTitle = "SR&ED Manager - Dashboard";
include __DIR__ . '/includes/header.php';
?>
<!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>
</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 class="container-fluid py-4">
<!-- Dashboard Chart Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h5 class="mb-0 fw-bold">Labour Hours Overview</h5>
<div class="btn-group btn-group-sm">
<a href="?chart_days=7" class="btn btn-outline-primary <?= $chart_days == 7 ? 'active' : '' ?>">7 Days</a>
<a href="?chart_days=14" class="btn btn-outline-primary <?= $chart_days == 14 ? 'active' : '' ?>">14 Days</a>
<a href="?chart_days=30" class="btn btn-outline-primary <?= $chart_days == 30 ? 'active' : '' ?>">30 Days</a>
</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>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
<div class="card-body">
<div style="height: 300px;">
<canvas id="hoursChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<!-- Projects Highlight -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Recent Projects</h6>
<a href="projects.php" class="btn btn-sm btn-link text-decoration-none">View All</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr class="extra-small text-muted">
<th>PROJECT</th>
<th>STATUS</th>
<th class="text-end">HOURS</th>
</tr>
</thead>
<tbody>
<?php foreach ($projectList as $p): ?>
<tr>
<td>
<div class="fw-bold small"><?= htmlspecialchars($p['name']) ?></div>
<div class="extra-small text-muted"><?= htmlspecialchars($p['code']) ?></div>
</td>
<td><span class="status-badge status-<?= str_replace('_', '-', $p['status']) ?>"><?= ucfirst($p['status']) ?></span></td>
<td class="text-end fw-bold"><?= number_format((float)$p['total_hours'], 1) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Labour Highlights -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Recent Labour</h6>
<a href="labour.php" class="btn btn-sm btn-link text-decoration-none">View All</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr class="extra-small text-muted">
<th>DATE</th>
<th>EMPLOYEE</th>
<th class="text-end">HOURS</th>
</tr>
</thead>
<tbody>
<?php foreach ($labourList as $l): ?>
<tr>
<td class="extra-small text-muted"><?= $l['entry_date'] ?></td>
<td class="small fw-bold"><?= htmlspecialchars($l['employee_name']) ?></td>
<td class="text-end"><span class="badge bg-light text-primary border"><?= number_format((float)$l['hours'], 1) ?>h</span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Expense Highlights -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Recent Expenses</h6>
<a href="expenses.php" class="btn btn-sm btn-link text-decoration-none">View All</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr class="extra-small text-muted">
<th>DATE</th>
<th>SUPPLIER</th>
<th class="text-end">AMOUNT</th>
</tr>
</thead>
<tbody>
<?php foreach ($expenseList as $ex): ?>
<tr>
<td class="extra-small text-muted"><?= $ex['entry_date'] ?></td>
<td class="small fw-bold"><?= htmlspecialchars($ex['supplier_name']) ?></td>
<td class="text-end fw-bold">$<?= number_format((float)$ex['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Employee Highlights -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">New Employees</h6>
<a href="employees.php" class="btn btn-sm btn-link text-decoration-none">View All</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr class="extra-small text-muted">
<th>NAME</th>
<th>POSITION</th>
<th class="text-end">JOINED</th>
</tr>
</thead>
<tbody>
<?php foreach ($employeeList as $e): ?>
<tr>
<td class="small fw-bold"><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></td>
<td class="extra-small text-muted"><?= htmlspecialchars($e['position']) ?></td>
<td class="text-end extra-small text-muted"><?= $e['start_date'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Recent Activity Section -->
<div class="row mt-4" id="activity-section">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Recent Activity</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr class="extra-small text-muted">
<th style="width: 200px;">TIME</th>
<th>ACTION</th>
<th>DETAILS</th>
</tr>
</thead>
<tbody>
<?php foreach ($activityList as $a): ?>
<tr>
<td class="text-muted extra-small"><?= date('M d, Y H:i', strtotime($a['created_at'])) ?></td>
<td><span class="badge bg-light text-primary border extra-small"><?= htmlspecialchars($a['action']) ?></span></td>
<td class="small"><?= htmlspecialchars($a['details']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('hoursChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: <?= json_encode($chartLabels) ?>,
datasets: [{
label: 'Labour Hours',
data: <?= json_encode($chartValues) ?>,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#3b82f6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { borderDash: [5, 5] } },
x: { grid: { display: false } }
}
}
});
});
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

635
labour.php Normal file
View File

@ -0,0 +1,635 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Bulk Labour
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_labour'])) {
$project_id = (int)($_POST['bulk_project_id'] ?? 0);
$employee_id = (int)($_POST['bulk_employee_id'] ?? 0);
$labour_type_id = (int)($_POST['bulk_labour_type_id'] ?? 0);
$evidence_type_id = (int)($_POST['bulk_evidence_type_id'] ?? 0);
$notes = $_POST['bulk_notes'] ?? '';
$days = $_POST['bulk_days'] ?? []; // Array of date => hours
if ($project_id && $employee_id && !empty($days)) {
$db = db();
foreach ($days as $date => $hours) {
$hours = (float)$hours;
if ($hours <= 0) continue;
$stmt = $db->prepare("INSERT INTO labour_entries (tenant_id, project_id, employee_id, entry_date, hours, labour_type_id, evidence_type_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $project_id, $employee_id, $date, $hours, $labour_type_id, $evidence_type_id, $notes]);
}
$stmt = $db->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Bulk Labour Added', "Logged hours for employee ID $employee_id via bulk entry"]);
header("Location: labour.php?success=1");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) {
$project_id = (int)($_POST['project_id'] ?? 0);
$employee_ids = isset($_POST['employee_ids']) ? array_map('intval', $_POST['employee_ids']) : [];
if (empty($employee_ids) && isset($_POST['employee_id'])) {
$employee_ids = [(int)$_POST['employee_id']];
}
$entry_date = $_POST['entry_date'] ?? date('Y-m-d');
$hours = (float)($_POST['hours'] ?? 0);
$labour_type_id = (int)($_POST['labour_type_id'] ?? 0);
$evidence_type_id = (int)($_POST['evidence_type_id'] ?? 0);
$notes = $_POST['notes'] ?? '';
if ($project_id && !empty($employee_ids) && $hours > 0) {
$db = db();
foreach ($employee_ids as $employee_id) {
$stmt = $db->prepare("INSERT INTO labour_entries (tenant_id, project_id, employee_id, entry_date, hours, labour_type_id, evidence_type_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $project_id, $employee_id, $entry_date, $hours, $labour_type_id, $evidence_type_id, $notes]);
$labour_entry_id = (int)$db->lastInsertId();
// Handle File Uploads (only for the first one or all? Usually all if it's a team entry)
if (!empty($_FILES['attachments']['name'][0])) {
foreach ($_FILES['attachments']['tmp_name'] as $key => $tmp_name) {
if ($_FILES['attachments']['error'][$key] === UPLOAD_ERR_OK) {
$file_name = $_FILES['attachments']['name'][$key];
$file_size = $_FILES['attachments']['size'][$key];
$mime_type = $_FILES['attachments']['type'][$key];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
$new_file_name = uniqid() . '.' . $file_ext;
$file_path = 'uploads/' . $new_file_name;
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
if (move_uploaded_file($tmp_name, $file_path)) {
$thumbnail_path = null;
if (isImage($mime_type)) {
$thumb_name = 'thumb_' . $new_file_name;
$thumb_path = 'uploads/' . $thumb_name;
if (createThumbnail($file_path, $thumb_path)) {
$thumbnail_path = $thumb_path;
}
}
$stmt = $db->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, thumbnail_path, file_size, mime_type, uploaded_by) VALUES (?, 'labour_entry', ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $labour_entry_id, $file_name, $file_path, $thumbnail_path, $file_size, $mime_type, $currentUserName]);
}
}
}
}
}
$stmt = $db->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Labour Added', "Logged $hours hours for " . count($employee_ids) . " employee(s)"]);
header("Location: labour.php?success=1");
exit;
}
}
// Fetch Data
$filter_project = (int)($_GET['project_id'] ?? 0);
$filter_employee = (int)($_GET['employee_id'] ?? 0);
$filter_type = (int)($_GET['labour_type_id'] ?? 0);
$filter_evidence = (int)($_GET['evidence_type_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$where = ["le.tenant_id = ?"];
$params = [$tenant_id];
if ($filter_project) {
$where[] = "le.project_id = ?";
$params[] = $filter_project;
}
if ($filter_employee) {
$where[] = "le.employee_id = ?";
$params[] = $filter_employee;
}
if ($filter_type) {
$where[] = "le.labour_type_id = ?";
$params[] = $filter_type;
}
if ($filter_evidence) {
$where[] = "le.evidence_type_id = ?";
$params[] = $filter_evidence;
}
if ($filter_start) {
$where[] = "le.entry_date >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "le.entry_date <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$labourEntries = db()->prepare("
SELECT le.*, p.name as project_name, e.name as employee_name, lt.name as labour_type, et.name as evidence_type
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
JOIN employees e ON le.employee_id = e.id
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
LEFT JOIN evidence_types et ON le.evidence_type_id = et.id
WHERE $where_clause
ORDER BY le.entry_date DESC, le.created_at DESC
");
$labourEntries->execute($params);
$labourList = $labourEntries->fetchAll();
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
$projects->execute([$tenant_id]);
$projectList = $projects->fetchAll();
$employees = db()->prepare("SELECT id, first_name, last_name FROM employees WHERE tenant_id = ? ORDER BY first_name, last_name");
$employees->execute([$tenant_id]);
$employeeList = $employees->fetchAll();
$labourTypes = db()->prepare("SELECT * FROM labour_types WHERE tenant_id = ? ORDER BY name");
$labourTypes->execute([$tenant_id]);
$labourTypeList = $labourTypes->fetchAll();
$evidenceTypes = db()->prepare("SELECT * FROM evidence_types WHERE tenant_id = ? ORDER BY name");
$evidenceTypes->execute([$tenant_id]);
$evidenceTypeList = $evidenceTypes->fetchAll();
// Find current employee (mocked as user_id 1)
$currentUser = db()->prepare("SELECT name FROM users WHERE id = 1");
$currentUser->execute();
$currentUserName = $currentUser->fetchColumn() ?: 'System';
$currentEmployeeStmt = db()->prepare("SELECT id FROM employees WHERE user_id = 1 AND tenant_id = ?");
$currentEmployeeStmt->execute([$tenant_id]);
$currentEmployeeId = $currentEmployeeStmt->fetchColumn() ?: null;
$teams = db()->prepare("SELECT * FROM teams WHERE tenant_id = ? ORDER BY name");
$teams->execute([$tenant_id]);
$teamList = $teams->fetchAll();
$teamMembers = db()->prepare("SELECT * FROM employee_teams WHERE tenant_id = ?");
$teamMembers->execute([$tenant_id]);
$teamMemberList = $teamMembers->fetchAll();
// Group by team
$teamMembersGrouped = [];
foreach ($teamMemberList as $tm) {
$teamMembersGrouped[$tm['team_id']][] = $tm['employee_id'];
}
$pageTitle = "SR&ED Manager - Labour Tracking";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Labour Tracking</h2>
<div>
<a href="api/export_labour.php?<?= http_build_query($_GET) ?>" class="btn btn-primary me-2"><i class="bi bi-file-earmark-excel me-1"></i> Export to Excel</a>
<button class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#bulkLabourModal"><i class="bi bi-calendar3 me-1"></i> Bulk Add</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addLabourModal">+ Add Labour Entry</button>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select form-select-sm">
<option value="">All Projects</option>
<?php
$allProjects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name");
$allProjects->execute([$tenant_id]);
foreach ($allProjects->fetchAll() as $p): ?>
<option value="<?= $p['id'] ?>" <?= $filter_project == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Employee</label>
<select name="employee_id" class="form-select form-select-sm">
<option value="">All Employees</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>" <?= $filter_employee == $e['id'] ? 'selected' : '' ?>><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Labour Type</label>
<select name="labour_type_id" class="form-select form-select-sm">
<option value="">All Types</option>
<?php foreach ($labourTypeList as $lt): ?>
<option value="<?= $lt['id'] ?>" <?= $filter_type == $lt['id'] ? 'selected' : '' ?>><?= htmlspecialchars($lt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Evidence</label>
<select name="evidence_type_id" class="form-select form-select-sm">
<option value="">All Evidence</option>
<?php foreach ($evidenceTypeList as $et): ?>
<option value="<?= $et['id'] ?>" <?= $filter_evidence == $et['id'] ? 'selected' : '' ?>><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-1">
<label class="form-label small fw-bold">From</label>
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_start) ?>">
</div>
<div class="col-md-1">
<label class="form-label small fw-bold">To</label>
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_end) ?>">
</div>
<div class="col-md-2">
<div class="d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1">Filter</button>
<a href="labour.php" class="btn btn-sm btn-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Labour entry successfully saved.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Date</th>
<th>Employee</th>
<th>Project</th>
<th>Hours</th>
<th>Type / Evidence</th>
<th>Notes</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($labourList)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted">No labour entries found.</td></tr>
<?php endif; ?>
<?php foreach ($labourList as $l): ?>
<tr>
<td class="text-muted"><?= $l['entry_date'] ?></td>
<td><strong><?= htmlspecialchars($l['employee_name']) ?></strong></td>
<td><?= htmlspecialchars($l['project_name']) ?></td>
<td><span class="badge bg-light text-primary border"><?= number_format((float)$l['hours'], 2) ?> h</span></td>
<td>
<div class="small fw-bold"><?= htmlspecialchars($l['labour_type'] ?? 'N/A') ?></div>
<div class="extra-small text-muted"><?= htmlspecialchars($l['evidence_type'] ?? 'N/A') ?></div>
</td>
<td class="small text-muted"><?= htmlspecialchars($l['notes'] ?? '') ?></td>
<td class="text-end">
<button class="btn btn-sm btn-secondary">Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="addLabourModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add Labour Entry</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body">
<div class="row">
<div class="col-12 mb-4">
<label class="form-label small fw-bold d-block">Entry Mode</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="entry_mode" id="modeIndividual" value="individual" checked autocomplete="off">
<label class="btn btn-outline-primary" for="modeIndividual">Individual</label>
<input type="radio" class="btn-check" name="entry_mode" id="modeTeam" value="team" autocomplete="off">
<label class="btn btn-outline-primary" for="modeTeam">Team</label>
</div>
</div>
<div class="col-md-12 mb-3" id="individualSelect">
<label class="form-label small fw-bold">Employee</label>
<select name="employee_id" class="form-select">
<option value="">Select Employee...</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>" <?= $e['id'] == $currentEmployeeId ? 'selected' : '' ?>><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="teamSelect" style="display: none;" class="col-12">
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold">Select Team</label>
<select id="teamIdSelect" class="form-select mb-3">
<option value="">Select Team...</option>
<?php foreach ($teamList as $t): ?>
<option value="<?= $t['id'] ?>"><?= htmlspecialchars($t['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold d-block">Team Members</label>
<div id="teamMembersList" class="p-3 border rounded bg-light" style="max-height: 200px; overflow-y: auto;">
<p class="text-muted small mb-0">Select a team to see members</p>
</div>
</div>
</div>
</div>
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select" required>
<option value="">Select Project...</option>
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Date</label>
<input type="date" name="entry_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Hours</label>
<input type="number" name="hours" class="form-control" step="0.25" min="0" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Type of Labour</label>
<select name="labour_type_id" class="form-select">
<?php foreach ($labourTypeList as $lt): ?>
<option value="<?= $lt['id'] ?>"><?= htmlspecialchars($lt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Objective Evidence</label>
<select name="evidence_type_id" class="form-select">
<?php foreach ($evidenceTypeList as $et): ?>
<option value="<?= $et['id'] ?>"><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 mb-3">
<label class="form-label small fw-bold">Attachments</label>
<input type="file" name="attachments[]" class="form-control" multiple>
</div>
<div class="col-12">
<label class="form-label small fw-bold">Notes</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="add_labour" class="btn btn-primary px-4">Save Entry</button>
</div>
</form>
</div>
</div>
</div>
<!-- Bulk Entry Modal -->
<div class="modal fade" id="bulkLabourModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Bulk Labour Entry</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="row g-4">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-bold mb-0">Weekly Timesheet</h6>
<div class="d-flex gap-2">
<input type="week" id="bulkWeek" class="form-control form-control-sm" value="<?= date('Y-\WW') ?>">
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered text-center">
<thead class="bg-light">
<tr id="bulkHeader">
<!-- To be populated by JS -->
</tr>
</thead>
<tbody id="bulkBody">
<!-- To be populated by JS -->
</tbody>
</table>
</div>
<div id="existingHoursInfo" class="small text-muted mt-2"></div>
</div>
<div class="col-md-4">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="fw-bold mb-3">Labour Details</h6>
<div class="mb-3">
<label class="form-label small fw-bold">Employee</label>
<select name="bulk_employee_id" id="bulkEmployee" class="form-select form-select-sm" required>
<option value="">Select Employee...</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>" <?= $e['id'] == $currentEmployeeId ? 'selected' : '' ?>><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Project</label>
<select name="bulk_project_id" id="bulkProject" class="form-select form-select-sm" required>
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Labour Type</label>
<select name="bulk_labour_type_id" class="form-select form-select-sm">
<?php foreach ($labourTypeList as $lt): ?>
<option value="<?= $lt['id'] ?>"><?= htmlspecialchars($lt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Evidence</label>
<select name="bulk_evidence_type_id" class="form-select form-select-sm">
<?php foreach ($evidenceTypeList as $et): ?>
<option value="<?= $et['id'] ?>"><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-0">
<label class="form-label small fw-bold">Notes</label>
<textarea name="bulk_notes" class="form-control form-control-sm" rows="2"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="bulk_labour" class="btn btn-primary px-4">Save All Entries</button>
</div>
</form>
</div>
</div>
</div>
<?php
// Convert PHP arrays to JS for dynamic UI
$employeesJson = json_encode($employeeList);
$teamMembersJson = json_encode($teamMembersGrouped);
?>
<script>
const employees = <?= $employeesJson ?>;
const teamMembersGrouped = <?= $teamMembersJson ?>;
document.addEventListener('DOMContentLoaded', function() {
const modeIndividual = document.getElementById('modeIndividual');
const modeTeam = document.getElementById('modeTeam');
const individualSelect = document.getElementById('individualSelect');
const teamSelect = document.getElementById('teamSelect');
const teamIdSelect = document.getElementById('teamIdSelect');
const teamMembersList = document.getElementById('teamMembersList');
function toggleMode() {
if (modeIndividual.checked) {
individualSelect.style.display = 'block';
teamSelect.style.display = 'none';
individualSelect.querySelector('select').required = true;
} else {
individualSelect.style.display = 'none';
teamSelect.style.display = 'block';
individualSelect.querySelector('select').required = false;
}
}
modeIndividual.addEventListener('change', toggleMode);
modeTeam.addEventListener('change', toggleMode);
teamIdSelect.addEventListener('change', function() {
const teamId = this.value;
teamMembersList.innerHTML = '';
if (teamId && teamMembersGrouped[teamId]) {
const members = teamMembersGrouped[teamId];
members.forEach(empId => {
const emp = employees.find(e => e.id == empId);
if (emp) {
const div = document.createElement('div');
div.className = 'form-check';
div.innerHTML = `
<input class="form-check-input" type="checkbox" name="employee_ids[]" value="${emp.id}" id="emp_${emp.id}" checked>
<label class="form-check-label" for="emp_${emp.id}">
${emp.first_name} ${emp.last_name}
</label>
`;
teamMembersList.appendChild(div);
}
});
} else {
teamMembersList.innerHTML = '<p class="text-muted small mb-0">Select a team to see members</p>';
}
});
// Bulk Entry Logic
const bulkWeek = document.getElementById('bulkWeek');
const bulkHeader = document.getElementById('bulkHeader');
const bulkBody = document.getElementById('bulkBody');
const bulkEmployee = document.getElementById('bulkEmployee');
const existingHoursInfo = document.getElementById('existingHoursInfo');
function updateBulkView() {
const weekStr = bulkWeek.value;
if (!weekStr) return;
const [year, week] = weekStr.split('-W').map(Number);
const startDate = getStartDateOfWeek(week, year);
bulkHeader.innerHTML = '';
bulkBody.innerHTML = '<tr>';
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const dates = [];
for (let i = 0; i < 7; i++) {
const d = new Date(startDate);
d.setDate(startDate.getDate() + i);
const dateStr = d.toISOString().split('T')[0];
dates.push(dateStr);
const th = document.createElement('th');
th.innerHTML = `${days[i]}<br><small class="fw-normal text-muted">${d.getMonth() + 1}/${d.getDate()}</small>`;
bulkHeader.appendChild(th);
const td = document.createElement('td');
td.innerHTML = `
<input type="number" name="bulk_days[${dateStr}]" class="form-control text-center bulk-hour-input" step="0.5" min="0" placeholder="0">
<div class="extra-small text-muted mt-1 existing-hours" data-date="${dateStr}"></div>
`;
bulkBody.appendChild(td);
}
bulkBody.innerHTML += '</tr>';
fetchExistingHours(dates[0], dates[6]);
}
function getStartDateOfWeek(w, y) {
const d = new Date(y, 0, 1 + (w - 1) * 7);
const dow = d.getDay();
const ISOweekStart = d;
if (dow <= 4)
ISOweekStart.setDate(d.getDate() - d.getDay());
else
ISOweekStart.setDate(d.getDate() + 8 - d.getDay());
return ISOweekStart;
}
function fetchExistingHours(start, end) {
const empId = bulkEmployee.value;
if (!empId) return;
fetch(`api/get_labour_stats.php?employee_id=${empId}&start_date=${start}&end_date=${end}`)
.then(res => res.json())
.then(data => {
document.querySelectorAll('.existing-hours').forEach(el => {
const date = el.dataset.date;
const record = data.find(d => d.entry_date === date);
if (record) {
el.innerHTML = `Total: ${record.total_hours}h`;
el.classList.add('text-primary', 'fw-bold');
} else {
el.innerHTML = 'Total: 0h';
el.classList.remove('text-primary', 'fw-bold');
}
});
});
}
bulkWeek.addEventListener('change', updateBulkView);
bulkEmployee.addEventListener('change', updateBulkView);
// Initial update
updateBulkView();
});
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

165
labour_files.php Normal file
View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Filters
$project_filter = $_GET['project_id'] ?? '';
$start_date = $_GET['start_date'] ?? '';
$end_date = $_GET['end_date'] ?? '';
$include_archived = isset($_GET['include_archived']) && $_GET['include_archived'] === '1';
// Get Projects for filter
$projects_sql = "SELECT id, name FROM projects WHERE tenant_id = ?";
if (!$include_archived) {
$projects_sql .= " AND is_archived = 0";
}
$projects_sql .= " ORDER BY name ASC";
$projects_stmt = db()->prepare($projects_sql);
$projects_stmt->execute([$tenant_id]);
$all_projects = $projects_stmt->fetchAll();
$where_clauses = ["a.tenant_id = ?", "a.entity_type = 'labour_entry'"];
$params = [$tenant_id];
if (!$include_archived) {
$where_clauses[] = "p.is_archived = 0";
}
if ($project_filter) {
$where_clauses[] = "le.project_id = ?";
$params[] = $project_filter;
}
if ($start_date) {
$where_clauses[] = "le.entry_date >= ?";
$params[] = $start_date;
}
if ($end_date) {
$where_clauses[] = "le.entry_date <= ?";
$params[] = $end_date;
}
$where_sql = implode(" AND ", $where_clauses);
// Fetch Labour Files
$stmt = db()->prepare("
SELECT a.*, le.entry_date, e.name as employee_name, p.name as project_name
FROM attachments a
JOIN labour_entries le ON a.entity_id = le.id
JOIN employees e ON le.employee_id = e.id
JOIN projects p ON le.project_id = p.id
WHERE $where_sql
ORDER BY a.created_at DESC
");
$stmt->execute($params);
$files = $stmt->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];
}
$pageTitle = "SR&ED Manager - Labour Files";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Labour Files</h2>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select">
<option value="">All Projects</option>
<?php foreach ($all_projects as $p): ?>
<option value="<?= $p['id'] ?>" <?= $project_filter == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($start_date) ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">End Date</label>
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($end_date) ?>">
</div>
<div class="col-md-2 d-flex align-items-center mt-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="include_archived" id="includeArchived" value="1" <?= $include_archived ? 'checked' : '' ?> onchange="this.form.submit()">
<label class="form-check-label small" for="includeArchived">Include Archived</label>
</div>
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="d-grid w-100 gap-2 d-md-flex">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="labour_files.php" class="btn btn-outline-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Filename</th>
<th>Size</th>
<th>Uploaded By</th>
<th>Created At</th>
<th>Linked Entry</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($files)): ?>
<tr><td colspan="6" class="text-center py-5 text-muted">No labour files found.</td></tr>
<?php endif; ?>
<?php foreach ($files as $f): ?>
<tr>
<td>
<i class="bi bi-file-earmark-text me-2 text-primary"></i>
<strong><?= htmlspecialchars($f['file_name']) ?></strong>
</td>
<td><small class="text-muted"><?= formatBytes((int)$f['file_size']) ?></small></td>
<td>
<div class="d-flex align-items-center">
<div class="bg-light rounded-circle p-1 me-2" style="width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-person small"></i>
</div>
<small><?= htmlspecialchars($f['uploaded_by'] ?? 'System') ?></small>
</div>
</td>
<td class="small text-muted"><?= date('M j, Y H:i', strtotime($f['created_at'])) ?></td>
<td>
<div class="small">
<span class="fw-bold text-dark"><?= $f['entry_date'] ?></span><br>
<span class="text-muted"><?= htmlspecialchars($f['employee_name']) ?> - <?= htmlspecialchars($f['project_name']) ?></span>
</div>
</td>
<td class="text-end">
<a href="<?= htmlspecialchars($f['file_path']) ?>" target="_blank" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

78
login.php Normal file
View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
ob_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
if (Auth::isLoggedIn()) {
header('Location: index.php', true, 302);
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$password = trim($_POST['password'] ?? '');
$stmt = db()->prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
Auth::login((int)$user['id'], (int)$user['tenant_id'], (string)$user['role']);
session_write_close();
header('Location: index.php', true, 302);
exit;
} else {
$error = 'Invalid email or password.';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login - SR&ED Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
.btn-primary:hover { background-color: #2563eb; }
</style>
</head>
<body>
<div class="card login-card p-4">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary">SR&ED MANAGER</h3>
<p class="text-muted small">Sign in to your account</p>
</div>
<?php if ($error): ?>
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label small fw-bold">Email Address</label>
<input type="email" name="email" class="form-control" placeholder="name@company.com" required autofocus>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<label class="form-label small fw-bold">Password</label>
<a href="forgot_password.php" class="small text-decoration-none">Forgot?</a>
</div>
<input type="password" name="password" class="form-control" placeholder="••••••••" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember">
<label class="form-check-label small" for="remember">Remember me</label>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">Login</button>
</form>
</div>
</body>
</html>

4
logout.php Normal file
View File

@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/auth_helper.php';
Auth::logout();

187
project_detail.php Normal file
View File

@ -0,0 +1,187 @@
<?php
require_once __DIR__ . '/db/config.php';
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: projects.php');
exit;
}
$db = db();
$project = $db->prepare("SELECT * FROM projects WHERE id = ?");
$project->execute([$id]);
$project = $project->fetch();
if (!$project) {
die("Project not found.");
}
$pageTitle = "Project Detail: " . htmlspecialchars($project['name']);
include __DIR__ . '/includes/header.php';
// Fetch recent labour
$labourStmt = $db->prepare("
SELECT l.*, e.name as employee_name, lt.name as labour_type
FROM labour_entries l
JOIN employees e ON l.employee_id = e.id
JOIN labour_types lt ON l.labour_type_id = lt.id
WHERE l.project_id = ?
ORDER BY l.entry_date DESC
LIMIT 10
");
$labourStmt->execute([$id]);
$labourEntries = $labourStmt->fetchAll();
// Fetch recent expenses
$expenseStmt = $db->prepare("
SELECT ex.*, et.name as expense_type, s.name as supplier_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
WHERE ex.project_id = ?
ORDER BY ex.entry_date DESC
LIMIT 10
");
$expenseStmt->execute([$id]);
$expenseEntries = $expenseStmt->fetchAll();
// Stats
$statsStmt = $db->prepare("
SELECT
(SELECT SUM(hours) FROM labour_entries WHERE project_id = ?) as total_hours,
(SELECT SUM(amount) FROM expenses WHERE project_id = ?) as total_expenses
");
$statsStmt->execute([$id, $id]);
$stats = $statsStmt->fetch();
?>
<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="projects.php" class="text-decoration-none">Projects</a></li>
<li class="breadcrumb-item active" aria-current="page"><?= htmlspecialchars($project['name']) ?></li>
</ol>
</nav>
<h1 class="h3 mb-0"><?= htmlspecialchars($project['name']) ?> (<?= htmlspecialchars($project['code']) ?>)</h1>
<span class="badge status-<?= $project['status'] ?>"><?= ucfirst($project['status']) ?></span>
<?php if ($project['is_archived']): ?>
<span class="badge bg-secondary">Archived</span>
<?php endif; ?>
</div>
<div>
<?php if ($project['is_archived']): ?>
<a href="projects.php?unarchive=<?= $id ?>" class="btn btn-outline-success btn-sm me-2" onclick="return confirm('Unarchive this project?')"><i class="bi bi-arrow-up-circle me-1"></i> Unarchive</a>
<?php else: ?>
<a href="projects.php?archive=<?= $id ?>" class="btn btn-outline-danger btn-sm me-2" onclick="return confirm('Archive this project?')"><i class="bi bi-archive me-1"></i> Archive Project</a>
<?php endif; ?>
<button class="btn btn-outline-primary btn-sm me-2"><i class="bi bi-pencil me-1"></i> Edit Project</button>
<?php if (!$project['is_archived']): ?>
<a href="labour.php?project_id=<?= $id ?>" class="btn btn-primary btn-sm"><i class="bi bi-plus-lg me-1"></i> Add Labour</a>
<?php endif; ?>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<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">Budget: <?= number_format($project['estimated_hours'] ?? 0) ?></small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="text-muted mb-2">Total Expenses</h6>
<h3 class="mb-0 text-success">$<?= number_format($stats['total_expenses'] ?? 0, 2) ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="text-muted mb-2">Type</h6>
<h3 class="mb-0 text-dark"><?= $project['type'] ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="text-muted mb-2">Start Date</h6>
<h3 class="mb-0 text-dark"><?= date('M j, Y', strtotime($project['start_date'])) ?></h3>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Recent Labour</span>
<a href="labour.php?project_id=<?= $id ?>" class="small text-decoration-none">View All</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Hours</th>
</tr>
</thead>
<tbody>
<?php foreach ($labourEntries as $le): ?>
<tr>
<td><?= date('Y-m-d', strtotime($le['entry_date'])) ?></td>
<td><?= htmlspecialchars($le['employee_name']) ?></td>
<td class="fw-bold text-primary"><?= number_format($le['hours'], 1) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Recent Expenses</span>
<a href="expenses.php?project_id=<?= $id ?>" class="small text-decoration-none">View All</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($expenseEntries as $ee): ?>
<tr>
<td><?= date('Y-m-d', strtotime($ee['entry_date'])) ?></td>
<td><?= htmlspecialchars($ee['expense_type']) ?></td>
<td class="fw-bold text-success">$<?= number_format($ee['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

319
projects.php Normal file
View File

@ -0,0 +1,319 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Add Project
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) {
$name = $_POST['name'] ?? '';
$code = $_POST['code'] ?? '';
$start_date = $_POST['start_date'] ?? date('Y-m-d');
$owner_id = !empty($_POST['owner_id']) ? (int)$_POST['owner_id'] : null;
$est_completion = !empty($_POST['estimated_completion_date']) ? $_POST['estimated_completion_date'] : null;
$type = $_POST['type'] ?? 'Internal';
$est_hours = (float)($_POST['estimated_hours'] ?? 0);
if ($name && $code) {
$stmt = db()->prepare("INSERT INTO projects (tenant_id, name, code, start_date, owner_id, estimated_completion_date, type, estimated_hours) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $name, $code, $start_date, $owner_id, $est_completion, $type, $est_hours]);
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Project Created', "Added project: $name ($code)"]);
header("Location: projects.php?success=1");
exit;
}
}
// Handle Archive/Unarchive
if (isset($_GET['archive'])) {
$id = (int)$_GET['archive'];
$stmt = db()->prepare("UPDATE projects SET is_archived = 1 WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
header("Location: projects.php?archived=1");
exit;
}
if (isset($_GET['unarchive'])) {
$id = (int)$_GET['unarchive'];
$stmt = db()->prepare("UPDATE projects SET is_archived = 0 WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
header("Location: projects.php?unarchived=1");
exit;
}
// Fetch Data
$search = $_GET['search'] ?? '';
$status_filter = $_GET['status'] ?? '';
$date_preset = $_GET['date_preset'] ?? '';
$start_from = $_GET['start_from'] ?? '';
$start_to = $_GET['start_to'] ?? '';
$include_archived = isset($_GET['include_archived']) && $_GET['include_archived'] === '1';
$query = "
SELECT p.*,
CONCAT(e.first_name, ' ', e.last_name) as owner_name,
COALESCE((SELECT SUM(hours) FROM labour_entries WHERE project_id = p.id), 0) as total_hours
FROM projects p
LEFT JOIN employees e ON p.owner_id = e.id
WHERE p.tenant_id = ?";
$params = [$tenant_id];
if (!$include_archived) {
$query .= " AND p.is_archived = 0";
}
if ($search) {
$query .= " AND (p.name LIKE ? OR p.code LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($status_filter) {
$query .= " AND p.status = ?";
$params[] = $status_filter;
}
if ($date_preset && $date_preset !== 'custom') {
switch ($date_preset) {
case 'today': $query .= " AND p.start_date = CURRENT_DATE"; break;
case 'this_week': $query .= " AND p.start_date >= DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)"; break;
case 'last_week': $query .= " AND p.start_date >= DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) + 7 DAY) AND p.start_date < DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)"; break;
case 'this_month': $query .= " AND p.start_date >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')"; break;
case 'last_month': $query .= " AND p.start_date >= DATE_FORMAT(DATE_SUB(CURRENT_DATE, INTERVAL 1 MONTH), '%Y-%m-01') AND p.start_date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')"; break;
case 'this_year': $query .= " AND p.start_date >= DATE_FORMAT(CURRENT_DATE, '%Y-01-01')"; break;
case 'last_year': $query .= " AND p.start_date >= DATE_FORMAT(DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR), '%Y-01-01') AND p.start_date < DATE_FORMAT(CURRENT_DATE, '%Y-01-01')"; break;
}
} elseif ($date_preset === 'custom' && $start_from && $start_to) {
$query .= " AND p.start_date BETWEEN ? AND ?";
$params[] = $start_from;
$params[] = $start_to;
}
$query .= " ORDER BY p.created_at DESC";
$projects = db()->prepare($query);
$projects->execute($params);
$projectList = $projects->fetchAll();
$employees = db()->prepare("SELECT id, first_name, last_name FROM employees WHERE tenant_id = ? ORDER BY first_name, last_name");
$employees->execute([$tenant_id]);
$employeeList = $employees->fetchAll();
$pageTitle = "SR&ED Manager - Projects";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Projects</h2>
<div class="d-flex gap-2">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">+ New Project</button>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully created.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['archived'])): ?>
<div class="alert alert-info alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully archived.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['unarchived'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully unarchived.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold">Search</label>
<div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control" placeholder="Project name or code..." value="<?= htmlspecialchars($search) ?>">
</div>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">All Statuses</option>
<option value="active" <?= $status_filter === 'active' ? 'selected' : '' ?>>Active</option>
<option value="on_hold" <?= $status_filter === 'on_hold' ? 'selected' : '' ?>>On Hold</option>
<option value="completed" <?= $status_filter === 'completed' ? 'selected' : '' ?>>Completed</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<div class="d-flex gap-2">
<select name="date_preset" class="form-select form-select-sm" onchange="handleDatePreset(this.value)" style="width: 140px;">
<option value="">Any Time</option>
<option value="this_month" <?= $date_preset === 'this_month' ? 'selected' : '' ?>>This Month</option>
<option value="this_year" <?= $date_preset === 'this_year' ? 'selected' : '' ?>>This Year</option>
<option value="custom" <?= $date_preset === 'custom' ? 'selected' : '' ?>>Custom...</option>
</select>
<div id="customDateRange" class="d-flex gap-1 <?= $date_preset === 'custom' ? '' : 'd-none' ?>">
<input type="date" name="start_from" class="form-control form-control-sm" value="<?= htmlspecialchars($start_from) ?>" placeholder="From">
<input type="date" name="start_to" class="form-control form-control-sm" value="<?= htmlspecialchars($start_to) ?>" placeholder="To">
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-check form-switch mb-1">
<input class="form-check-input" type="checkbox" name="include_archived" id="includeArchived" value="1" <?= $include_archived ? 'checked' : '' ?>>
<label class="form-check-label small fw-bold" for="includeArchived">Archived</label>
</div>
</div>
<div class="col-md-2 text-end">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1">Filter</button>
<a href="projects.php" class="btn btn-sm btn-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<div class="row position-relative">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Project Name</th>
<th>Owner</th>
<th>Type</th>
<th>Hours (Logged/Est)</th>
<th>Variance</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($projectList)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted">No projects found.</td></tr>
<?php endif; ?>
<?php foreach ($projectList as $p):
$total_hours = (float)$p['total_hours'];
$est_hours = (float)$p['estimated_hours'];
$variance = $est_hours - $total_hours;
$is_over = $total_hours > $est_hours && $est_hours > 0;
?>
<tr>
<td>
<strong><a href="project_detail.php?id=<?= $p['id'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($p['name']) ?></a></strong>
<?php if ($p['is_archived']): ?>
<span class="badge bg-secondary extra-small ms-1">Archived</span>
<?php endif; ?><br>
<code class="extra-small text-primary"><?= htmlspecialchars($p['code']) ?></code>
</td>
<td><small><?= htmlspecialchars($p['owner_name'] ?: 'Unassigned') ?></small></td>
<td><span class="badge bg-light text-dark border"><?= $p['type'] ?></span></td>
<td>
<span class="fw-bold <?= $is_over ? 'text-danger' : '' ?>"><?= number_format($total_hours, 1) ?></span>
<span class="text-muted">/ <?= number_format($est_hours, 1) ?></span>
</td>
<td>
<span class="fw-bold <?= $is_over ? 'text-danger' : 'text-success' ?>">
<?= ($variance >= 0 ? '+' : '') . number_format($variance, 1) ?>
</span>
</td>
<td><span class="status-badge status-<?= str_replace('_', '-', $p['status']) ?>"><?= ucfirst(str_replace('_', ' ', $p['status'])) ?></span></td>
<td class="text-end">
<div class="dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">Actions</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="project_detail.php?id=<?= $p['id'] ?>">View Details</a></li>
<li><hr class="dropdown-divider"></li>
<?php if ($p['is_archived']): ?>
<li><a class="dropdown-item" href="projects.php?unarchive=<?= $p['id'] ?>" onclick="return confirm('Unarchive this project?')">Unarchive</a></li>
<?php else: ?>
<li><a class="dropdown-item text-danger" href="projects.php?archive=<?= $p['id'] ?>" onclick="return confirm('Are you sure you want to archive this project? Future hours and expenses will be limited.')">Archive</a></li>
<?php endif; ?>
</ul>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="addProjectModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label small fw-bold">Project Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold">Project Code</label>
<input type="text" name="code" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Project Owner</label>
<select name="owner_id" class="form-select">
<option value="">Select Owner...</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>"><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Project Type</label>
<select name="type" class="form-select">
<option value="Internal">Internal</option>
<option value="SRED">SR&ED</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Estimated Hours</label>
<input type="number" name="estimated_hours" class="form-control" step="0.5" min="0" value="0">
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="add_project" class="btn btn-primary px-4">Create Project</button>
</div>
</form>
</div>
</div>
</div>
<script>
function handleDatePreset(val) {
const custom = document.getElementById('customDateRange');
if (val === 'custom') {
custom.classList.remove('d-none');
} else {
custom.classList.add('d-none');
}
}
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

339
reports.php Normal file
View File

@ -0,0 +1,339 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
// Simulate Tenant Context
$tenant_id = (int)$_SESSION['tenant_id'];
$report_type = $_GET['report_type'] ?? 'labour_export';
// Filter Data for Export Report
$f_employee = $_GET['employee_id'] ?? '';
$f_team = $_GET['team_id'] ?? '';
$f_start = $_GET['start_date'] ?? date('Y-m-01');
$f_end = $_GET['end_date'] ?? date('Y-m-t');
$f_projects = $_GET['projects'] ?? []; // Array
$f_activity = $_GET['activity_type_id'] ?? '';
// Handle Results for Labour Export
$exportResults = [];
$chartData = [];
if ($report_type === 'labour_export') {
$query = "
SELECT le.*, p.name as project_name, e.name as employee_name, lt.name as labour_type
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
JOIN employees e ON le.employee_id = e.id
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
WHERE le.tenant_id = ? AND le.entry_date BETWEEN ? AND ?
";
$params = [$tenant_id, $f_start, $f_end];
if ($f_employee) {
$query .= " AND le.employee_id = ?";
$params[] = $f_employee;
}
if ($f_team) {
$query .= " AND le.employee_id IN (SELECT employee_id FROM employee_teams WHERE team_id = ?)";
$params[] = $f_team;
}
if (!empty($f_projects)) {
$placeholders = implode(',', array_fill(0, count($f_projects), '?'));
$query .= " AND le.project_id IN ($placeholders)";
foreach ($f_projects as $pid) $params[] = $pid;
}
if ($f_activity) {
$query .= " AND le.labour_type_id = ?";
$params[] = $f_activity;
}
$query .= " ORDER BY le.entry_date ASC";
$stmt = db()->prepare($query);
$stmt->execute($params);
$exportResults = $stmt->fetchAll();
// Group for Chart (Hours per Day)
$grouped = [];
foreach ($exportResults as $row) {
$grouped[$row['entry_date']] = ($grouped[$row['entry_date']] ?? 0) + (float)$row['hours'];
}
ksort($grouped);
$chartData = [
'labels' => array_keys($grouped),
'values' => array_values($grouped)
];
}
// Handle Calendar Report
$cal_month = $_GET['month'] ?? date('Y-m');
$calendarData = [];
if ($report_type === 'calendar') {
$stmt = db()->prepare("
SELECT entry_date, SUM(hours) as total_hours
FROM labour_entries
WHERE tenant_id = ? AND DATE_FORMAT(entry_date, '%Y-%m') = ?
GROUP BY entry_date
");
$stmt->execute([$tenant_id, $cal_month]);
$calendarData = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
}
// Fetch helper data
$employees = db()->prepare("SELECT id, name FROM employees WHERE tenant_id = ? ORDER BY name");
$employees->execute([$tenant_id]);
$employeeList = $employees->fetchAll();
$teams = db()->prepare("SELECT id, name FROM teams WHERE tenant_id = ? ORDER BY name");
$teams->execute([$tenant_id]);
$teamList = $teams->fetchAll();
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name");
$projects->execute([$tenant_id]);
$projectList = $projects->fetchAll();
$labourTypes = db()->prepare("SELECT id, name FROM labour_types WHERE tenant_id = ? ORDER BY name");
$labourTypes->execute([$tenant_id]);
$labourTypeList = $labourTypes->fetchAll();
$pageTitle = "SR&ED Manager - Reports";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<ul class="nav nav-pills mb-3">
<li class="nav-item">
<a class="nav-link <?= $report_type === 'labour_export' ? 'active' : '' ?>" href="reports.php?report_type=labour_export">Labour Export</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $report_type === 'calendar' ? 'active' : '' ?>" href="reports.php?report_type=calendar">Monthly Calendar</a>
</li>
<li class="nav-item">
<a class="nav-link" href="sred_claim_report_selector.php">SRED Claim Report</a>
</li>
<li class="nav-item">
<a class="nav-link" href="reports_media.php">Media Gallery</a>
</li>
</ul>
<?php if ($report_type === 'labour_export'): ?>
<form method="GET" class="row g-3 align-items-end">
<input type="hidden" name="report_type" value="labour_export">
<div class="col-md-3">
<label class="form-label small fw-bold">Employee</label>
<select name="employee_id" class="form-select form-select-sm">
<option value="">All Employees</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>" <?= $f_employee == $e['id'] ? 'selected' : '' ?>><?= htmlspecialchars($e['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Team</label>
<select name="team_id" class="form-select form-select-sm">
<option value="">All Teams</option>
<?php foreach ($teamList as $t): ?>
<option value="<?= $t['id'] ?>" <?= $f_team == $t['id'] ? 'selected' : '' ?>><?= htmlspecialchars($t['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= $f_start ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">End Date</label>
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= $f_end ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Projects (Multi-select)</label>
<select name="projects[]" class="form-select form-select-sm" multiple style="height: 100px;">
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>" <?= in_array($p['id'], $f_projects) ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
<div class="extra-small text-muted">Hold Ctrl/Cmd to select multiple</div>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Activity Type</label>
<select name="activity_type_id" class="form-select form-select-sm">
<option value="">All Activities</option>
<?php foreach ($labourTypeList as $lt): ?>
<option value="<?= $lt['id'] ?>" <?= $f_activity == $lt['id'] ? 'selected' : '' ?>><?= htmlspecialchars($lt['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-sm btn-primary w-100">Generate Report</button>
</div>
</form>
<?php else: ?>
<form method="GET" class="row g-3 align-items-end">
<input type="hidden" name="report_type" value="calendar">
<div class="col-md-3">
<label class="form-label small fw-bold">Select Month</label>
<input type="month" name="month" class="form-control form-control-sm" value="<?= $cal_month ?>">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-sm btn-primary w-100">View Calendar</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php if ($report_type === 'labour_export'): ?>
<div class="row">
<div class="col-lg-8">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Detailed Labour Records</h6>
<div class="btn-group">
<button class="btn btn-sm btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i> Print</button>
<a href="api/export_labour.php?<?= http_build_query($_GET) ?>" class="btn btn-sm btn-primary"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
<a href="export_pdf.php?<?= http_build_query($_GET) ?>" class="btn btn-sm btn-primary ms-1"><i class="bi bi-file-earmark-pdf"></i> Download PDF</a>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Date</th>
<th>Employee</th>
<th>Project</th>
<th>Activity</th>
<th class="text-end">Hours</th>
</tr>
</thead>
<tbody>
<?php if (empty($exportResults)): ?>
<tr><td colspan="5" class="text-center py-5 text-muted">No records found for the selected criteria.</td></tr>
<?php endif; ?>
<?php $total = 0; foreach ($exportResults as $row): $total += (float)$row['hours']; ?>
<tr>
<td><?= $row['entry_date'] ?></td>
<td><strong><?= htmlspecialchars($row['employee_name']) ?></strong></td>
<td><?= htmlspecialchars($row['project_name']) ?></td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($row['labour_type'] ?? 'Uncategorized') ?></span></td>
<td class="text-end fw-bold"><?= number_format((float)$row['hours'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<?php if (!empty($exportResults)): ?>
<tfoot class="bg-light">
<tr>
<td colspan="4" class="text-end fw-bold">Total Hours:</td>
<td class="text-end fw-bold text-primary"><?= number_format($total, 2) ?></td>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold">Hours Breakdown</h6>
</div>
<div class="card-body">
<canvas id="exportChart" style="height: 300px;"></canvas>
</div>
</div>
</div>
</div>
<script>
const ctx = document.getElementById('exportChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: <?= json_encode($chartData['labels']) ?>,
datasets: [{
label: 'Hours',
data: <?= json_encode($chartData['values']) ?>,
backgroundColor: 'rgba(13, 110, 253, 0.2)',
borderColor: '#0d6efd',
borderWidth: 2,
borderRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { display: false } },
x: { grid: { display: false } }
}
}
});
</script>
<?php elseif ($report_type === 'calendar'): ?>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Monthly Labour Calendar - <?= date('F Y', strtotime($cal_month)) ?></h6>
<div class="btn-group">
<button class="btn btn-sm btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i> Print</button>
<a href="export_pdf.php?<?= http_build_query($_GET) ?>" class="btn btn-sm btn-primary"><i class="bi bi-file-earmark-pdf"></i> Download PDF</a>
</div>
</div>
<div class="card-body p-0">
<div class="calendar-grid">
<div class="calendar-header-day">Sun</div>
<div class="calendar-header-day">Mon</div>
<div class="calendar-header-day">Tue</div>
<div class="calendar-header-day">Wed</div>
<div class="calendar-header-day">Thu</div>
<div class="calendar-header-day">Fri</div>
<div class="calendar-header-day">Sat</div>
<?php
$first_day = date('Y-m-01', strtotime($cal_month));
$days_in_month = date('t', strtotime($cal_month));
$start_day_of_week = (int)date('w', strtotime($first_day));
// Fill empty days before first of month
for ($i = 0; $i < $start_day_of_week; $i++) {
echo '<div class="calendar-day other-month"></div>';
}
// Days of month
for ($day = 1; $day <= $days_in_month; $day++) {
$current_date = date('Y-m-', strtotime($cal_month)) . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
$hours = $calendarData[$current_date] ?? 0;
?>
<div class="calendar-day">
<span class="day-number"><?= $day ?></span>
<?php if ($hours > 0): ?>
<span class="day-hours"><?= number_format((float)$hours, 1) ?><small class="ms-1 h6 mb-0" style="font-size: 0.6rem;">h</small></span>
<?php endif; ?>
</div>
<?php
}
// Fill remaining days to complete grid
$total_cells = ceil(($start_day_of_week + $days_in_month) / 7) * 7;
for ($i = ($start_day_of_week + $days_in_month); $i < $total_cells; $i++) {
echo '<div class="calendar-day other-month"></div>';
}
?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

228
reports_media.php Normal file
View File

@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = (int)$_SESSION['tenant_id'];
// Filters
$filter_author = $_GET['author'] ?? '';
$filter_project = (int)($_GET['project_id'] ?? 0);
$filter_evidence = (int)($_GET['evidence_type_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$where = ["a.tenant_id = ?", "(a.mime_type LIKE 'image/%' OR a.mime_type LIKE 'video/%')"];
$params = [$tenant_id];
if ($filter_author) {
$where[] = "a.uploaded_by = ?";
$params[] = $filter_author;
}
if ($filter_project) {
$where[] = "COALESCE(le.project_id, ex.project_id) = ?";
$params[] = $filter_project;
}
if ($filter_evidence) {
$where[] = "le.evidence_type_id = ?";
$params[] = $filter_evidence;
}
if ($filter_start) {
$where[] = "DATE(a.created_at) >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "DATE(a.created_at) <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$query = "
SELECT a.*,
p.name as project_name,
et.name as evidence_type_name
FROM attachments a
LEFT JOIN labour_entries le ON a.entity_type = 'labour_entry' AND a.entity_id = le.id
LEFT JOIN expenses ex ON a.entity_type = 'expense' AND a.entity_id = ex.id
LEFT JOIN projects p ON p.id = COALESCE(le.project_id, ex.project_id)
LEFT JOIN evidence_types et ON le.evidence_type_id = et.id
WHERE $where_clause
ORDER BY a.created_at DESC
";
$stmt = db()->prepare($query);
$stmt->execute($params);
$mediaItems = $stmt->fetchAll();
// Get filter options
$authors = db()->prepare("SELECT DISTINCT uploaded_by FROM attachments WHERE tenant_id = ? AND uploaded_by IS NOT NULL ORDER BY uploaded_by");
$authors->execute([$tenant_id]);
$authorList = $authors->fetchAll(PDO::FETCH_COLUMN);
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
$projects->execute([$tenant_id]);
$projectList = $projects->fetchAll();
$evidenceTypes = db()->prepare("SELECT id, name FROM evidence_types WHERE tenant_id = ? ORDER BY name");
$evidenceTypes->execute([$tenant_id]);
$evidenceTypeList = $evidenceTypes->fetchAll();
$pageTitle = "SR&ED Manager - Media Gallery";
include __DIR__ . '/includes/header.php';
?>
<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="reports.php" class="text-decoration-none text-muted">Reports</a></li>
<li class="breadcrumb-item active" aria-current="page">Media Gallery</li>
</ol>
</nav>
<h2 class="fw-bold mb-0">Media Gallery</h2>
</div>
<div class="text-muted small">
Total Items: <?= count($mediaItems) ?>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select form-select-sm">
<option value="">All Projects</option>
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>" <?= $filter_project == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Author</label>
<select name="author" class="form-select form-select-sm">
<option value="">All Authors</option>
<?php foreach ($authorList as $author): ?>
<option value="<?= htmlspecialchars($author) ?>" <?= $filter_author == $author ? 'selected' : '' ?>><?= htmlspecialchars($author) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Objective Evidence</label>
<select name="evidence_type_id" class="form-select form-select-sm">
<option value="">All Evidence</option>
<?php foreach ($evidenceTypeList as $et): ?>
<option value="<?= $et['id'] ?>" <?= $filter_evidence == $et['id'] ? 'selected' : '' ?>><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">From</label>
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_start) ?>">
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">To</label>
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_end) ?>">
</div>
<div class="col-md-1">
<div class="d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1">Filter</button>
<a href="reports_media.php" class="btn btn-sm btn-outline-secondary" title="Reset"><i class="bi bi-x-lg"></i></a>
</div>
</div>
</form>
</div>
</div>
<?php if (empty($mediaItems)): ?>
<div class="card border-0 shadow-sm text-center py-5">
<div class="card-body">
<i class="bi bi-images text-muted" style="font-size: 3rem;"></i>
<h5 class="mt-3 text-muted">No media files found</h5>
<p class="text-muted small">Try adjusting your filters or upload some images/videos.</p>
<a href="reports_media.php" class="btn btn-sm btn-outline-secondary">Reset All Filters</a>
</div>
</div>
<?php else: ?>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-4">
<?php foreach ($mediaItems as $item):
$isImg = isImage($item['mime_type']);
$isVideo = isVideo($item['mime_type']);
$thumb = $item['thumbnail_path'] ?: $item['file_path'];
// If it's a video and no thumbnail, show a placeholder icon
$showThumb = true;
if ($isVideo && !$item['thumbnail_path']) {
$showThumb = false;
}
?>
<div class="col">
<div class="card h-100 border-0 shadow-sm overflow-hidden position-relative group">
<div class="ratio ratio-1x1 bg-light">
<?php if ($showThumb): ?>
<img src="<?= htmlspecialchars($thumb) ?>" class="object-fit-cover w-100 h-100 transition" alt="<?= htmlspecialchars($item['file_name']) ?>">
<?php else: ?>
<div class="d-flex align-items-center justify-content-center bg-dark">
<i class="bi bi-play-circle text-white" style="font-size: 2.5rem;"></i>
</div>
<?php endif; ?>
<div class="overlay d-flex align-items-center justify-content-center bg-dark bg-opacity-50 opacity-0 transition">
<div class="d-flex gap-2">
<a href="<?= htmlspecialchars($item['file_path']) ?>" target="_blank" class="btn btn-sm btn-light rounded-circle shadow-sm" title="View Full">
<i class="bi bi-eye"></i>
</a>
<a href="<?= htmlspecialchars($item['file_path']) ?>" download class="btn btn-sm btn-light rounded-circle shadow-sm" title="Download">
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="badge <?= $isImg ? 'bg-primary' : 'bg-danger' ?> extra-small">
<?= $isImg ? 'IMAGE' : 'VIDEO' ?>
</span>
<small class="text-muted extra-small"><?= date('M j, Y', strtotime($item['created_at'])) ?></small>
</div>
<div class="text-truncate small fw-bold text-dark" title="<?= htmlspecialchars($item['file_name']) ?>">
<?= htmlspecialchars($item['file_name']) ?>
</div>
<div class="extra-small text-muted text-truncate mt-1">
<i class="bi bi-folder2 me-1"></i> <?= htmlspecialchars($item['project_name'] ?: 'No Project') ?>
</div>
<?php if ($item['evidence_type_name']): ?>
<div class="extra-small text-muted text-truncate">
<i class="bi bi-shield-check me-1"></i> <?= htmlspecialchars($item['evidence_type_name']) ?>
</div>
<?php endif; ?>
<div class="d-flex align-items-center mt-2 pt-2 border-top">
<div class="bg-light rounded-circle p-1 me-2" style="width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-person extra-small"></i>
</div>
<span class="extra-small text-muted"><?= htmlspecialchars($item['uploaded_by'] ?: 'System') ?></span>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<style>
.group:hover .overlay { opacity: 1 !important; }
.transition { transition: all 0.3s ease; }
.extra-small { font-size: 0.7rem; }
.object-fit-cover { object-fit: cover; }
.bg-opacity-50 { background-color: rgba(0,0,0,0.5) !important; }
.opacity-0 { opacity: 0; }
</style>
<?php include __DIR__ . '/includes/footer.php'; ?>

105
reset_password.php Normal file
View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
$token = $_GET['token'] ?? '';
$error = '';
$success = false;
if (!$token) {
header('Location: login.php');
exit;
}
$stmt = db()->prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW() LIMIT 1");
$stmt->execute([$token]);
$reset = $stmt->fetch();
if (!$reset) {
$error = "This password reset link is invalid or has expired.";
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $reset) {
$password = $_POST['password'] ?? '';
$confirm = $_POST['confirm_password'] ?? '';
if (strlen($password) < 8) {
$error = "Password must be at least 8 characters long.";
} elseif ($password !== $confirm) {
$error = "Passwords do not match.";
} else {
$hashed = password_hash($password, PASSWORD_DEFAULT);
db()->beginTransaction();
try {
$stmt = db()->prepare("UPDATE users SET password = ?, require_password_change = 0 WHERE email = ?");
$stmt->execute([$hashed, $reset['email']]);
$stmt = db()->prepare("DELETE FROM password_resets WHERE email = ?");
$stmt->execute([$reset['email']]);
// Log the password change activity
$ip = Auth::getIpAddress();
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([0, 'Password Changed', "Email: {$reset['email']}, IP: $ip"]);
db()->commit();
$success = true;
} catch (\Exception $e) {
db()->rollBack();
$error = "An error occurred while resetting your password.";
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Reset Password - SR&ED Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
</style>
</head>
<body>
<div class="card login-card p-4">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary">NEW PASSWORD</h3>
<p class="text-muted small">Please set your new secure password</p>
</div>
<?php if ($success): ?>
<div class="alert alert-success small">
Your password has been successfully reset.
</div>
<a href="login.php" class="btn btn-primary w-100">Login Now</a>
<?php elseif ($error && !$reset): ?>
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
<a href="forgot_password.php" class="btn btn-outline-secondary w-100">Request New Link</a>
<?php else: ?>
<?php if ($error): ?>
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label small fw-bold">New Password</label>
<input type="password" name="password" class="form-control" placeholder="••••••••" required autofocus>
<div class="form-text extra-small">Minimum 8 characters.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Confirm New Password</label>
<input type="password" name="confirm_password" class="form-control" placeholder="••••••••" required>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">Reset Password</button>
</form>
<?php endif; ?>
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
project_code,supplier_name,expense_type,amount,date,notes
P001,ACME Corp,Materials,1250.00,2026-02-10,Lumber for site A
P002,Build-It Co,Subcontractors,5000.00,2026-02-12,Foundation work
P001,Global Telecom,Overhead,150.00,2026-02-15,Monthly internet
1 project_code supplier_name expense_type amount date notes
2 P001 ACME Corp Materials 1250.00 2026-02-10 Lumber for site A
3 P002 Build-It Co Subcontractors 5000.00 2026-02-12 Foundation work
4 P001 Global Telecom Overhead 150.00 2026-02-15 Monthly internet

View File

@ -0,0 +1,4 @@
project_code,employee_email,date,hours,labour_type,evidence_type,notes
PROJ-001,john.doe@example.com,2026-02-01,7.5,Experimental Development,Logbooks,Analysis of component A
PROJ-001,jane.smith@example.com,2026-02-01,4.0,Technical Support,Test Results,Testing component B
PROJ-002,john.doe@example.com,2026-02-02,6.0,Technical Planning,Design Documents,Drafting architecture
1 project_code employee_email date hours labour_type evidence_type notes
2 PROJ-001 john.doe@example.com 2026-02-01 7.5 Experimental Development Logbooks Analysis of component A
3 PROJ-001 jane.smith@example.com 2026-02-01 4.0 Technical Support Test Results Testing component B
4 PROJ-002 john.doe@example.com 2026-02-02 6.0 Technical Planning Design Documents Drafting architecture

View File

@ -0,0 +1,4 @@
name,type,contact_info
ACME Corp,supplier,admin@acme.com
Build-It Co,contractor,contact@buildit.com
Global Telecom,supplier,info@globaltelecom.com
1 name type contact_info
2 ACME Corp supplier admin@acme.com
3 Build-It Co contractor contact@buildit.com
4 Global Telecom supplier info@globaltelecom.com

349
settings.php Normal file
View File

@ -0,0 +1,349 @@
<?php
/**
* Settings Page - Manage System Datasets
*/
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Form Submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$id = (int)($_POST['id'] ?? 0);
$name = $_POST['name'] ?? '';
$error = '';
if ($action === 'add_labour_type' && $name) {
$stmt = db()->prepare("INSERT INTO labour_types (tenant_id, name) VALUES (?, ?)");
$stmt->execute([$tenant_id, $name]);
} elseif ($action === 'edit_labour_type' && $id && $name) {
$stmt = db()->prepare("UPDATE labour_types SET name = ? WHERE id = ? AND tenant_id = ?");
$stmt->execute([$name, $id, $tenant_id]);
} elseif ($action === 'delete_labour_type' && $id) {
$check = db()->prepare("SELECT COUNT(*) FROM labour_entries WHERE labour_type_id = ? AND tenant_id = ?");
$check->execute([$id, $tenant_id]);
if ($check->fetchColumn() > 0) {
$error = "Cannot delete: Labour type is used in labour entries.";
} else {
$stmt = db()->prepare("DELETE FROM labour_types WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
}
}
elseif ($action === 'add_evidence_type' && $name) {
$stmt = db()->prepare("INSERT INTO evidence_types (tenant_id, name) VALUES (?, ?)");
$stmt->execute([$tenant_id, $name]);
} elseif ($action === 'edit_evidence_type' && $id && $name) {
$stmt = db()->prepare("UPDATE evidence_types SET name = ? WHERE id = ? AND tenant_id = ?");
$stmt->execute([$name, $id, $tenant_id]);
} elseif ($action === 'delete_evidence_type' && $id) {
$check = db()->prepare("SELECT COUNT(*) FROM labour_entries WHERE evidence_type_id = ? AND tenant_id = ?");
$check->execute([$id, $tenant_id]);
if ($check->fetchColumn() > 0) {
$error = "Cannot delete: Evidence type is used in labour entries.";
} else {
$stmt = db()->prepare("DELETE FROM evidence_types WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
}
}
elseif ($action === 'add_expense_type' && $name) {
$stmt = db()->prepare("INSERT INTO expense_types (tenant_id, name) VALUES (?, ?)");
$stmt->execute([$tenant_id, $name]);
} elseif ($action === 'edit_expense_type' && $id && $name) {
$stmt = db()->prepare("UPDATE expense_types SET name = ? WHERE id = ? AND tenant_id = ?");
$stmt->execute([$name, $id, $tenant_id]);
} elseif ($action === 'delete_expense_type' && $id) {
$check = db()->prepare("SELECT COUNT(*) FROM expenses WHERE expense_type_id = ? AND tenant_id = ?");
$check->execute([$id, $tenant_id]);
if ($check->fetchColumn() > 0) {
$error = "Cannot delete: Expense type is used in expense logs.";
} else {
$stmt = db()->prepare("DELETE FROM expense_types WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
}
}
elseif ($action === 'add_team' && $name) {
$stmt = db()->prepare("INSERT INTO teams (tenant_id, name) VALUES (?, ?)");
$stmt->execute([$tenant_id, $name]);
} elseif ($action === 'edit_team' && $id && $name) {
$stmt = db()->prepare("UPDATE teams SET name = ? WHERE id = ? AND tenant_id = ?");
$stmt->execute([$name, $id, $tenant_id]);
} elseif ($action === 'delete_team' && $id) {
$check = db()->prepare("SELECT COUNT(*) FROM employee_teams WHERE team_id = ? AND tenant_id = ?");
$check->execute([$id, $tenant_id]);
if ($check->fetchColumn() > 0) {
$error = "Cannot delete: Team has assigned employees.";
} else {
$stmt = db()->prepare("DELETE FROM teams WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
}
}
if ($error) {
header("Location: settings.php?error=" . urlencode($error));
} else {
header("Location: settings.php?success=1");
}
exit;
}
// Fetch all datasets
$labourTypes = db()->prepare("SELECT * FROM labour_types WHERE tenant_id = ? ORDER BY name");
$labourTypes->execute([$tenant_id]);
$labourTypeList = $labourTypes->fetchAll();
$evidenceTypes = db()->prepare("SELECT * FROM evidence_types WHERE tenant_id = ? ORDER BY name");
$evidenceTypes->execute([$tenant_id]);
$evidenceTypeList = $evidenceTypes->fetchAll();
$expenseTypes = db()->prepare("SELECT * FROM expense_types WHERE tenant_id = ? ORDER BY name");
$expenseTypes->execute([$tenant_id]);
$expenseTypeList = $expenseTypes->fetchAll();
$teams = db()->prepare("SELECT * FROM teams WHERE tenant_id = ? ORDER BY name");
$teams->execute([$tenant_id]);
$teamList = $teams->fetchAll();
$pageTitle = "SR&ED Manager - Settings";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="fw-bold mb-0">System Settings & Datasets</h4>
<div class="d-flex gap-2">
<?php if (isset($_GET['success'])): ?>
<span class="badge bg-success py-2 px-3">Dataset updated successfully</span>
<?php endif; ?>
<?php if (isset($_GET['error'])): ?>
<span class="badge bg-danger py-2 px-3"><?= htmlspecialchars($_GET['error']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="row">
<!-- Labour Types -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-bold">Labour Types</span>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addLabourTypeModal">+ Add</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead><tr><th>Name</th><th class="text-end">Actions</th></tr></thead>
<tbody>
<?php foreach ($labourTypeList as $item): ?>
<tr>
<td><?= htmlspecialchars($item['name']) ?></td>
<td class="text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" onclick="editItem('edit_labour_type', <?= $item['id'] ?>, '<?= addslashes($item['name']) ?>')">Edit</button>
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteItem('delete_labour_type', <?= $item['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Evidence Types -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-bold">Evidence Types</span>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addEvidenceTypeModal">+ Add</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead><tr><th>Name</th><th class="text-end">Actions</th></tr></thead>
<tbody>
<?php foreach ($evidenceTypeList as $item): ?>
<tr>
<td><?= htmlspecialchars($item['name']) ?></td>
<td class="text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" onclick="editItem('edit_evidence_type', <?= $item['id'] ?>, '<?= addslashes($item['name']) ?>')">Edit</button>
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteItem('delete_evidence_type', <?= $item['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Expense Types -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-bold">Expense Types</span>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addExpenseTypeModal">+ Add</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead><tr><th>Name</th><th class="text-end">Actions</th></tr></thead>
<tbody>
<?php foreach ($expenseTypeList as $item): ?>
<tr>
<td><?= htmlspecialchars($item['name']) ?></td>
<td class="text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" onclick="editItem('edit_expense_type', <?= $item['id'] ?>, '<?= addslashes($item['name']) ?>')">Edit</button>
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteItem('delete_expense_type', <?= $item['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Teams -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<span class="fw-bold">Teams</span>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addTeamModal">+ Add</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead><tr><th>Name</th><th class="text-end">Actions</th></tr></thead>
<tbody>
<?php foreach ($teamList as $item): ?>
<tr>
<td><?= htmlspecialchars($item['name']) ?></td>
<td class="text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" onclick="editItem('edit_team', <?= $item['id'] ?>, '<?= addslashes($item['name']) ?>')">Edit</button>
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteItem('delete_team', <?= $item['id'] ?>)">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Data Management -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white">
<span class="fw-bold">Data Management & Imports</span>
</div>
<div class="card-body">
<p class="small text-muted mb-4">Import legacy data or bulk records from other systems using CSV templates.</p>
<div class="d-grid gap-3">
<a href="import_suppliers.php" class="btn btn-outline-secondary d-flex justify-content-between align-items-center py-2">
<span><i class="bi bi-truck me-2"></i>Import Suppliers</span>
<i class="bi bi-chevron-right"></i>
</a>
<a href="import_expenses.php" class="btn btn-outline-secondary d-flex justify-content-between align-items-center py-2">
<span><i class="bi bi-receipt me-2"></i>Import Expenses</span>
<i class="bi bi-chevron-right"></i>
</a>
<a href="import_labour.php" class="btn btn-outline-secondary d-flex justify-content-between align-items-center py-2">
<span><i class="bi bi-clock-history me-2"></i>Import Labour Activities</span>
<i class="bi bi-chevron-right"></i>
</a>
</div>
</div>
</div>
</div>
<!-- Company Configuration -->
<div class="col-md-6 mb-4">
<div class="card h-100 border-0 shadow-sm text-white" style="background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);">
<div class="card-body d-flex flex-column justify-content-center align-items-center text-center py-5">
<i class="bi bi-building fs-1 mb-3"></i>
<h5 class="fw-bold">Company Preferences</h5>
<p class="small mb-4 opacity-75">Configure your company identity, logo, fiscal year end, and notification settings.</p>
<a href="company_settings.php" class="btn btn-light px-4 rounded-pill fw-bold">Manage Company Info</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="editItemModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header"><h5 class="modal-title fw-bold" id="editItemTitle">Edit Item</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" id="editItemAction">
<input type="hidden" name="id" id="editItemId">
<div class="mb-3">
<label class="form-label small fw-bold">Name</label>
<input type="text" name="name" id="editItemName" class="form-control" required>
</div>
</div>
<div class="modal-footer border-0"><button type="submit" class="btn btn-primary px-4">Save Changes</button></div>
</form>
</div>
</div>
</div>
<form id="deleteForm" method="POST" style="display:none;">
<input type="hidden" name="action" id="deleteAction">
<input type="hidden" name="id" id="deleteId">
</form>
<script>
function editItem(action, id, name) {
document.getElementById('editItemAction').value = action;
document.getElementById('editItemId').value = id;
document.getElementById('editItemName').value = name;
document.getElementById('editItemTitle').innerText = 'Edit ' + action.replace('edit_', '').replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
new bootstrap.Modal(document.getElementById('editItemModal')).show();
}
function deleteItem(action, id) {
if (confirm('Are you sure you want to delete this item? This action cannot be undone.')) {
document.getElementById('deleteAction').value = action;
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<!-- Modals -->
<?php
$modals = [
['id' => 'addLabourTypeModal', 'title' => 'Add Labour Type', 'action' => 'add_labour_type'],
['id' => 'addEvidenceTypeModal', 'title' => 'Add Evidence Type', 'action' => 'add_evidence_type'],
['id' => 'addExpenseTypeModal', 'title' => 'Add Expense Type', 'action' => 'add_expense_type'],
['id' => 'addTeamModal', 'title' => 'Add Team', 'action' => 'add_team'],
];
foreach ($modals as $m):
?>
<div class="modal fade" id="<?= $m['id'] ?>" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header"><h5 class="modal-title fw-bold"><?= $m['title'] ?></h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="<?= $m['action'] ?>">
<div class="mb-3">
<label class="form-label small fw-bold">Name</label>
<input type="text" name="name" class="form-control" placeholder="Enter name..." required>
</div>
</div>
<div class="modal-footer border-0"><button type="submit" class="btn btn-primary px-4">Add Item</button></div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
<?php include __DIR__ . '/includes/footer.php'; ?>

510
sred_claim_report.php Normal file
View File

@ -0,0 +1,510 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
$selected_year = (int)($_GET['year'] ?? date('Y'));
// 1. Get Company Settings
$stmt = db()->prepare("SELECT * FROM company_settings WHERE id = 1");
$stmt->execute();
$company = $stmt->fetch();
// 2. Determine Fiscal Year Range
$fiscal_end_raw = $company['fiscal_year_end'] ?? '12-31';
$fiscal_month = (int)date('m', strtotime($fiscal_end_raw));
$fiscal_day = (int)date('d', strtotime($fiscal_end_raw));
// If fiscal end is 2024-03-31, and selected year is 2024:
// Start: 2023-04-01, End: 2024-03-31
$end_date = sprintf('%d-%02d-%02d', $selected_year, $fiscal_month, $fiscal_day);
$start_date = date('Y-m-d', strtotime($end_date . ' -1 year +1 day'));
// 3. Fetch Projects active in this period (with labour or expenses)
$projects_query = "
SELECT DISTINCT p.*
FROM projects p
LEFT JOIN labour_entries le ON p.id = le.project_id AND le.entry_date BETWEEN ? AND ?
LEFT JOIN expenses e ON p.id = e.project_id AND e.entry_date BETWEEN ? AND ?
WHERE (le.id IS NOT NULL OR e.id IS NOT NULL)
ORDER BY p.name ASC
";
$stmt = db()->prepare($projects_query);
$stmt->execute([$start_date, $end_date, $start_date, $end_date]);
$active_projects = $stmt->fetchAll();
// 4. Helper for Labour Data
function getLabourForProject($projectId, $start, $end) {
$stmt = db()->prepare("
SELECT le.*, e.name as employee_name, lt.name as labour_type
FROM labour_entries le
JOIN employees e ON le.employee_id = e.id
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
WHERE le.project_id = ? AND le.entry_date BETWEEN ? AND ?
ORDER BY le.entry_date ASC
");
$stmt->execute([$projectId, $start, $end]);
return $stmt->fetchAll();
}
// 5. Helper for Expense Data
function getExpensesForProject($projectId, $start, $end) {
$stmt = db()->prepare("
SELECT e.*, s.name as supplier_name, et.name as expense_type
FROM expenses e
JOIN suppliers s ON e.supplier_id = s.id
LEFT JOIN expense_types et ON e.expense_type_id = et.id
WHERE e.project_id = ? AND e.entry_date BETWEEN ? AND ?
ORDER BY e.entry_date ASC
");
$stmt->execute([$projectId, $start, $end]);
return $stmt->fetchAll();
}
// 6. Helper for Summary Calendar (Labour Hours)
$summary_labour_query = "
SELECT p.id as project_id, p.name as project_name, DATE_FORMAT(le.entry_date, '%Y-%m') as month, SUM(le.hours) as total_hours, SUM(le.hours * IFNULL(le.hourly_rate, 0)) as total_cost
FROM labour_entries le
JOIN projects p ON le.project_id = p.id
WHERE le.entry_date BETWEEN ? AND ?
GROUP BY p.id, p.name, month
";
$stmt = db()->prepare($summary_labour_query);
$stmt->execute([$start_date, $end_date]);
$summary_labour_data = $stmt->fetchAll();
// 7. Helper for Summary Calendar (Expenses)
$summary_expense_query = "
SELECT p.id as project_id, p.name as project_name, DATE_FORMAT(e.entry_date, '%Y-%m') as month, SUM(e.amount) as total_amount
FROM expenses e
JOIN projects p ON e.project_id = p.id
WHERE e.entry_date BETWEEN ? AND ?
GROUP BY p.id, p.name, month
";
$stmt = db()->prepare($summary_expense_query);
$stmt->execute([$start_date, $end_date]);
$summary_expense_data = $stmt->fetchAll();
// Organize Summary Data
$months = [];
try {
$current_dt = new DateTime($start_date);
$current_dt->modify('first day of this month');
$end_dt = new DateTime($end_date);
$end_dt->modify('first day of this month');
while ($current_dt <= $end_dt) {
$months[] = $current_dt->format('Y-m');
$current_dt->modify('+1 month');
if (count($months) > 13) break; // Safety for roughly 1 year
}
} catch (Exception $e) {
// Fallback if DateTime fails
$months = [date('Y-m', strtotime($start_date))];
}
$labour_matrix = [];
$project_names = [];
foreach ($summary_labour_data as $row) {
$pid = $row['project_id'];
$project_names[$pid] = $row['project_name'];
$labour_matrix[$pid][$row['month']] = ['hours' => $row['total_hours'], 'cost' => $row['total_cost']];
}
$expense_matrix = [];
foreach ($summary_expense_data as $row) {
$pid = $row['project_id'];
$project_names[$pid] = $row['project_name'];
$expense_matrix[$pid][$row['month']] = $row['total_amount'];
}
$pageTitle = "SRED Claim Report - " . $selected_year;
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= $pageTitle ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@media print {
.no-print { display: none !important; }
.page-break { page-break-before: always; }
body { background: white; }
.container { width: 100%; max-width: 100%; margin: 0; padding: 0; }
.card { border: none !important; box-shadow: none !important; }
}
body { background: #f8f9fa; font-family: 'Inter', sans-serif; }
.report-page { background: white; min-height: 297mm; padding: 20mm; margin: 20px auto; box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15); width: 210mm; }
.title-page { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; }
.toc-item { display: flex; justify-content: space-between; border-bottom: 1px dotted #ccc; margin-bottom: 10px; }
.chart-container { height: 300px; }
.table-tight th, .table-tight td { padding: 4px 8px; font-size: 0.85rem; }
.calendar-table th, .calendar-table td { font-size: 0.75rem; text-align: center; vertical-align: middle; }
</style>
</head>
<body>
<div class="no-print bg-dark text-white p-3 sticky-top shadow d-flex justify-content-between align-items-center">
<div>
<a href="sred_claim_report_selector.php" class="btn btn-sm btn-outline-light me-2"><i class="bi bi-arrow-left"></i> Back</a>
<strong>SRED Claim Report Generator</strong>
</div>
<button onclick="window.print()" class="btn btn-primary btn-sm"><i class="bi bi-printer"></i> Print Report / Save PDF</button>
</div>
<!-- PAGE 1: TITLE PAGE -->
<div class="report-page title-page">
<?php if (!empty($company['logo_path'])): ?>
<img src="<?= htmlspecialchars($company['logo_path']) ?>" alt="Logo" class="mb-5" style="max-height: 150px;">
<?php endif; ?>
<div style="margin-top: auto; margin-bottom: auto;">
<h1 class="display-3 fw-bold mb-4"><?= htmlspecialchars($company['company_name'] ?? 'Company Name') ?></h1>
<h2 class="text-primary mb-5">SRED CLAIM REPORT</h2>
<h4 class="text-muted">Fiscal Year: <?= $selected_year ?></h4>
<p class="mt-3 text-muted">Range: <?= date('M d, Y', strtotime($start_date)) ?> to <?= date('M d, Y', strtotime($end_date)) ?></p>
</div>
<div class="mt-auto border-top pt-4 w-100">
<p class="mb-1 fw-bold"><?= htmlspecialchars($company['address_1'] ?? '') ?></p>
<?php if ($company['address_2']): ?><p class="mb-1"><?= htmlspecialchars($company['address_2']) ?></p><?php endif; ?>
<p class="mb-1"><?= htmlspecialchars($company['city'] ?? '') ?>, <?= htmlspecialchars($company['province'] ?? '') ?> <?= htmlspecialchars($company['postal_code'] ?? '') ?></p>
<p class="mb-1">Phone: <?= htmlspecialchars($company['phone'] ?? '') ?> | Email: <?= htmlspecialchars($company['email'] ?? '') ?></p>
<p class="mb-0">Website: <?= htmlspecialchars($company['website'] ?? '') ?></p>
<?php if ($company['business_number']): ?>
<p class="mt-2 fw-bold">Business Number: <?= htmlspecialchars($company['business_number']) ?></p>
<?php endif; ?>
</div>
</div>
<!-- PAGE 2: TABLE OF CONTENTS -->
<div class="report-page page-break">
<h2 class="mb-5 border-bottom pb-3">Table of Contents</h2>
<div class="mt-4">
<div class="toc-item"><span>1. Title Page</span><span>1</span></div>
<div class="toc-item"><span>2. Table of Contents</span><span>2</span></div>
<?php
$page_counter = 3;
foreach ($active_projects as $p):
$proj_start_page = $page_counter;
?>
<div class="toc-item fw-bold mt-3"><span>Project: <?= htmlspecialchars($p['name']) ?></span><span><?= $page_counter++ ?></span></div>
<div class="toc-item ps-4 text-muted"><span>Insights & Charts</span><span><?= ($page_counter - 1) ?></span></div>
<div class="toc-item ps-4 text-muted"><span>Labour Detail Report</span><span><?= $page_counter++ ?></span></div>
<div class="toc-item ps-4 text-muted"><span>Expense Detail Report</span><span><?= $page_counter++ ?></span></div>
<?php endforeach; ?>
<div class="toc-item fw-bold mt-4"><span>Summarized Claim (Calendar Views)</span><span><?= $page_counter++ ?></span></div>
<div class="toc-item ps-4 text-muted"><span>Labour Summary</span><span><?= ($page_counter - 1) ?></span></div>
<div class="toc-item ps-4 text-muted"><span>Expense Summary</span><span><?= $page_counter++ ?></span></div>
</div>
</div>
<!-- PROJECT PAGES -->
<?php foreach ($active_projects as $idx => $project):
$labour = getLabourForProject($project['id'], $start_date, $end_date);
$expenses = getExpensesForProject($project['id'], $start_date, $end_date);
// Process Insights
$employee_hours = [];
$labour_type_hours = [];
$total_proj_hours = 0;
$total_proj_labour_cost = 0;
foreach ($labour as $l) {
$employee_hours[$l['employee_name']] = ($employee_hours[$l['employee_name']] ?? 0) + (float)$l['hours'];
$labour_type_hours[$l['labour_type'] ?? 'Other'] = ($labour_type_hours[$l['labour_type'] ?? 'Other'] ?? 0) + (float)$l['hours'];
$total_proj_hours += (float)$l['hours'];
$total_proj_labour_cost += (float)$l['hours'] * (float)($l['hourly_rate'] ?? 0);
}
arsort($employee_hours);
$top_employees = array_slice($employee_hours, 0, 5, true);
$total_proj_expenses = 0;
foreach ($expenses as $e) {
$total_proj_expenses += (float)$e['amount'];
}
?>
<!-- Project Page 1: Insights -->
<div class="report-page page-break">
<div class="d-flex justify-content-between align-items-start mb-4 border-bottom pb-3">
<div>
<h6 class="text-uppercase text-primary fw-bold mb-1">Project Analysis</h6>
<h2 class="mb-0"><?= htmlspecialchars($project['name']) ?></h2>
</div>
<div class="text-end">
<span class="badge bg-light text-dark border">Project ID: <?= htmlspecialchars($project['code'] ?? $project['id']) ?></span>
</div>
</div>
<p class="lead text-muted mb-4"><?= nl2br(htmlspecialchars($project['description'] ?? 'No project description available.')) ?></p>
<div class="row mb-5">
<div class="col-6">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="text-muted small text-uppercase">Total Labour Hours</h6>
<h3 class="mb-0 fw-bold"><?= number_format($total_proj_hours, 1) ?> h</h3>
</div>
</div>
</div>
<div class="col-6">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="text-muted small text-uppercase">Total Estimated Cost</h6>
<h3 class="mb-0 fw-bold text-success">$<?= number_format($total_proj_labour_cost + $total_proj_expenses, 2) ?></h3>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<h6 class="fw-bold mb-3">Top Employees by Hours</h6>
<div class="chart-container">
<canvas id="chart_emp_<?= $project['id'] ?>"></canvas>
</div>
</div>
<div class="col-6">
<h6 class="fw-bold mb-3">Labour by Category</h6>
<div class="chart-container">
<canvas id="chart_type_<?= $project['id'] ?>"></canvas>
</div>
</div>
</div>
<script>
new Chart(document.getElementById('chart_emp_<?= $project['id'] ?>').getContext('2d'), {
type: 'bar',
data: {
labels: <?= json_encode(array_keys($top_employees)) ?>,
datasets: [{
label: 'Hours',
data: <?= json_encode(array_values($top_employees)) ?>,
backgroundColor: '#0d6efd'
}]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
});
new Chart(document.getElementById('chart_type_<?= $project['id'] ?>').getContext('2d'), {
type: 'doughnut',
data: {
labels: <?= json_encode(array_keys($labour_type_hours)) ?>,
datasets: [{
data: <?= json_encode(array_values($labour_type_hours)) ?>,
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#0dcaf0', '#6c757d']
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
</script>
</div>
<!-- Project Page 2: Labour Details -->
<div class="report-page page-break">
<h4 class="mb-4 border-bottom pb-2">Labour Detail Report: <?= htmlspecialchars($project['name']) ?></h4>
<table class="table table-bordered table-tight">
<thead class="bg-light">
<tr>
<th>Date</th>
<th>Employee</th>
<th>Activity Type</th>
<th class="text-end">Hours</th>
<th class="text-end">Rate</th>
<th class="text-end">Total Wage</th>
</tr>
</thead>
<tbody>
<?php if (empty($labour)): ?>
<tr><td colspan="6" class="text-center text-muted">No labour entries recorded.</td></tr>
<?php endif; ?>
<?php foreach ($labour as $l):
$cost = (float)$l['hours'] * (float)($l['hourly_rate'] ?? 0);
?>
<tr>
<td><?= $l['entry_date'] ?></td>
<td><?= htmlspecialchars($l['employee_name']) ?></td>
<td><?= htmlspecialchars($l['labour_type'] ?? 'N/A') ?></td>
<td class="text-end"><?= number_format((float)$l['hours'], 2) ?></td>
<td class="text-end"><?= $l['hourly_rate'] ? '$'.number_format((float)$l['hourly_rate'], 2) : '-' ?></td>
<td class="text-end fw-bold"><?= $cost > 0 ? '$'.number_format($cost, 2) : '-' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="bg-light">
<tr>
<td colspan="3" class="text-end fw-bold">Project Totals:</td>
<td class="text-end fw-bold text-primary"><?= number_format($total_proj_hours, 2) ?> h</td>
<td></td>
<td class="text-end fw-bold text-success">$<?= number_format($total_proj_labour_cost, 2) ?></td>
</tr>
</tfoot>
</table>
<div class="mt-4 extra-small text-muted">
Note: Hourly rates are recorded at the time of entry to reflect historical wage adjustments accurately.
</div>
</div>
<!-- Project Page 3: Expense Details -->
<div class="report-page page-break">
<h4 class="mb-4 border-bottom pb-2">Expense Detail Report: <?= htmlspecialchars($project['name']) ?></h4>
<table class="table table-bordered table-tight">
<thead class="bg-light">
<tr>
<th>Date</th>
<th>Supplier</th>
<th>Expense Type</th>
<th>Notes</th>
<th class="text-end">Allocation</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenses)): ?>
<tr><td colspan="6" class="text-center text-muted">No expenses recorded.</td></tr>
<?php endif; ?>
<?php foreach ($expenses as $e): ?>
<tr>
<td><?= $e['entry_date'] ?></td>
<td><?= htmlspecialchars($e['supplier_name']) ?></td>
<td><?= htmlspecialchars($e['expense_type'] ?? 'N/A') ?></td>
<td style="max-width: 200px;" class="text-truncate"><?= htmlspecialchars($e['notes'] ?? '') ?></td>
<td class="text-end"><?= number_format((float)$e['allocation_percent'], 0) ?>%</td>
<td class="text-end fw-bold">$<?= number_format((float)$e['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="bg-light">
<tr>
<td colspan="5" class="text-end fw-bold">Total Expenses:</td>
<td class="text-end fw-bold text-success">$<?= number_format($total_proj_expenses, 2) ?></td>
</tr>
</tfoot>
</table>
</div>
<?php endforeach; ?>
<!-- SUMMARIZED CLAIM - LABOUR -->
<div class="report-page page-break">
<h3 class="mb-4 border-bottom pb-2">Summarized Claim: Monthly Labour Hours</h3>
<div class="table-responsive">
<table class="table table-bordered calendar-table">
<thead class="bg-light">
<tr>
<th class="text-start">Project Name</th>
<?php foreach ($months as $m): ?>
<th><?= date('M y', strtotime($m)) ?></th>
<?php endforeach; ?>
<th class="bg-secondary text-white">Total $</th>
</tr>
</thead>
<tbody>
<?php
$grand_total_hours = 0;
$grand_total_cost = 0;
foreach ($labour_matrix as $pid => $data):
$row_cost = 0;
$pname = $project_names[$pid] ?? 'Unknown Project';
?>
<tr>
<td class="text-start fw-bold"><?= htmlspecialchars($pname) ?></td>
<?php foreach ($months as $m):
$h = (float)($data[$m]['hours'] ?? 0);
$c = (float)($data[$m]['cost'] ?? 0);
$row_cost += $c;
$grand_total_hours += $h;
?>
<td><?= $h > 0 ? number_format($h, 1) : '-' ?></td>
<?php endforeach; ?>
<td class="fw-bold">$<?= number_format($row_cost, 0) ?></td>
</tr>
<?php
$grand_total_cost += $row_cost;
endforeach; ?>
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td class="text-start">Monthly Totals</td>
<?php foreach ($months as $m):
$m_hours = 0;
foreach($labour_matrix as $pid => $d) $m_hours += (float)($d[$m]['hours'] ?? 0);
?>
<td><?= $m_hours > 0 ? number_format($m_hours, 1) : '-' ?></td>
<?php endforeach; ?>
<td class="text-success">$<?= number_format($grand_total_cost, 2) ?></td>
</tr>
</tfoot>
</table>
</div>
<p class="small text-muted mt-3">All amounts are calculated using the recorded hourly rate at the time of labour entry.</p>
</div>
<!-- SUMMARIZED CLAIM - EXPENSES -->
<div class="report-page page-break">
<h3 class="mb-4 border-bottom pb-2">Summarized Claim: Monthly Expenses</h3>
<div class="table-responsive">
<table class="table table-bordered calendar-table">
<thead class="bg-light">
<tr>
<th class="text-start">Project Name</th>
<?php foreach ($months as $m): ?>
<th><?= date('M y', strtotime($m)) ?></th>
<?php endforeach; ?>
<th class="bg-secondary text-white">Total $</th>
</tr>
</thead>
<tbody>
<?php
$grand_total_expense = 0;
foreach ($expense_matrix as $pid => $data):
$row_amount = 0;
$pname = $project_names[$pid] ?? 'Unknown Project';
?>
<tr>
<td class="text-start fw-bold"><?= htmlspecialchars($pname) ?></td>
<?php foreach ($months as $m):
$a = (float)($data[$m] ?? 0);
$row_amount += $a;
?>
<td><?= $a > 0 ? '$'.number_format($a, 0) : '-' ?></td>
<?php endforeach; ?>
<td class="fw-bold">$<?= number_format($row_amount, 0) ?></td>
</tr>
<?php
$grand_total_expense += $row_amount;
endforeach; ?>
</tbody>
<tfoot class="bg-light fw-bold">
<tr>
<td class="text-start">Monthly Totals</td>
<?php foreach ($months as $m):
$m_amt = 0;
foreach($expense_matrix as $pid => $d) $m_amt += (float)($d[$m] ?? 0);
?>
<td><?= $m_amt > 0 ? '$'.number_format($m_amt, 0) : '-' ?></td>
<?php endforeach; ?>
<td class="text-success">$<?= number_format($grand_total_expense, 2) ?></td>
</tr>
</tfoot>
</table>
</div>
<div class="mt-5 text-center">
<div class="border p-4 d-inline-block">
<h5 class="text-uppercase text-muted small mb-3">Total Captured Wages and Expenses for SR&ED Claim</h5>
<h1 class="display-4 fw-bold text-primary">$<?= number_format($grand_total_cost + $grand_total_expense, 2) ?></h1>
<p class="mb-0 text-muted">Includes all Labour Wages and Project Expenses for Fiscal Year <?= $selected_year ?></p>
</div>
</div>
</div>
<footer class="no-print mt-5 text-center p-4 text-muted border-top bg-white">
SR&ED Manager Claim Report Generator | &copy; <?= date('Y') ?>
</footer>
</body>
</html>

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Fetch company settings to get fiscal year end
$stmt = db()->prepare("SELECT fiscal_year_end FROM company_settings WHERE id = 1");
$stmt->execute();
$settings = $stmt->fetch();
$fiscal_year_end = $settings['fiscal_year_end'] ?? null;
// Determine possible years based on data
$years = [];
$res = db()->query("SELECT DISTINCT YEAR(entry_date) as y FROM labour_entries UNION SELECT DISTINCT YEAR(entry_date) as y FROM expenses ORDER BY y DESC");
while($row = $res->fetch()) {
$years[] = (int)$row['y'];
}
if (empty($years)) $years[] = (int)date('Y');
$pageTitle = "SR&ED Claim Report Selector";
include __DIR__ . '/includes/header.php';
?>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body p-5 text-center">
<i class="bi bi-file-earmark-bar-graph text-primary mb-4" style="font-size: 3rem;"></i>
<h2 class="fw-bold mb-3">SRED Claim Report</h2>
<p class="text-muted mb-4">Generate a comprehensive Scientific Research and Experimental Development (SR&ED) claim report for a specific fiscal year.</p>
<form action="sred_claim_report.php" method="GET">
<div class="mb-4 text-start">
<label class="form-label fw-bold small text-uppercase">Select Fiscal Year</label>
<select name="year" class="form-select form-select-lg">
<?php foreach($years as $y): ?>
<option value="<?= $y ?>" <?= $y == date('Y') ? 'selected' : '' ?>><?= $y ?></option>
<?php endforeach; ?>
</select>
<?php if ($fiscal_year_end): ?>
<div class="form-text mt-2">
Your fiscal year ends on: <strong><?= date('F jS', strtotime($fiscal_year_end)) ?></strong>
</div>
<?php else: ?>
<div class="form-text mt-2 text-warning">
<i class="bi bi-exclamation-triangle"></i> Fiscal year end not set in <a href="company_settings.php">Company Preferences</a>.
</div>
<?php endif; ?>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">
Generate Report <i class="bi bi-arrow-right ms-2"></i>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

115
system_preferences.php Normal file
View File

@ -0,0 +1,115 @@
<?php
/**
* System Preferences - Manage Password Requirements and Security
*/
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/auth_helper.php';
Auth::requireLogin();
$tenant_id = (int)$_SESSION['tenant_id'];
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$prefs = [
'pwd_min_length' => $_POST['pwd_min_length'] ?? '8',
'pwd_require_upper' => isset($_POST['pwd_require_upper']) ? '1' : '0',
'pwd_require_lower' => isset($_POST['pwd_require_lower']) ? '1' : '0',
'pwd_require_numbers' => isset($_POST['pwd_require_numbers']) ? '1' : '0',
'pwd_no_common_words' => isset($_POST['pwd_no_common_words']) ? '1' : '0'
];
foreach ($prefs as $key => $val) {
$stmt = db()->prepare("INSERT INTO system_preferences (tenant_id, pref_key, pref_value) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value)");
$stmt->execute([$tenant_id, $key, $val]);
}
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Settings Updated', 'Updated system preferences and password requirements']);
header("Location: system_preferences.php?success=1");
exit;
}
// Fetch current preferences
$stmt = db()->prepare("SELECT pref_key, pref_value FROM system_preferences WHERE tenant_id = ?");
$stmt->execute([$tenant_id]);
$prefs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$pageTitle = "SR&ED Manager - System Preferences";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">System Preferences</h2>
<?php if (isset($_GET['success'])): ?>
<span class="badge bg-success py-2 px-3">Preferences saved successfully</span>
<?php endif; ?>
</div>
<form method="POST">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Password Requirements</h5>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-bold small">Minimum Characters</label>
<input type="number" name="pwd_min_length" class="form-control" value="<?= htmlspecialchars($prefs['pwd_min_length'] ?? '8') ?>" min="4" max="32">
<div class="form-text extra-small text-muted">Passwords shorter than this will be rejected.</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch p-3 border rounded">
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_upper" id="reqUpper" <?= ($prefs['pwd_require_upper'] ?? '1') === '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-bold small" for="reqUpper">Require Uppercase Letters</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch p-3 border rounded">
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_lower" id="reqLower" <?= ($prefs['pwd_require_lower'] ?? '1') === '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-bold small" for="reqLower">Require Lowercase Letters</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch p-3 border rounded">
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_numbers" id="reqNumbers" <?= ($prefs['pwd_require_numbers'] ?? '1') === '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-bold small" for="reqNumbers">Require Numbers</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch p-3 border rounded">
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_no_common_words" id="noCommon" <?= ($prefs['pwd_no_common_words'] ?? '1') === '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-bold small" for="noCommon">Don't Allow Common Words</label>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Authentication & 2FA</h5>
</div>
<div class="card-body">
<p class="text-muted small mb-0">2FA settings are currently managed per-user in the Employee management section. Telephone numbers provided there will be used for SMS-based verification factors.</p>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="settings.php" class="btn btn-light px-4">Cancel</a>
<button type="submit" class="btn btn-primary px-4">Save Preferences</button>
</div>
</form>
</div>
</div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>

19
test_session.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/includes/auth_helper.php';
if (!isset($_SESSION['test_counter'])) {
$_SESSION['test_counter'] = 0;
}
$_SESSION['test_counter']++;
echo "<h1>Session Diagnostic</h1>";
echo "<b>Counter:</b> " . $_SESSION['test_counter'] . " (Refresh to see if it increases)<br>";
echo "<b>Session ID:</b> " . session_id() . "<br>";
echo "<b>HTTPS:</b> " . (isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : 'off') . "<br>";
echo "<b>SameSite:</b> " . (session_get_cookie_params()['samesite'] ?? 'Not set') . "<br>";
echo "<hr>";
echo "<h3>If the counter doesn't increase on refresh:</h3>";
echo "Your browser is likely blocking the session cookie because the preview is in an iframe. ";
echo "I have set <code>SameSite=None; Secure</code> which should fix this, but some browsers require extra permissions or have strict privacy settings.";
echo "<br><br><a href='test_session.php'>Click here to Refresh</a>";
echo "<br><br><a href='login.php'>Go to Login</a>";

BIN
uploads/69912b6e657db.pdf Normal file

Binary file not shown.

BIN
uploads/6991f572867cb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB