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 = " +
Your account has been created. Click the button below to set your password and get started.
+ +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) {= htmlspecialchars($employee['position'] ?? 'Staff') ?> • Joined = $employee['start_date'] ? date('M j, Y', strtotime($employee['start_date'])) : 'N/A' ?>
| Login Time | +IP / Country | +
|---|---|
| No session history found. | |
|
+ = date('M j, H:i', strtotime($s['login_at'])) ?>
+ = date('Y', strtotime($s['login_at'])) ?>
+ |
+
+ = htmlspecialchars($s['ip_address']) ?>
+ = htmlspecialchars($s['country'] ?? 'Unknown') ?>
+ |
+
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.
+ +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; +} + +?> + + + + + +Enter your email to receive a reset link
+Sign in to your account
+Please set your new secure password
+Range: = date('M d, Y', strtotime($start_date)) ?> to = date('M d, Y', strtotime($end_date)) ?>
+= htmlspecialchars($company['address_1'] ?? '') ?>
+= htmlspecialchars($company['address_2']) ?>
+= htmlspecialchars($company['city'] ?? '') ?>, = htmlspecialchars($company['province'] ?? '') ?> = htmlspecialchars($company['postal_code'] ?? '') ?>
+Phone: = htmlspecialchars($company['phone'] ?? '') ?> | Email: = htmlspecialchars($company['email'] ?? '') ?>
+Website: = htmlspecialchars($company['website'] ?? '') ?>
+ +Business Number: = htmlspecialchars($company['business_number']) ?>
+ += nl2br(htmlspecialchars($project['description'] ?? 'No project description available.')) ?>
+ +| Date | +Employee | +Activity Type | +Hours | +Rate | +Total Wage | +
|---|---|---|---|---|---|
| No labour entries recorded. | |||||
| = $l['entry_date'] ?> | += htmlspecialchars($l['employee_name']) ?> | += htmlspecialchars($l['labour_type'] ?? 'N/A') ?> | += number_format((float)$l['hours'], 2) ?> | += $l['hourly_rate'] ? '$'.number_format((float)$l['hourly_rate'], 2) : '-' ?> | += $cost > 0 ? '$'.number_format($cost, 2) : '-' ?> | +
| Project Totals: | += number_format($total_proj_hours, 2) ?> h | ++ | $= number_format($total_proj_labour_cost, 2) ?> | +||
| Date | +Supplier | +Expense Type | +Notes | +Allocation | +Amount | +
|---|---|---|---|---|---|
| No expenses recorded. | |||||
| = $e['entry_date'] ?> | += htmlspecialchars($e['supplier_name']) ?> | += htmlspecialchars($e['expense_type'] ?? 'N/A') ?> | += htmlspecialchars($e['notes'] ?? '') ?> | += number_format((float)$e['allocation_percent'], 0) ?>% | +$= number_format((float)$e['amount'], 2) ?> | +
| Total Expenses: | +$= number_format($total_proj_expenses, 2) ?> | +||||
| Project Name | + += date('M y', strtotime($m)) ?> | + +Total $ | +
|---|---|---|
| = htmlspecialchars($pname) ?> | + += $h > 0 ? number_format($h, 1) : '-' ?> | + +$= number_format($row_cost, 0) ?> | +
| Monthly Totals | + $d) $m_hours += (float)($d[$m]['hours'] ?? 0); + ?> += $m_hours > 0 ? number_format($m_hours, 1) : '-' ?> | + +$= number_format($grand_total_cost, 2) ?> | +
All amounts are calculated using the recorded hourly rate at the time of labour entry.
+| Project Name | + += date('M y', strtotime($m)) ?> | + +Total $ | +
|---|---|---|
| = htmlspecialchars($pname) ?> | + += $a > 0 ? '$'.number_format($a, 0) : '-' ?> | + +$= number_format($row_amount, 0) ?> | +
| Monthly Totals | + $d) $m_amt += (float)($d[$m] ?? 0); + ?> += $m_amt > 0 ? '$'.number_format($m_amt, 0) : '-' ?> | + +$= number_format($grand_total_expense, 2) ?> | +
Includes all Labour Wages and Project Expenses for Fiscal Year = $selected_year ?>
+Generate a comprehensive Scientific Research and Experimental Development (SR&ED) claim report for a specific fiscal year.
+ + +