diff --git a/assets/images/logo/company_logo_1771177204.png b/assets/images/logo/company_logo_1771177204.png new file mode 100644 index 0000000..5802d9f Binary files /dev/null and b/assets/images/logo/company_logo_1771177204.png differ diff --git a/company_settings.php b/company_settings.php index 1113c65..eef5efa 100644 --- a/company_settings.php +++ b/company_settings.php @@ -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 = ''; diff --git a/db/migrations/005_add_wage_to_labour.sql b/db/migrations/005_add_wage_to_labour.sql new file mode 100644 index 0000000..92392bc --- /dev/null +++ b/db/migrations/005_add_wage_to_labour.sql @@ -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; diff --git a/db/migrations/006_auth_enhancements.sql b/db/migrations/006_auth_enhancements.sql new file mode 100644 index 0000000..cf423f0 --- /dev/null +++ b/db/migrations/006_auth_enhancements.sql @@ -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; diff --git a/employee_detail.php b/employee_detail.php index 68b34bb..9c2cf9c 100644 --- a/employee_detail.php +++ b/employee_detail.php @@ -1,5 +1,7 @@ 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 = " +

Welcome to SR&ED Manager!

+

Your account has been created. Click the button below to set your password and get started.

+

Set Up Account

+

This link will expire in 48 hours.

+ "; + $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) {

• Joined

+ +
+ + +
+ +
+ Add Labour
@@ -220,7 +307,7 @@ function formatBytes($bytes, $precision = 2) { -
+
Recent Files & Attachments
@@ -233,7 +320,6 @@ function formatBytes($bytes, $precision = 2) { Filename Linked To - Project Size Date Action @@ -241,21 +327,19 @@ function formatBytes($bytes, $precision = 2) { - No files found. + No files found.
- +
- - @@ -270,6 +354,45 @@ function formatBytes($bytes, $precision = 2) {
+ + +
+
+
+
Recent Sessions
+
+
+
+ + + + + + + + + + + + + + + + + + + +
Login TimeIP / Country
No session history found.
+
+
+
+
+
+
+
+
+
+
diff --git a/employees.php b/employees.php index 4669a9e..f2f7a2a 100644 --- a/employees.php +++ b/employees.php @@ -1,8 +1,10 @@ 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 = " +

Password Reset Request

+

We received a request to reset your password for SR&ED Manager.

+

Click the link below to set a new password. This link will expire in 1 hour.

+

Reset Password

+

If you did not request this, please ignore this email.

+ "; + $text = "Reset your password by clicking this link: $resetLink"; + + MailService::sendMail($email, $subject, $html, $text); + } + + // Always show success to prevent email enumeration + $success = true; +} + +?> + + + + + + Forgot Password - SR&ED Manager + + + + + +
+
+

RESET PASSWORD

+

Enter your email to receive a reset link

+
+ + +
+ If an account exists with that email, you will receive a password reset link shortly. +
+
+ Back to Login +
+ +
+
+ + +
+ + +
+ +
+ + diff --git a/includes/auth_helper.php b/includes/auth_helper.php new file mode 100644 index 0000000..539eee1 --- /dev/null +++ b/includes/auth_helper.php @@ -0,0 +1,93 @@ +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"]); + } +} diff --git a/includes/header.php b/includes/header.php index 17cf347..41b3b79 100644 --- a/includes/header.php +++ b/includes/header.php @@ -62,6 +62,7 @@ $currentPage = basename($_SERVER['PHP_SELF']); @@ -89,9 +90,20 @@ $currentPage = basename($_SERVER['PHP_SELF']); -
- Tenant: Acme Research - Global Admin +
diff --git a/index.php b/index.php index 784074d..355f4c1 100644 --- a/index.php +++ b/index.php @@ -1,8 +1,10 @@ prepare(" diff --git a/labour.php b/labour.php index 25fd0f2..754529e 100644 --- a/labour.php +++ b/labour.php @@ -1,9 +1,11 @@ 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.'; + } +} + +?> + + + + + + Login - SR&ED Manager + + + + + +
+
+

SR&ED MANAGER

+

Sign in to your account

+
+ + +
+ + +
+
+ + +
+
+
+ + Forgot? +
+ +
+
+ + +
+ +
+
+ + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..cfe6a75 --- /dev/null +++ b/logout.php @@ -0,0 +1,4 @@ + Monthly Calendar + diff --git a/reports_media.php b/reports_media.php index 2e5012b..8960187 100644 --- a/reports_media.php +++ b/reports_media.php @@ -1,9 +1,11 @@ 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."; + } + } +} + +?> + + + + + + Reset Password - SR&ED Manager + + + + + +
+
+

NEW PASSWORD

+

Please set your new secure password

+
+ + +
+ Your password has been successfully reset. +
+ Login Now + +
+ Request New Link + + +
+ +
+
+ + +
Minimum 8 characters.
+
+
+ + +
+ +
+ +
+ + diff --git a/settings.php b/settings.php index 1bbaf1f..6160bfb 100644 --- a/settings.php +++ b/settings.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 Submissions if ($_SERVER['REQUEST_METHOD'] === 'POST') { diff --git a/sred_claim_report.php b/sred_claim_report.php new file mode 100644 index 0000000..217e508 --- /dev/null +++ b/sred_claim_report.php @@ -0,0 +1,510 @@ +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; +?> + + + + + + <?= $pageTitle ?> + + + + + + + +
+
+ Back + SRED Claim Report Generator +
+ +
+ + +
+ + Logo + + +
+

+

SRED CLAIM REPORT

+

Fiscal Year:

+

Range: to

+
+ +
+

+

+

,

+

Phone: | Email:

+

Website:

+ +

Business Number:

+ +
+
+ + +
+

Table of Contents

+
+
1. Title Page1
+
2. Table of Contents2
+ +
Project:
+
Insights & Charts
+
Labour Detail Report
+
Expense Detail Report
+ +
Summarized Claim (Calendar Views)
+
Labour Summary
+
Expense Summary
+
+
+ + + $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 Analysis
+

+
+
+ Project ID: +
+
+ +

+ +
+
+
+
+
Total Labour Hours
+

h

+
+
+
+
+
+
+
Total Estimated Cost
+

$

+
+
+
+
+ +
+
+
Top Employees by Hours
+
+ +
+
+
+
Labour by Category
+
+ +
+
+
+ + +
+ + +
+

Labour Detail Report:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateEmployeeActivity TypeHoursRateTotal Wage
No labour entries recorded.
0 ? '$'.number_format($cost, 2) : '-' ?>
Project Totals: h$
+
+ Note: Hourly rates are recorded at the time of entry to reflect historical wage adjustments accurately. +
+
+ + +
+

Expense Detail Report:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateSupplierExpense TypeNotesAllocationAmount
No expenses recorded.
%$
Total Expenses:$
+
+ + + +
+

Summarized Claim: Monthly Labour Hours

+
+ + + + + + + + + + + + $data): + $row_cost = 0; + $pname = $project_names[$pid] ?? 'Unknown Project'; + ?> + + + + + + + + + + + + + $d) $m_hours += (float)($d[$m]['hours'] ?? 0); + ?> + + + + + +
Project NameTotal $
0 ? number_format($h, 1) : '-' ?>$
Monthly Totals 0 ? number_format($m_hours, 1) : '-' ?>$
+
+

All amounts are calculated using the recorded hourly rate at the time of labour entry.

+
+ + +
+

Summarized Claim: Monthly Expenses

+
+ + + + + + + + + + + + $data): + $row_amount = 0; + $pname = $project_names[$pid] ?? 'Unknown Project'; + ?> + + + + + + + + + + + + + $d) $m_amt += (float)($d[$m] ?? 0); + ?> + + + + + +
Project NameTotal $
0 ? '$'.number_format($a, 0) : '-' ?>$
Monthly Totals 0 ? '$'.number_format($m_amt, 0) : '-' ?>$
+
+ +
+
+
Total Captured Wages and Expenses for SR&ED Claim
+

$

+

Includes all Labour Wages and Project Expenses for Fiscal Year

+
+
+
+ + + + + diff --git a/sred_claim_report_selector.php b/sred_claim_report_selector.php new file mode 100644 index 0000000..465ec5d --- /dev/null +++ b/sred_claim_report_selector.php @@ -0,0 +1,66 @@ +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'; +?> + +
+
+
+
+
+ +

SRED Claim Report

+

Generate a comprehensive Scientific Research and Experimental Development (SR&ED) claim report for a specific fiscal year.

+ +
+
+ + + +
+ Your fiscal year ends on: +
+ +
+ Fiscal year end not set in Company Preferences. +
+ +
+ + +
+
+
+
+
+
+ + diff --git a/system_preferences.php b/system_preferences.php index da18782..8c912e0 100644 --- a/system_preferences.php +++ b/system_preferences.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') {