Password changes
This commit is contained in:
parent
c8a94f29c5
commit
36b8744143
BIN
assets/images/logo/company_logo_1771177204.png
Normal file
BIN
assets/images/logo/company_logo_1771177204.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@ -4,6 +4,8 @@
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$success = false;
|
||||
$error = '';
|
||||
|
||||
16
db/migrations/005_add_wage_to_labour.sql
Normal file
16
db/migrations/005_add_wage_to_labour.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- Add hourly_rate to labour_entries to capture wage at time of entry
|
||||
ALTER TABLE labour_entries ADD COLUMN hourly_rate DECIMAL(10, 2) DEFAULT NULL AFTER hours;
|
||||
|
||||
-- Backfill hourly_rate from employee_wages
|
||||
UPDATE labour_entries le
|
||||
JOIN (
|
||||
SELECT ew1.employee_id, ew1.hourly_rate, ew1.effective_date
|
||||
FROM employee_wages ew1
|
||||
WHERE ew1.effective_date = (
|
||||
SELECT MAX(ew2.effective_date)
|
||||
FROM employee_wages ew2
|
||||
WHERE ew2.employee_id = ew1.employee_id
|
||||
)
|
||||
) latest_wage ON le.employee_id = latest_wage.employee_id
|
||||
SET le.hourly_rate = latest_wage.hourly_rate
|
||||
WHERE le.hourly_rate IS NULL;
|
||||
27
db/migrations/006_auth_enhancements.sql
Normal file
27
db/migrations/006_auth_enhancements.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- Create password_resets table
|
||||
CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (email),
|
||||
INDEX (token)
|
||||
);
|
||||
|
||||
-- Create user_sessions table
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
country VARCHAR(100) DEFAULT NULL,
|
||||
user_agent TEXT DEFAULT NULL,
|
||||
login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Add tracking columns to users
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at DATETIME DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_ip VARCHAR(45) DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS welcome_email_sent_at DATETIME DEFAULT NULL;
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id) {
|
||||
@ -8,7 +10,14 @@ if (!$id) {
|
||||
}
|
||||
|
||||
$db = db();
|
||||
$employee = $db->prepare("SELECT * FROM employees WHERE id = ?");
|
||||
|
||||
// Fetch employee with user info
|
||||
$employee = $db->prepare("
|
||||
SELECT e.*, u.id as linked_user_id, u.email as user_email, u.last_login_at, u.welcome_email_sent_at
|
||||
FROM employees e
|
||||
LEFT JOIN users u ON e.user_id = u.id
|
||||
WHERE e.id = ?
|
||||
");
|
||||
$employee->execute([$id]);
|
||||
$employee = $employee->fetch();
|
||||
|
||||
@ -16,9 +25,77 @@ 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
|
||||
@ -97,6 +174,16 @@ function formatBytes($bytes, $precision = 2) {
|
||||
<p class="text-muted mb-0"><?= htmlspecialchars($employee['position'] ?? 'Staff') ?> • Joined <?= $employee['start_date'] ? date('M j, Y', strtotime($employee['start_date'])) : 'N/A' ?></p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<?php if (isset($success_msg)): ?>
|
||||
<div class="alert alert-success py-1 px-2 mb-0 small"><?= $success_msg ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($employee['email'] || $employee['linked_user_id']): ?>
|
||||
<form method="POST" class="d-inline">
|
||||
<button type="submit" name="send_welcome" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-envelope-at me-1"></i> <?= $employee['welcome_email_sent_at'] ? 'Resend' : 'Send' ?> Welcome Email
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-secondary btn-sm"><i class="bi bi-pencil me-1"></i> Edit Employee</button>
|
||||
<a href="labour.php?employee_id=<?= $id ?>" class="btn btn-primary btn-sm"><i class="bi bi-plus-lg me-1"></i> Add Labour</a>
|
||||
</div>
|
||||
@ -220,7 +307,7 @@ function formatBytes($bytes, $precision = 2) {
|
||||
</div>
|
||||
|
||||
<!-- Recent Files -->
|
||||
<div class="col-12">
|
||||
<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>
|
||||
@ -233,7 +320,6 @@ function formatBytes($bytes, $precision = 2) {
|
||||
<tr class="small text-muted text-uppercase">
|
||||
<th class="ps-3">Filename</th>
|
||||
<th>Linked To</th>
|
||||
<th>Project</th>
|
||||
<th>Size</th>
|
||||
<th>Date</th>
|
||||
<th class="text-end pe-3">Action</th>
|
||||
@ -241,21 +327,19 @@ function formatBytes($bytes, $precision = 2) {
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recentFiles)): ?>
|
||||
<tr><td colspan="6" class="text-center py-4 text-muted">No files found.</td></tr>
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">No files found.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recentFiles as $f): ?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-file-earmark-text me-2 text-primary"></i>
|
||||
<span class="small fw-bold text-dark"><?= htmlspecialchars($f['file_name']) ?></span>
|
||||
<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>
|
||||
<span class="text-muted ms-1"><?= $f['entry_date'] ?></span>
|
||||
</td>
|
||||
<td class="small"><?= htmlspecialchars($f['project_name'] ?? 'N/A') ?></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">
|
||||
@ -270,6 +354,45 @@ function formatBytes($bytes, $precision = 2) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Sessions -->
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h6 class="mb-0 fw-bold">Recent Sessions</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr class="extra-small text-muted text-uppercase">
|
||||
<th class="ps-3">Login Time</th>
|
||||
<th>IP / Country</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($sessions)): ?>
|
||||
<tr><td colspan="2" class="text-center py-4 text-muted small">No session history found.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($sessions as $s): ?>
|
||||
<tr>
|
||||
<td class="ps-3 small">
|
||||
<div class="fw-bold"><?= date('M j, H:i', strtotime($s['login_at'])) ?></div>
|
||||
<div class="extra-small text-muted"><?= date('Y', strtotime($s['login_at'])) ?></div>
|
||||
</td>
|
||||
<td class="small">
|
||||
<div><?= htmlspecialchars($s['ip_address']) ?></div>
|
||||
<div class="extra-small text-muted"><i class="bi bi-geo-alt me-1"></i><?= htmlspecialchars($s['country'] ?? 'Unknown') ?></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Add Employee
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Filters
|
||||
$project_filter = $_GET['project_id'] ?? '';
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?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 = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Add Expense
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
|
||||
|
||||
@ -2,13 +2,15 @@
|
||||
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 = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
$report_type = $_GET['report_type'] ?? 'labour_export';
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Filters
|
||||
$project_filter = $_GET['project_id'] ?? '';
|
||||
|
||||
88
forgot_password.php
Normal file
88
forgot_password.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
require_once __DIR__ . '/mail/MailService.php';
|
||||
|
||||
$success = false;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = $_POST['email'] ?? '';
|
||||
$ip = Auth::getIpAddress();
|
||||
|
||||
// Record attempt for security tracking
|
||||
Auth::recordResetAttempt($email, $ip);
|
||||
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE email = ? LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$email, $token, $expires]);
|
||||
|
||||
// Send Email
|
||||
$resetLink = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]/reset_password.php?token=$token";
|
||||
$subject = "Password Reset Request";
|
||||
$html = "
|
||||
<h3>Password Reset Request</h3>
|
||||
<p>We received a request to reset your password for SR&ED Manager.</p>
|
||||
<p>Click the link below to set a new password. This link will expire in 1 hour.</p>
|
||||
<p><a href='$resetLink' style='padding: 10px 20px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 5px; display: inline-block;'>Reset Password</a></p>
|
||||
<p>If you did not request this, please ignore this email.</p>
|
||||
";
|
||||
$text = "Reset your password by clicking this link: $resetLink";
|
||||
|
||||
MailService::sendMail($email, $subject, $html, $text);
|
||||
}
|
||||
|
||||
// Always show success to prevent email enumeration
|
||||
$success = true;
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Forgot Password - SR&ED Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
|
||||
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card login-card p-4">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold text-primary">RESET PASSWORD</h3>
|
||||
<p class="text-muted small">Enter your email to receive a reset link</p>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success small">
|
||||
If an account exists with that email, you will receive a password reset link shortly.
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="login.php" class="btn btn-outline-secondary w-100">Back to Login</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Email Address</label>
|
||||
<input type="email" name="email" class="form-control" placeholder="name@company.com" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3">Send Reset Link</button>
|
||||
<div class="text-center">
|
||||
<a href="login.php" class="small text-decoration-none">Back to Login</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
93
includes/auth_helper.php
Normal file
93
includes/auth_helper.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
|
||||
/**
|
||||
* Authentication Helper
|
||||
*/
|
||||
class Auth {
|
||||
public static function isLoggedIn(): bool {
|
||||
return isset($_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 {
|
||||
$_SESSION['user_id'] = $userId;
|
||||
$_SESSION['tenant_id'] = $tenantId;
|
||||
$_SESSION['role'] = $role;
|
||||
|
||||
// Ensure session is saved before any potential issues or redirects
|
||||
session_write_close();
|
||||
// Re-open session if we need to write more later (unlikely here but good practice if we were to)
|
||||
session_start();
|
||||
|
||||
$ip = self::getIpAddress();
|
||||
$country = self::getCountryFromIp($ip);
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
|
||||
try {
|
||||
// Record session
|
||||
$stmt = db()->prepare("INSERT INTO user_sessions (user_id, ip_address, country, user_agent) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$userId, $ip, $country, $userAgent]);
|
||||
|
||||
// Update user
|
||||
$stmt = db()->prepare("UPDATE users SET last_login_at = NOW(), last_login_ip = ? WHERE id = ?");
|
||||
$stmt->execute([$ip, $userId]);
|
||||
} catch (\Throwable $e) {
|
||||
// Log error but don't prevent login
|
||||
error_log("Auth::login tracking error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
if (isset($_COOKIE[session_name()])) {
|
||||
setcookie(session_name(), '', time() - 42000, '/');
|
||||
}
|
||||
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) {
|
||||
// Ignore errors for geolocation
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
public static function recordResetAttempt(string $email, string $ip): void {
|
||||
// We could log this to a separate table or activity_log
|
||||
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
|
||||
$stmt->execute([0, 'Password Reset Attempt', "Email: $email, IP: $ip"]);
|
||||
}
|
||||
}
|
||||
@ -62,6 +62,7 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
</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>
|
||||
@ -89,9 +90,20 @@ $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="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 class="dropdown">
|
||||
<button class="btn btn-light btn-sm border dropdown-toggle d-flex align-items-center" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-2"></i>
|
||||
<div class="text-start me-2">
|
||||
<div class="extra-small fw-bold lh-1"><?= htmlspecialchars($_SESSION['role'] ?? 'User') ?></div>
|
||||
<div class="extra-small text-muted lh-1">Acme Research</div>
|
||||
</div>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
|
||||
<li><h6 class="dropdown-header small">Account Settings</h6></li>
|
||||
<li><a class="dropdown-item small" href="#"><i class="bi bi-person me-2"></i>My Profile</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item small text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i>Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Fetch Highlights Data
|
||||
$projectHighlights = db()->prepare("
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?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 = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Bulk Labour
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_labour'])) {
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Filters
|
||||
$project_filter = $_GET['project_id'] ?? '';
|
||||
|
||||
78
login.php
Normal file
78
login.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
ob_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
|
||||
if (Auth::isLoggedIn()) {
|
||||
header('Location: index.php', true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
Auth::login((int)$user['id'], (int)$user['tenant_id'], (string)$user['role']);
|
||||
session_write_close();
|
||||
header('Location: index.php', true, 302);
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Invalid email or password.';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login - SR&ED Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
|
||||
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
|
||||
.btn-primary:hover { background-color: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card login-card p-4">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold text-primary">SR&ED MANAGER</h3>
|
||||
<p class="text-muted small">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Email Address</label>
|
||||
<input type="email" name="email" class="form-control" placeholder="name@company.com" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label class="form-label small fw-bold">Password</label>
|
||||
<a href="forgot_password.php" class="small text-decoration-none">Forgot?</a>
|
||||
</div>
|
||||
<input type="password" name="password" class="form-control" placeholder="••••••••" required>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember">
|
||||
<label class="form-check-label small" for="remember">Remember me</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
4
logout.php
Normal file
4
logout.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::logout();
|
||||
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Add Project
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) {
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?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 = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
$report_type = $_GET['report_type'] ?? 'labour_export';
|
||||
|
||||
@ -115,6 +117,9 @@ 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>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?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 = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Filters
|
||||
$filter_author = $_GET['author'] ?? '';
|
||||
|
||||
105
reset_password.php
Normal file
105
reset_password.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
$error = '';
|
||||
$success = false;
|
||||
|
||||
if (!$token) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM password_resets WHERE token = ? AND expires_at > NOW() LIMIT 1");
|
||||
$stmt->execute([$token]);
|
||||
$reset = $stmt->fetch();
|
||||
|
||||
if (!$reset) {
|
||||
$error = "This password reset link is invalid or has expired.";
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $reset) {
|
||||
$password = $_POST['password'] ?? '';
|
||||
$confirm = $_POST['confirm_password'] ?? '';
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
$error = "Password must be at least 8 characters long.";
|
||||
} elseif ($password !== $confirm) {
|
||||
$error = "Passwords do not match.";
|
||||
} else {
|
||||
$hashed = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
db()->beginTransaction();
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE users SET password = ?, require_password_change = 0 WHERE email = ?");
|
||||
$stmt->execute([$hashed, $reset['email']]);
|
||||
|
||||
$stmt = db()->prepare("DELETE FROM password_resets WHERE email = ?");
|
||||
$stmt->execute([$reset['email']]);
|
||||
|
||||
// Log the password change activity
|
||||
$ip = Auth::getIpAddress();
|
||||
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
|
||||
$stmt->execute([0, 'Password Changed', "Email: {$reset['email']}, IP: $ip"]);
|
||||
|
||||
db()->commit();
|
||||
$success = true;
|
||||
} catch (\Exception $e) {
|
||||
db()->rollBack();
|
||||
$error = "An error occurred while resetting your password.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Reset Password - SR&ED Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.login-card { width: 100%; max-width: 400px; border: none; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
|
||||
.btn-primary { background-color: #3b82f6; border: none; padding: 10px; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card login-card p-4">
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="fw-bold text-primary">NEW PASSWORD</h3>
|
||||
<p class="text-muted small">Please set your new secure password</p>
|
||||
</div>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success small">
|
||||
Your password has been successfully reset.
|
||||
</div>
|
||||
<a href="login.php" class="btn btn-primary w-100">Login Now</a>
|
||||
<?php elseif ($error && !$reset): ?>
|
||||
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
|
||||
<a href="forgot_password.php" class="btn btn-outline-secondary w-100">Request New Link</a>
|
||||
<?php else: ?>
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger small"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">New Password</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="••••••••" required autofocus>
|
||||
<div class="form-text extra-small">Minimum 8 characters.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">Confirm New Password</label>
|
||||
<input type="password" name="confirm_password" class="form-control" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3">Reset Password</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -4,8 +4,10 @@
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Form Submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
510
sred_claim_report.php
Normal file
510
sred_claim_report.php
Normal file
@ -0,0 +1,510 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
$selected_year = (int)($_GET['year'] ?? date('Y'));
|
||||
|
||||
// 1. Get Company Settings
|
||||
$stmt = db()->prepare("SELECT * FROM company_settings WHERE id = 1");
|
||||
$stmt->execute();
|
||||
$company = $stmt->fetch();
|
||||
|
||||
// 2. Determine Fiscal Year Range
|
||||
$fiscal_end_raw = $company['fiscal_year_end'] ?? '12-31';
|
||||
$fiscal_month = (int)date('m', strtotime($fiscal_end_raw));
|
||||
$fiscal_day = (int)date('d', strtotime($fiscal_end_raw));
|
||||
|
||||
// If fiscal end is 2024-03-31, and selected year is 2024:
|
||||
// Start: 2023-04-01, End: 2024-03-31
|
||||
$end_date = sprintf('%d-%02d-%02d', $selected_year, $fiscal_month, $fiscal_day);
|
||||
$start_date = date('Y-m-d', strtotime($end_date . ' -1 year +1 day'));
|
||||
|
||||
// 3. Fetch Projects active in this period (with labour or expenses)
|
||||
$projects_query = "
|
||||
SELECT DISTINCT p.*
|
||||
FROM projects p
|
||||
LEFT JOIN labour_entries le ON p.id = le.project_id AND le.entry_date BETWEEN ? AND ?
|
||||
LEFT JOIN expenses e ON p.id = e.project_id AND e.entry_date BETWEEN ? AND ?
|
||||
WHERE (le.id IS NOT NULL OR e.id IS NOT NULL)
|
||||
ORDER BY p.name ASC
|
||||
";
|
||||
$stmt = db()->prepare($projects_query);
|
||||
$stmt->execute([$start_date, $end_date, $start_date, $end_date]);
|
||||
$active_projects = $stmt->fetchAll();
|
||||
|
||||
// 4. Helper for Labour Data
|
||||
function getLabourForProject($projectId, $start, $end) {
|
||||
$stmt = db()->prepare("
|
||||
SELECT le.*, e.name as employee_name, lt.name as labour_type
|
||||
FROM labour_entries le
|
||||
JOIN employees e ON le.employee_id = e.id
|
||||
LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
|
||||
WHERE le.project_id = ? AND le.entry_date BETWEEN ? AND ?
|
||||
ORDER BY le.entry_date ASC
|
||||
");
|
||||
$stmt->execute([$projectId, $start, $end]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// 5. Helper for Expense Data
|
||||
function getExpensesForProject($projectId, $start, $end) {
|
||||
$stmt = db()->prepare("
|
||||
SELECT e.*, s.name as supplier_name, et.name as expense_type
|
||||
FROM expenses e
|
||||
JOIN suppliers s ON e.supplier_id = s.id
|
||||
LEFT JOIN expense_types et ON e.expense_type_id = et.id
|
||||
WHERE e.project_id = ? AND e.entry_date BETWEEN ? AND ?
|
||||
ORDER BY e.entry_date ASC
|
||||
");
|
||||
$stmt->execute([$projectId, $start, $end]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
// 6. Helper for Summary Calendar (Labour Hours)
|
||||
$summary_labour_query = "
|
||||
SELECT p.id as project_id, p.name as project_name, DATE_FORMAT(le.entry_date, '%Y-%m') as month, SUM(le.hours) as total_hours, SUM(le.hours * IFNULL(le.hourly_rate, 0)) as total_cost
|
||||
FROM labour_entries le
|
||||
JOIN projects p ON le.project_id = p.id
|
||||
WHERE le.entry_date BETWEEN ? AND ?
|
||||
GROUP BY p.id, p.name, month
|
||||
";
|
||||
$stmt = db()->prepare($summary_labour_query);
|
||||
$stmt->execute([$start_date, $end_date]);
|
||||
$summary_labour_data = $stmt->fetchAll();
|
||||
|
||||
// 7. Helper for Summary Calendar (Expenses)
|
||||
$summary_expense_query = "
|
||||
SELECT p.id as project_id, p.name as project_name, DATE_FORMAT(e.entry_date, '%Y-%m') as month, SUM(e.amount) as total_amount
|
||||
FROM expenses e
|
||||
JOIN projects p ON e.project_id = p.id
|
||||
WHERE e.entry_date BETWEEN ? AND ?
|
||||
GROUP BY p.id, p.name, month
|
||||
";
|
||||
$stmt = db()->prepare($summary_expense_query);
|
||||
$stmt->execute([$start_date, $end_date]);
|
||||
$summary_expense_data = $stmt->fetchAll();
|
||||
|
||||
// Organize Summary Data
|
||||
$months = [];
|
||||
try {
|
||||
$current_dt = new DateTime($start_date);
|
||||
$current_dt->modify('first day of this month');
|
||||
$end_dt = new DateTime($end_date);
|
||||
$end_dt->modify('first day of this month');
|
||||
|
||||
while ($current_dt <= $end_dt) {
|
||||
$months[] = $current_dt->format('Y-m');
|
||||
$current_dt->modify('+1 month');
|
||||
if (count($months) > 13) break; // Safety for roughly 1 year
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Fallback if DateTime fails
|
||||
$months = [date('Y-m', strtotime($start_date))];
|
||||
}
|
||||
|
||||
$labour_matrix = [];
|
||||
$project_names = [];
|
||||
foreach ($summary_labour_data as $row) {
|
||||
$pid = $row['project_id'];
|
||||
$project_names[$pid] = $row['project_name'];
|
||||
$labour_matrix[$pid][$row['month']] = ['hours' => $row['total_hours'], 'cost' => $row['total_cost']];
|
||||
}
|
||||
|
||||
$expense_matrix = [];
|
||||
foreach ($summary_expense_data as $row) {
|
||||
$pid = $row['project_id'];
|
||||
$project_names[$pid] = $row['project_name'];
|
||||
$expense_matrix[$pid][$row['month']] = $row['total_amount'];
|
||||
}
|
||||
|
||||
$pageTitle = "SRED Claim Report - " . $selected_year;
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= $pageTitle ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-before: always; }
|
||||
body { background: white; }
|
||||
.container { width: 100%; max-width: 100%; margin: 0; padding: 0; }
|
||||
.card { border: none !important; box-shadow: none !important; }
|
||||
}
|
||||
body { background: #f8f9fa; font-family: 'Inter', sans-serif; }
|
||||
.report-page { background: white; min-height: 297mm; padding: 20mm; margin: 20px auto; box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15); width: 210mm; }
|
||||
.title-page { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; }
|
||||
.toc-item { display: flex; justify-content: space-between; border-bottom: 1px dotted #ccc; margin-bottom: 10px; }
|
||||
.chart-container { height: 300px; }
|
||||
.table-tight th, .table-tight td { padding: 4px 8px; font-size: 0.85rem; }
|
||||
.calendar-table th, .calendar-table td { font-size: 0.75rem; text-align: center; vertical-align: middle; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="no-print bg-dark text-white p-3 sticky-top shadow d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="sred_claim_report_selector.php" class="btn btn-sm btn-outline-light me-2"><i class="bi bi-arrow-left"></i> Back</a>
|
||||
<strong>SRED Claim Report Generator</strong>
|
||||
</div>
|
||||
<button onclick="window.print()" class="btn btn-primary btn-sm"><i class="bi bi-printer"></i> Print Report / Save PDF</button>
|
||||
</div>
|
||||
|
||||
<!-- PAGE 1: TITLE PAGE -->
|
||||
<div class="report-page title-page">
|
||||
<?php if (!empty($company['logo_path'])): ?>
|
||||
<img src="<?= htmlspecialchars($company['logo_path']) ?>" alt="Logo" class="mb-5" style="max-height: 150px;">
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="margin-top: auto; margin-bottom: auto;">
|
||||
<h1 class="display-3 fw-bold mb-4"><?= htmlspecialchars($company['company_name'] ?? 'Company Name') ?></h1>
|
||||
<h2 class="text-primary mb-5">SRED CLAIM REPORT</h2>
|
||||
<h4 class="text-muted">Fiscal Year: <?= $selected_year ?></h4>
|
||||
<p class="mt-3 text-muted">Range: <?= date('M d, Y', strtotime($start_date)) ?> to <?= date('M d, Y', strtotime($end_date)) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto border-top pt-4 w-100">
|
||||
<p class="mb-1 fw-bold"><?= htmlspecialchars($company['address_1'] ?? '') ?></p>
|
||||
<?php if ($company['address_2']): ?><p class="mb-1"><?= htmlspecialchars($company['address_2']) ?></p><?php endif; ?>
|
||||
<p class="mb-1"><?= htmlspecialchars($company['city'] ?? '') ?>, <?= htmlspecialchars($company['province'] ?? '') ?> <?= htmlspecialchars($company['postal_code'] ?? '') ?></p>
|
||||
<p class="mb-1">Phone: <?= htmlspecialchars($company['phone'] ?? '') ?> | Email: <?= htmlspecialchars($company['email'] ?? '') ?></p>
|
||||
<p class="mb-0">Website: <?= htmlspecialchars($company['website'] ?? '') ?></p>
|
||||
<?php if ($company['business_number']): ?>
|
||||
<p class="mt-2 fw-bold">Business Number: <?= htmlspecialchars($company['business_number']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PAGE 2: TABLE OF CONTENTS -->
|
||||
<div class="report-page page-break">
|
||||
<h2 class="mb-5 border-bottom pb-3">Table of Contents</h2>
|
||||
<div class="mt-4">
|
||||
<div class="toc-item"><span>1. Title Page</span><span>1</span></div>
|
||||
<div class="toc-item"><span>2. Table of Contents</span><span>2</span></div>
|
||||
<?php
|
||||
$page_counter = 3;
|
||||
foreach ($active_projects as $p):
|
||||
$proj_start_page = $page_counter;
|
||||
?>
|
||||
<div class="toc-item fw-bold mt-3"><span>Project: <?= htmlspecialchars($p['name']) ?></span><span><?= $page_counter++ ?></span></div>
|
||||
<div class="toc-item ps-4 text-muted"><span>Insights & Charts</span><span><?= ($page_counter - 1) ?></span></div>
|
||||
<div class="toc-item ps-4 text-muted"><span>Labour Detail Report</span><span><?= $page_counter++ ?></span></div>
|
||||
<div class="toc-item ps-4 text-muted"><span>Expense Detail Report</span><span><?= $page_counter++ ?></span></div>
|
||||
<?php endforeach; ?>
|
||||
<div class="toc-item fw-bold mt-4"><span>Summarized Claim (Calendar Views)</span><span><?= $page_counter++ ?></span></div>
|
||||
<div class="toc-item ps-4 text-muted"><span>Labour Summary</span><span><?= ($page_counter - 1) ?></span></div>
|
||||
<div class="toc-item ps-4 text-muted"><span>Expense Summary</span><span><?= $page_counter++ ?></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT PAGES -->
|
||||
<?php foreach ($active_projects as $idx => $project):
|
||||
$labour = getLabourForProject($project['id'], $start_date, $end_date);
|
||||
$expenses = getExpensesForProject($project['id'], $start_date, $end_date);
|
||||
|
||||
// Process Insights
|
||||
$employee_hours = [];
|
||||
$labour_type_hours = [];
|
||||
$total_proj_hours = 0;
|
||||
$total_proj_labour_cost = 0;
|
||||
foreach ($labour as $l) {
|
||||
$employee_hours[$l['employee_name']] = ($employee_hours[$l['employee_name']] ?? 0) + (float)$l['hours'];
|
||||
$labour_type_hours[$l['labour_type'] ?? 'Other'] = ($labour_type_hours[$l['labour_type'] ?? 'Other'] ?? 0) + (float)$l['hours'];
|
||||
$total_proj_hours += (float)$l['hours'];
|
||||
$total_proj_labour_cost += (float)$l['hours'] * (float)($l['hourly_rate'] ?? 0);
|
||||
}
|
||||
arsort($employee_hours);
|
||||
$top_employees = array_slice($employee_hours, 0, 5, true);
|
||||
|
||||
$total_proj_expenses = 0;
|
||||
foreach ($expenses as $e) {
|
||||
$total_proj_expenses += (float)$e['amount'];
|
||||
}
|
||||
?>
|
||||
<!-- Project Page 1: Insights -->
|
||||
<div class="report-page page-break">
|
||||
<div class="d-flex justify-content-between align-items-start mb-4 border-bottom pb-3">
|
||||
<div>
|
||||
<h6 class="text-uppercase text-primary fw-bold mb-1">Project Analysis</h6>
|
||||
<h2 class="mb-0"><?= htmlspecialchars($project['name']) ?></h2>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-light text-dark border">Project ID: <?= htmlspecialchars($project['code'] ?? $project['id']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="lead text-muted mb-4"><?= nl2br(htmlspecialchars($project['description'] ?? 'No project description available.')) ?></p>
|
||||
|
||||
<div class="row mb-5">
|
||||
<div class="col-6">
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted small text-uppercase">Total Labour Hours</h6>
|
||||
<h3 class="mb-0 fw-bold"><?= number_format($total_proj_hours, 1) ?> h</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted small text-uppercase">Total Estimated Cost</h6>
|
||||
<h3 class="mb-0 fw-bold text-success">$<?= number_format($total_proj_labour_cost + $total_proj_expenses, 2) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<h6 class="fw-bold mb-3">Top Employees by Hours</h6>
|
||||
<div class="chart-container">
|
||||
<canvas id="chart_emp_<?= $project['id'] ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h6 class="fw-bold mb-3">Labour by Category</h6>
|
||||
<div class="chart-container">
|
||||
<canvas id="chart_type_<?= $project['id'] ?>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Chart(document.getElementById('chart_emp_<?= $project['id'] ?>').getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: <?= json_encode(array_keys($top_employees)) ?>,
|
||||
datasets: [{
|
||||
label: 'Hours',
|
||||
data: <?= json_encode(array_values($top_employees)) ?>,
|
||||
backgroundColor: '#0d6efd'
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
||||
});
|
||||
new Chart(document.getElementById('chart_type_<?= $project['id'] ?>').getContext('2d'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: <?= json_encode(array_keys($labour_type_hours)) ?>,
|
||||
datasets: [{
|
||||
data: <?= json_encode(array_values($labour_type_hours)) ?>,
|
||||
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#0dcaf0', '#6c757d']
|
||||
}]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: false }
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Project Page 2: Labour Details -->
|
||||
<div class="report-page page-break">
|
||||
<h4 class="mb-4 border-bottom pb-2">Labour Detail Report: <?= htmlspecialchars($project['name']) ?></h4>
|
||||
<table class="table table-bordered table-tight">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Employee</th>
|
||||
<th>Activity Type</th>
|
||||
<th class="text-end">Hours</th>
|
||||
<th class="text-end">Rate</th>
|
||||
<th class="text-end">Total Wage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($labour)): ?>
|
||||
<tr><td colspan="6" class="text-center text-muted">No labour entries recorded.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($labour as $l):
|
||||
$cost = (float)$l['hours'] * (float)($l['hourly_rate'] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $l['entry_date'] ?></td>
|
||||
<td><?= htmlspecialchars($l['employee_name']) ?></td>
|
||||
<td><?= htmlspecialchars($l['labour_type'] ?? 'N/A') ?></td>
|
||||
<td class="text-end"><?= number_format((float)$l['hours'], 2) ?></td>
|
||||
<td class="text-end"><?= $l['hourly_rate'] ? '$'.number_format((float)$l['hourly_rate'], 2) : '-' ?></td>
|
||||
<td class="text-end fw-bold"><?= $cost > 0 ? '$'.number_format($cost, 2) : '-' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="bg-light">
|
||||
<tr>
|
||||
<td colspan="3" class="text-end fw-bold">Project Totals:</td>
|
||||
<td class="text-end fw-bold text-primary"><?= number_format($total_proj_hours, 2) ?> h</td>
|
||||
<td></td>
|
||||
<td class="text-end fw-bold text-success">$<?= number_format($total_proj_labour_cost, 2) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="mt-4 extra-small text-muted">
|
||||
Note: Hourly rates are recorded at the time of entry to reflect historical wage adjustments accurately.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Page 3: Expense Details -->
|
||||
<div class="report-page page-break">
|
||||
<h4 class="mb-4 border-bottom pb-2">Expense Detail Report: <?= htmlspecialchars($project['name']) ?></h4>
|
||||
<table class="table table-bordered table-tight">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Supplier</th>
|
||||
<th>Expense Type</th>
|
||||
<th>Notes</th>
|
||||
<th class="text-end">Allocation</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($expenses)): ?>
|
||||
<tr><td colspan="6" class="text-center text-muted">No expenses recorded.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($expenses as $e): ?>
|
||||
<tr>
|
||||
<td><?= $e['entry_date'] ?></td>
|
||||
<td><?= htmlspecialchars($e['supplier_name']) ?></td>
|
||||
<td><?= htmlspecialchars($e['expense_type'] ?? 'N/A') ?></td>
|
||||
<td style="max-width: 200px;" class="text-truncate"><?= htmlspecialchars($e['notes'] ?? '') ?></td>
|
||||
<td class="text-end"><?= number_format((float)$e['allocation_percent'], 0) ?>%</td>
|
||||
<td class="text-end fw-bold">$<?= number_format((float)$e['amount'], 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="bg-light">
|
||||
<tr>
|
||||
<td colspan="5" class="text-end fw-bold">Total Expenses:</td>
|
||||
<td class="text-end fw-bold text-success">$<?= number_format($total_proj_expenses, 2) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- SUMMARIZED CLAIM - LABOUR -->
|
||||
<div class="report-page page-break">
|
||||
<h3 class="mb-4 border-bottom pb-2">Summarized Claim: Monthly Labour Hours</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered calendar-table">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="text-start">Project Name</th>
|
||||
<?php foreach ($months as $m): ?>
|
||||
<th><?= date('M y', strtotime($m)) ?></th>
|
||||
<?php endforeach; ?>
|
||||
<th class="bg-secondary text-white">Total $</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$grand_total_hours = 0;
|
||||
$grand_total_cost = 0;
|
||||
foreach ($labour_matrix as $pid => $data):
|
||||
$row_cost = 0;
|
||||
$pname = $project_names[$pid] ?? 'Unknown Project';
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-start fw-bold"><?= htmlspecialchars($pname) ?></td>
|
||||
<?php foreach ($months as $m):
|
||||
$h = (float)($data[$m]['hours'] ?? 0);
|
||||
$c = (float)($data[$m]['cost'] ?? 0);
|
||||
$row_cost += $c;
|
||||
$grand_total_hours += $h;
|
||||
?>
|
||||
<td><?= $h > 0 ? number_format($h, 1) : '-' ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td class="fw-bold">$<?= number_format($row_cost, 0) ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
$grand_total_cost += $row_cost;
|
||||
endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="bg-light fw-bold">
|
||||
<tr>
|
||||
<td class="text-start">Monthly Totals</td>
|
||||
<?php foreach ($months as $m):
|
||||
$m_hours = 0;
|
||||
foreach($labour_matrix as $pid => $d) $m_hours += (float)($d[$m]['hours'] ?? 0);
|
||||
?>
|
||||
<td><?= $m_hours > 0 ? number_format($m_hours, 1) : '-' ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td class="text-success">$<?= number_format($grand_total_cost, 2) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p class="small text-muted mt-3">All amounts are calculated using the recorded hourly rate at the time of labour entry.</p>
|
||||
</div>
|
||||
|
||||
<!-- SUMMARIZED CLAIM - EXPENSES -->
|
||||
<div class="report-page page-break">
|
||||
<h3 class="mb-4 border-bottom pb-2">Summarized Claim: Monthly Expenses</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered calendar-table">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="text-start">Project Name</th>
|
||||
<?php foreach ($months as $m): ?>
|
||||
<th><?= date('M y', strtotime($m)) ?></th>
|
||||
<?php endforeach; ?>
|
||||
<th class="bg-secondary text-white">Total $</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$grand_total_expense = 0;
|
||||
foreach ($expense_matrix as $pid => $data):
|
||||
$row_amount = 0;
|
||||
$pname = $project_names[$pid] ?? 'Unknown Project';
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-start fw-bold"><?= htmlspecialchars($pname) ?></td>
|
||||
<?php foreach ($months as $m):
|
||||
$a = (float)($data[$m] ?? 0);
|
||||
$row_amount += $a;
|
||||
?>
|
||||
<td><?= $a > 0 ? '$'.number_format($a, 0) : '-' ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td class="fw-bold">$<?= number_format($row_amount, 0) ?></td>
|
||||
</tr>
|
||||
<?php
|
||||
$grand_total_expense += $row_amount;
|
||||
endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="bg-light fw-bold">
|
||||
<tr>
|
||||
<td class="text-start">Monthly Totals</td>
|
||||
<?php foreach ($months as $m):
|
||||
$m_amt = 0;
|
||||
foreach($expense_matrix as $pid => $d) $m_amt += (float)($d[$m] ?? 0);
|
||||
?>
|
||||
<td><?= $m_amt > 0 ? '$'.number_format($m_amt, 0) : '-' ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td class="text-success">$<?= number_format($grand_total_expense, 2) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<div class="border p-4 d-inline-block">
|
||||
<h5 class="text-uppercase text-muted small mb-3">Total Captured Wages and Expenses for SR&ED Claim</h5>
|
||||
<h1 class="display-4 fw-bold text-primary">$<?= number_format($grand_total_cost + $grand_total_expense, 2) ?></h1>
|
||||
<p class="mb-0 text-muted">Includes all Labour Wages and Project Expenses for Fiscal Year <?= $selected_year ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="no-print mt-5 text-center p-4 text-muted border-top bg-white">
|
||||
SR&ED Manager Claim Report Generator | © <?= date('Y') ?>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
66
sred_claim_report_selector.php
Normal file
66
sred_claim_report_selector.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Fetch company settings to get fiscal year end
|
||||
$stmt = db()->prepare("SELECT fiscal_year_end FROM company_settings WHERE id = 1");
|
||||
$stmt->execute();
|
||||
$settings = $stmt->fetch();
|
||||
|
||||
$fiscal_year_end = $settings['fiscal_year_end'] ?? null;
|
||||
|
||||
// Determine possible years based on data
|
||||
$years = [];
|
||||
$res = db()->query("SELECT DISTINCT YEAR(entry_date) as y FROM labour_entries UNION SELECT DISTINCT YEAR(entry_date) as y FROM expenses ORDER BY y DESC");
|
||||
while($row = $res->fetch()) {
|
||||
$years[] = (int)$row['y'];
|
||||
}
|
||||
if (empty($years)) $years[] = (int)date('Y');
|
||||
|
||||
$pageTitle = "SR&ED Claim Report Selector";
|
||||
include __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-5 text-center">
|
||||
<i class="bi bi-file-earmark-bar-graph text-primary mb-4" style="font-size: 3rem;"></i>
|
||||
<h2 class="fw-bold mb-3">SRED Claim Report</h2>
|
||||
<p class="text-muted mb-4">Generate a comprehensive Scientific Research and Experimental Development (SR&ED) claim report for a specific fiscal year.</p>
|
||||
|
||||
<form action="sred_claim_report.php" method="GET">
|
||||
<div class="mb-4 text-start">
|
||||
<label class="form-label fw-bold small text-uppercase">Select Fiscal Year</label>
|
||||
<select name="year" class="form-select form-select-lg">
|
||||
<?php foreach($years as $y): ?>
|
||||
<option value="<?= $y ?>" <?= $y == date('Y') ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if ($fiscal_year_end): ?>
|
||||
<div class="form-text mt-2">
|
||||
Your fiscal year ends on: <strong><?= date('F jS', strtotime($fiscal_year_end)) ?></strong>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="form-text mt-2 text-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i> Fiscal year end not set in <a href="company_settings.php">Company Preferences</a>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||
Generate Report <i class="bi bi-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/includes/footer.php'; ?>
|
||||
@ -4,8 +4,10 @@
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/auth_helper.php';
|
||||
Auth::requireLogin();
|
||||
|
||||
$tenant_id = 1;
|
||||
$tenant_id = (int)$_SESSION['tenant_id'];
|
||||
|
||||
// Handle Form Submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user