Compare commits
No commits in common. "9ff74420bdd74835c0a115a4f1f1a6bbe81372be" and "cb637ac2fc777b81b231ac977f0a1182b579cb72" have entirely different histories.
9ff74420bd
...
cb637ac2fc
@ -1,57 +0,0 @@
|
||||
<?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;
|
||||
@ -1,79 +0,0 @@
|
||||
<?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;
|
||||
@ -1,26 +0,0 @@
|
||||
<?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()]);
|
||||
}
|
||||
@ -15,17 +15,13 @@ 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 = $db->prepare("SELECT id, name, code 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,
|
||||
'label' => $row['name'] . ' (' . $row['code'] . ')',
|
||||
'url' => 'project_detail.php?id=' . $row['id']
|
||||
];
|
||||
}
|
||||
|
||||
@ -61,48 +61,12 @@ body {
|
||||
.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 {
|
||||
.btn-primary:hover {
|
||||
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 {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@ -1,266 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,24 +0,0 @@
|
||||
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');
|
||||
@ -1,16 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,27 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,15 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,7 +1,5 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id) {
|
||||
@ -10,14 +8,7 @@ if (!$id) {
|
||||
}
|
||||
|
||||
$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 = $db->prepare("SELECT * FROM employees WHERE id = ?");
|
||||
$employee->execute([$id]);
|
||||
$employee = $employee->fetch();
|
||||
|
||||
@ -25,77 +16,9 @@ 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
|
||||
@ -104,61 +27,15 @@ $stmt = $db->prepare("
|
||||
JOIN labour_types lt ON l.labour_type_id = lt.id
|
||||
WHERE l.employee_id = ?
|
||||
ORDER BY l.entry_date DESC
|
||||
LIMIT 10
|
||||
LIMIT 20
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$labourEntries = $stmt->fetchAll();
|
||||
$entries = $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 = $db->prepare("SELECT SUM(hours) as total_hours, COUNT(*) as entry_count FROM labour_entries WHERE employee_id = ?");
|
||||
$stats->execute([$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">
|
||||
@ -166,224 +43,69 @@ function formatBytes($bytes, $precision = 2) {
|
||||
<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"><a href="employees.php 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>
|
||||
<p class="text-muted"><?= htmlspecialchars($employee['position'] ?? 'Staff') ?></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>
|
||||
<button class="btn btn-outline-primary btn-sm"><i class="bi bi-pencil me-1"></i> Edit Employee</button>
|
||||
</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 class="col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Information</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="small text-muted d-block">Email</label>
|
||||
<span><?= htmlspecialchars($employee['email'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="small text-muted d-block">Start Date</label>
|
||||
<span><?= $employee['start_date'] ? date('M j, Y', strtotime($employee['start_date'])) : 'N/A' ?></span>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="small text-muted d-block">Total Hours Logged</label>
|
||||
<span class="fs-4 fw-bold text-primary"><?= number_format($stats['total_hours'] ?? 0, 1) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>Recent Labour Entries</span>
|
||||
<a href="labour.php?employee_id=<?= $id ?>" class="btn btn-sm btn-link text-decoration-none">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): ?>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<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>
|
||||
<th>Project</th>
|
||||
<th>Type</th>
|
||||
<th>Hours</th>
|
||||
<th>Notes</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): ?>
|
||||
<?php if (empty($entries)): ?>
|
||||
<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>
|
||||
<td colspan="5" class="text-center py-4 text-muted">No entries found for this employee.</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): ?>
|
||||
<?php foreach ($entries as $e): ?>
|
||||
<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>
|
||||
<td><?= date('Y-m-d', strtotime($e['entry_date'])) ?></td>
|
||||
<td><a href="project_detail.php?id=<?= $e['project_id'] ?>" class="text-decoration-none"><?= htmlspecialchars($e['project_name']) ?></a></td>
|
||||
<td><?= htmlspecialchars($e['labour_type']) ?></td>
|
||||
<td class="fw-bold text-primary"><?= number_format($e['hours'], 1) ?></td>
|
||||
<td class="text-muted small"><?= htmlspecialchars(substr($e['notes'], 0, 50)) ?><?= strlen($e['notes']) > 50 ? '...' : '' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Handle Add Employee
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
|
||||
@ -14,31 +12,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
|
||||
$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]);
|
||||
$stmt = db()->prepare("INSERT IGNORE INTO users (tenant_id, name, email, role) VALUES (?, ?, ?, 'staff')");
|
||||
$stmt->execute([$tenant_id, "$first_name $last_name", $email]);
|
||||
$user_id = (int)db()->lastInsertId();
|
||||
if ($user_id === 0) {
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$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"]);
|
||||
$stmt = db()->prepare("INSERT INTO employees (tenant_id, first_name, last_name, email, position, start_date, is_limited, user_id, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, $first_name, $last_name, $email, $position, $start_date, $is_limited, $user_id, "$first_name $last_name"]);
|
||||
$employee_id = (int)db()->lastInsertId();
|
||||
|
||||
if ($initial_wage > 0) {
|
||||
@ -62,10 +53,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -116,11 +103,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<?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><strong><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></strong></td>
|
||||
<td class="small text-muted"><?= htmlspecialchars($e['position']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
@ -135,8 +118,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-primary">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@ -169,10 +151,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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">
|
||||
@ -196,32 +174,10 @@ include __DIR__ . '/includes/header.php';
|
||||
<?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 class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="is_limited" id="limitedCheck" checked>
|
||||
<label class="form-check-label small fw-bold" for="limitedCheck">Limited Web Reporting (Cannot Login)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -234,29 +190,4 @@ include __DIR__ . '/includes/header.php';
|
||||
</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'; ?>
|
||||
|
||||
@ -1,34 +1,22 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Get Projects for filter
|
||||
$projects_stmt = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name ASC");
|
||||
$projects_stmt->execute([$tenant_id]);
|
||||
$all_projects = $projects_stmt->fetchAll();
|
||||
|
||||
// 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;
|
||||
@ -96,12 +84,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
|
||||
97
expenses.php
97
expenses.php
@ -1,11 +1,8 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Handle Add Expense
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
|
||||
@ -35,16 +32,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
|
||||
|
||||
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 attachments (tenant_id, entity_type, entity_id, file_name, file_path, file_size, mime_type, uploaded_by) VALUES (?, 'expense', ?, ?, ?, ?, ?, 'John Manager')");
|
||||
$stmt->execute([$tenant_id, $expense_id, $file_name, $file_path, $file_size, $mime_type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,46 +48,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
|
||||
}
|
||||
|
||||
// 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
|
||||
WHERE e.tenant_id = ?
|
||||
ORDER BY e.entry_date DESC, e.created_at DESC
|
||||
");
|
||||
$expenseEntries->execute($params);
|
||||
$expenseEntries->execute([$tenant_id]);
|
||||
$expenseList = $expenseEntries->fetchAll();
|
||||
|
||||
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
|
||||
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name");
|
||||
$projects->execute([$tenant_id]);
|
||||
$projectList = $projects->fetchAll();
|
||||
|
||||
@ -117,53 +79,8 @@ 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">
|
||||
@ -205,7 +122,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-secondary">Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@ -2,15 +2,13 @@
|
||||
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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
$report_type = $_GET['report_type'] ?? 'labour_export';
|
||||
|
||||
|
||||
30
files.php
30
files.php
@ -1,34 +1,22 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Get Projects for filter
|
||||
$projects_stmt = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name ASC");
|
||||
$projects_stmt->execute([$tenant_id]);
|
||||
$all_projects = $projects_stmt->fetchAll();
|
||||
|
||||
// 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;
|
||||
@ -104,12 +92,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
<?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>
|
||||
@ -1,189 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,185 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,152 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,124 +0,0 @@
|
||||
<?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) {}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,12 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<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>
|
||||
<style>
|
||||
.nav-link.active {
|
||||
color: #3b82f6 !important;
|
||||
border-bottom: 2px solid #3b82f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -57,29 +63,16 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<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">
|
||||
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['reports.php', 'files.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 class="nav-item">
|
||||
<a class="nav-link <?= $currentPage === 'settings.php' ? 'active' : '' ?>" href="settings.php">Settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
@ -90,20 +83,9 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
</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 class="d-flex align-items-center text-muted small">
|
||||
<span class="me-3">Tenant: <strong>Acme Research</strong></span>
|
||||
<span class="badge bg-light text-dark border">Global Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
<?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/');
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Fetch Highlights Data
|
||||
$projectHighlights = db()->prepare("
|
||||
|
||||
477
labour.php
477
labour.php
@ -1,60 +1,25 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Handle Add Labour
|
||||
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']];
|
||||
}
|
||||
|
||||
$employee_id = (int)($_POST['employee_id'] ?? 0);
|
||||
$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 (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($project_id && $employee_id && $hours > 0) {
|
||||
$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();
|
||||
$labour_entry_id = (int)db()->lastInsertId();
|
||||
|
||||
// Handle File Uploads (only for the first one or all? Usually all if it's a team entry)
|
||||
// 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) {
|
||||
@ -67,26 +32,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) {
|
||||
|
||||
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 attachments (tenant_id, entity_type, entity_id, file_name, file_path, file_size, mime_type, uploaded_by) VALUES (?, 'labour_entry', ?, ?, ?, ?, ?, 'John Manager')");
|
||||
$stmt->execute([$tenant_id, $labour_entry_id, $file_name, $file_path, $file_size, $mime_type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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)"]);
|
||||
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, 'Labour Added', "Logged $hours hours for employee ID $employee_id"]);
|
||||
|
||||
header("Location: labour.php?success=1");
|
||||
exit;
|
||||
@ -94,43 +48,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) {
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -138,13 +55,13 @@ $labourEntries = db()->prepare("
|
||||
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
|
||||
WHERE le.tenant_id = ?
|
||||
ORDER BY le.entry_date DESC, le.created_at DESC
|
||||
");
|
||||
$labourEntries->execute($params);
|
||||
$labourEntries->execute([$tenant_id]);
|
||||
$labourList = $labourEntries->fetchAll();
|
||||
|
||||
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
|
||||
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name");
|
||||
$projects->execute([$tenant_id]);
|
||||
$projectList = $projects->fetchAll();
|
||||
|
||||
@ -160,28 +77,6 @@ $evidenceTypes = db()->prepare("SELECT * FROM evidence_types WHERE tenant_id = ?
|
||||
$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';
|
||||
?>
|
||||
@ -189,72 +84,8 @@ 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">
|
||||
@ -293,7 +124,7 @@ include __DIR__ . '/includes/header.php';
|
||||
</td>
|
||||
<td class="small text-muted"><?= htmlspecialchars($l['notes'] ?? '') ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-secondary">Details</button>
|
||||
<button class="btn btn-sm btn-outline-secondary">Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@ -314,48 +145,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<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">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label small fw-bold">Project</label>
|
||||
<select name="project_id" class="form-select" required>
|
||||
<option value="">Select Project...</option>
|
||||
@ -364,6 +154,15 @@ include __DIR__ . '/includes/header.php';
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label small fw-bold">Employee</label>
|
||||
<select name="employee_id" class="form-select" required>
|
||||
<option value="">Select Employee...</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">Date</label>
|
||||
<input type="date" name="entry_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
@ -406,230 +205,4 @@ include __DIR__ . '/includes/header.php';
|
||||
</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'; ?>
|
||||
|
||||
@ -1,34 +1,22 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Get Projects for filter
|
||||
$projects_stmt = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? ORDER BY name ASC");
|
||||
$projects_stmt->execute([$tenant_id]);
|
||||
$all_projects = $projects_stmt->fetchAll();
|
||||
|
||||
// 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;
|
||||
@ -96,12 +84,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
|
||||
78
login.php
78
login.php
@ -1,78 +0,0 @@
|
||||
<?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>
|
||||
@ -1,4 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::logout();
|
||||
@ -1,187 +0,0 @@
|
||||
<?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'; ?>
|
||||
160
projects.php
160
projects.php
@ -1,10 +1,8 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Handle Add Project
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) {
|
||||
@ -28,29 +26,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.*,
|
||||
@ -61,10 +42,6 @@ $query = "
|
||||
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%";
|
||||
@ -109,6 +86,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<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-outline-secondary" onclick="toggleFilters()"><i class="bi bi-funnel"></i> Filter</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">+ New Project</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -119,68 +97,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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">
|
||||
@ -210,10 +126,7 @@ include __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
<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>
|
||||
<strong><?= htmlspecialchars($p['name']) ?></strong><br>
|
||||
<code class="extra-small text-primary"><?= htmlspecialchars($p['code']) ?></code>
|
||||
</td>
|
||||
<td><small><?= htmlspecialchars($p['owner_name'] ?: 'Unassigned') ?></small></td>
|
||||
@ -229,18 +142,7 @@ include __DIR__ . '/includes/header.php';
|
||||
</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>
|
||||
<button class="btn btn-sm btn-outline-secondary">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@ -249,6 +151,56 @@ include __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Sidebar (Absolute or slide-in) -->
|
||||
<div class="filter-sidebar shadow-sm" id="projectFilterSidebar" style="display:none; position: absolute; top: 0; right: 0; z-index: 1050; width: 300px;">
|
||||
<div class="card border-0">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">FILTERS</span>
|
||||
<button type="button" class="btn-close" onclick="toggleFilters()"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="GET">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Search</label>
|
||||
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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="mb-3">
|
||||
<label class="form-label small fw-bold">Start Date</label>
|
||||
<select name="date_preset" class="form-select form-select-sm" onchange="handleDatePreset(this.value)">
|
||||
<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 Range...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="customDateRange" class="<?= $date_preset === 'custom' ? '' : 'd-none' ?>">
|
||||
<div class="mb-2">
|
||||
<label class="form-label extra-small text-muted">From</label>
|
||||
<input type="date" name="start_from" class="form-control form-control-sm" value="<?= htmlspecialchars($start_from) ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label extra-small text-muted">To</label>
|
||||
<input type="date" name="start_to" class="form-control form-control-sm" value="<?= htmlspecialchars($start_to) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Apply Filters</button>
|
||||
<a href="projects.php" class="btn btn-sm btn-link text-decoration-none">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -306,6 +258,10 @@ include __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleFilters() {
|
||||
const sidebar = document.getElementById('projectFilterSidebar');
|
||||
sidebar.style.display = sidebar.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
function handleDatePreset(val) {
|
||||
const custom = document.getElementById('customDateRange');
|
||||
if (val === 'custom') {
|
||||
|
||||
17
reports.php
17
reports.php
@ -1,11 +1,9 @@
|
||||
<?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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
$report_type = $_GET['report_type'] ?? 'labour_export';
|
||||
|
||||
@ -117,12 +115,6 @@ include __DIR__ . '/includes/header.php';
|
||||
<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'): ?>
|
||||
@ -200,9 +192,8 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-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="table-responsive">
|
||||
@ -286,7 +277,7 @@ include __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-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>
|
||||
|
||||
@ -1,228 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,105 +0,0 @@
|
||||
<?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>
|
||||
@ -1,4 +0,0 @@
|
||||
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,4 +0,0 @@
|
||||
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,4 +0,0 @@
|
||||
name,type,contact_info
|
||||
ACME Corp,supplier,admin@acme.com
|
||||
Build-It Co,contractor,contact@buildit.com
|
||||
Global Telecom,supplier,info@globaltelecom.com
|
||||
|
42
settings.php
42
settings.php
@ -4,10 +4,8 @@
|
||||
*/
|
||||
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'];
|
||||
$tenant_id = 1;
|
||||
|
||||
// Handle Form Submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@ -232,44 +230,6 @@ include __DIR__ . '/includes/header.php';
|
||||
</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>
|
||||
|
||||
@ -1,510 +0,0 @@
|
||||
<?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 | © <?= date('Y') ?>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,66 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,115 +0,0 @@
|
||||
<?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'; ?>
|
||||
@ -1,19 +0,0 @@
|
||||
<?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>";
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 281 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 196 KiB |
Loading…
x
Reference in New Issue
Block a user