From f03a7a8de5572ce2a74cfb78ec151fcf1ba32492 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 15 Feb 2026 00:26:46 +0000 Subject: [PATCH] employees --- db/migrations/003_expense_module.sql | 48 +++ db/migrations/004_enhanced_employees.sql | 50 +++ index.php | 526 +++++++++++++++++++++-- 3 files changed, 592 insertions(+), 32 deletions(-) create mode 100644 db/migrations/003_expense_module.sql create mode 100644 db/migrations/004_enhanced_employees.sql diff --git a/db/migrations/003_expense_module.sql b/db/migrations/003_expense_module.sql new file mode 100644 index 0000000..f043079 --- /dev/null +++ b/db/migrations/003_expense_module.sql @@ -0,0 +1,48 @@ +-- Table for Suppliers/Contractors +CREATE TABLE IF NOT EXISTS suppliers ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + type ENUM('supplier', 'contractor') DEFAULT 'supplier', + contact_info TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX (tenant_id) +); + +-- Table for Expense Types (Company editable list) +CREATE TABLE IF NOT EXISTS expense_types ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX (tenant_id) +); + +-- Table for Expenses +CREATE TABLE IF NOT EXISTS expenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + project_id INT NOT NULL, + supplier_id INT NOT NULL, + expense_type_id INT NOT NULL, + amount DECIMAL(15, 2) NOT NULL, + allocation_percent DECIMAL(5, 2) DEFAULT 100.00, + entry_date DATE NOT NULL, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (project_id) REFERENCES projects(id), + FOREIGN KEY (supplier_id) REFERENCES suppliers(id), + FOREIGN KEY (expense_type_id) REFERENCES expense_types(id), + INDEX (tenant_id) +); + +-- Insert some default data for the demo tenant (tenant_id = 1) +INSERT INTO suppliers (tenant_id, name, type, contact_info) VALUES +(1, 'Cloud Services Inc.', 'supplier', 'billing@cloudservices.com'), +(1, 'John Doe Consulting', 'contractor', 'john@doe.com'); + +INSERT INTO expense_types (tenant_id, name) VALUES +(1, 'Materials'), +(1, 'Subcontractors'), +(1, 'Software Licenses'), +(1, 'Overhead'); diff --git a/db/migrations/004_enhanced_employees.sql b/db/migrations/004_enhanced_employees.sql new file mode 100644 index 0000000..7348143 --- /dev/null +++ b/db/migrations/004_enhanced_employees.sql @@ -0,0 +1,50 @@ +-- Migration for Enhanced Employee Management + +-- 1. Alter employees table to add required fields +ALTER TABLE employees ADD COLUMN first_name VARCHAR(100) AFTER tenant_id; +ALTER TABLE employees ADD COLUMN last_name VARCHAR(100) AFTER first_name; +ALTER TABLE employees ADD COLUMN start_date DATE AFTER position; +ALTER TABLE employees ADD COLUMN is_limited BOOLEAN DEFAULT TRUE AFTER start_date; +ALTER TABLE employees ADD COLUMN user_id INT NULL AFTER is_limited; +ALTER TABLE employees ADD CONSTRAINT fk_employee_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + +-- Migrate existing names to first/last (rough split) +UPDATE employees SET first_name = SUBSTRING_INDEX(name, ' ', 1), last_name = SUBSTRING_INDEX(name, ' ', -1) WHERE name LIKE '% %'; +UPDATE employees SET first_name = name, last_name = '' WHERE name NOT LIKE '% %'; + +-- 2. Wage History Table +CREATE TABLE IF NOT EXISTS employee_wages ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + employee_id INT NOT NULL, + hourly_rate DECIMAL(10, 2) NOT NULL, + effective_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE +); + +-- 3. Teams Table +CREATE TABLE IF NOT EXISTS teams ( + id INT AUTO_INCREMENT PRIMARY KEY, + tenant_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); + +-- 4. Employee-Team Link (Many-to-Many) +CREATE TABLE IF NOT EXISTS employee_teams ( + employee_id INT NOT NULL, + team_id INT NOT NULL, + tenant_id INT NOT NULL, + PRIMARY KEY (employee_id, team_id), + FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE, + FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE +); + +-- Seed some teams +INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'Engineering'); +INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'R&D'); +INSERT IGNORE INTO teams (tenant_id, name) VALUES (1, 'Product Management'); diff --git a/index.php b/index.php index f1183ab..61838ae 100644 --- a/index.php +++ b/index.php @@ -5,6 +5,83 @@ require_once __DIR__ . '/db/config.php'; // Simulate Tenant Context (Hardcoded for demo) $tenant_id = 1; +// Handle Add Employee +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) { + $first_name = $_POST['first_name'] ?? ''; + $last_name = $_POST['last_name'] ?? ''; + $email = $_POST['email'] ?? ''; + $position = $_POST['position'] ?? ''; + $start_date = $_POST['start_date'] ?? date('Y-m-d'); + $is_limited = isset($_POST['is_limited']) ? 1 : 0; + $initial_wage = (float)($_POST['initial_wage'] ?? 0); + $team_ids = $_POST['teams'] ?? []; + + if ($first_name && $last_name) { + $user_id = null; + // If not limited, create a user account + if (!$is_limited && $email) { + $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) { // Already exists + $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, 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(); + + // Initial Wage + if ($initial_wage > 0) { + $stmt = db()->prepare("INSERT INTO employee_wages (tenant_id, employee_id, hourly_rate, effective_date) VALUES (?, ?, ?, ?)"); + $stmt->execute([$tenant_id, $employee_id, $initial_wage, $start_date]); + } + + // Teams + if (!empty($team_ids)) { + foreach ($team_ids as $tid) { + $stmt = db()->prepare("INSERT INTO employee_teams (tenant_id, employee_id, team_id) VALUES (?, ?, ?)"); + $stmt->execute([$tenant_id, $employee_id, $tid]); + } + } + + // Log Activity + $stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)"); + $stmt->execute([$tenant_id, 'Employee Created', "Added employee: $first_name $last_name"]); + + header("Location: index.php?success=employee"); + exit; + } +} + +// Handle Add Team +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_team'])) { + $name = $_POST['name'] ?? ''; + if ($name) { + $stmt = db()->prepare("INSERT INTO teams (tenant_id, name) VALUES (?, ?)"); + $stmt->execute([$tenant_id, $name]); + header("Location: index.php?success=team"); + exit; + } +} + +// Handle Add Wage Adjustment +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_wage'])) { + $employee_id = (int)$_POST['employee_id']; + $rate = (float)$_POST['hourly_rate']; + $effective_date = $_POST['effective_date']; + + if ($employee_id && $rate > 0) { + $stmt = db()->prepare("INSERT INTO employee_wages (tenant_id, employee_id, hourly_rate, effective_date) VALUES (?, ?, ?, ?)"); + $stmt->execute([$tenant_id, $employee_id, $rate, $effective_date]); + header("Location: index.php?success=wage"); + exit; + } +} + // Handle Add Project if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) { $name = $_POST['name'] ?? ''; @@ -65,15 +142,90 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) { } } +// Handle Add Expense +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) { + $project_id = (int)($_POST['project_id'] ?? 0); + $supplier_id = (int)($_POST['supplier_id'] ?? 0); + $expense_type_id = (int)($_POST['expense_type_id'] ?? 0); + $amount = (float)($_POST['amount'] ?? 0); + $allocation = (float)($_POST['allocation_percent'] ?? 100); + $entry_date = $_POST['entry_date'] ?? date('Y-m-d'); + $notes = $_POST['notes'] ?? ''; + + if ($project_id && $supplier_id && $amount > 0) { + $stmt = db()->prepare("INSERT INTO expenses (tenant_id, project_id, supplier_id, expense_type_id, amount, allocation_percent, entry_date, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$tenant_id, $project_id, $supplier_id, $expense_type_id, $amount, $allocation, $entry_date, $notes]); + $expense_id = (int)db()->lastInsertId(); + + // Handle File Uploads (Centralized Attachments) + if (!empty($_FILES['attachments']['name'][0])) { + foreach ($_FILES['attachments']['tmp_name'] as $key => $tmp_name) { + if (!$_FILES['attachments']['error'][$key]) { + $file_name = $_FILES['attachments']['name'][$key]; + $file_size = $_FILES['attachments']['size'][$key]; + $mime_type = $_FILES['attachments']['type'][$key]; + $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); + $new_file_name = uniqid() . '.' . $file_ext; + $file_path = 'uploads/' . $new_file_name; + + if (move_uploaded_file($tmp_name, $file_path)) { + $stmt = db()->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, file_size, mime_type) VALUES (?, 'expense', ?, ?, ?, ?, ?)"); + $stmt->execute([$tenant_id, $expense_id, $file_name, $file_path, $file_size, $mime_type]); + } + } + } + } + + // Log Activity + $stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)"); + $stmt->execute([$tenant_id, 'Expense Logged', "Logged \$" . number_format($amount, 2) . " expense for project ID $project_id"]); + + header("Location: index.php?success=expense"); + exit; + } +} + +// Handle Add Supplier +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_supplier'])) { + $name = $_POST['name'] ?? ''; + $type = $_POST['type'] ?? 'supplier'; + $contact = $_POST['contact_info'] ?? ''; + + if ($name) { + $stmt = db()->prepare("INSERT INTO suppliers (tenant_id, name, type, contact_info) VALUES (?, ?, ?, ?)"); + $stmt->execute([$tenant_id, $name, $type, $contact]); + header("Location: index.php?success=supplier"); + exit; + } +} + // Fetch Data $projects = db()->prepare("SELECT * FROM projects WHERE tenant_id = ? ORDER BY created_at DESC"); $projects->execute([$tenant_id]); $projectList = $projects->fetchAll(); -$employees = db()->prepare("SELECT * FROM employees WHERE tenant_id = ? ORDER BY name"); +$employees = db()->prepare(" + SELECT e.*, + (SELECT hourly_rate FROM employee_wages WHERE employee_id = e.id ORDER BY effective_date DESC LIMIT 1) as current_wage + FROM employees e + WHERE e.tenant_id = ? + ORDER BY e.first_name, e.last_name +"); $employees->execute([$tenant_id]); $employeeList = $employees->fetchAll(); +$teams = db()->prepare("SELECT * FROM teams WHERE tenant_id = ? ORDER BY name"); +$teams->execute([$tenant_id]); +$teamList = $teams->fetchAll(); + +$suppliers = db()->prepare("SELECT * FROM suppliers WHERE tenant_id = ? ORDER BY name"); +$suppliers->execute([$tenant_id]); +$supplierList = $suppliers->fetchAll(); + +$expenseTypes = db()->prepare("SELECT * FROM expense_types WHERE tenant_id = ? ORDER BY name"); +$expenseTypes->execute([$tenant_id]); +$expenseTypeList = $expenseTypes->fetchAll(); + $labourTypes = db()->prepare("SELECT * FROM labour_types WHERE tenant_id = ? ORDER BY name"); $labourTypes->execute([$tenant_id]); $labourTypeList = $labourTypes->fetchAll(); @@ -95,6 +247,18 @@ $labourEntries = db()->prepare(" $labourEntries->execute([$tenant_id]); $labourList = $labourEntries->fetchAll(); +$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 e.tenant_id = ? + ORDER BY e.entry_date DESC, e.created_at DESC +"); +$expenseEntries->execute([$tenant_id]); +$expenseList = $expenseEntries->fetchAll(); + $activities = db()->prepare("SELECT * FROM activity_log WHERE tenant_id = ? ORDER BY created_at DESC LIMIT 10"); $activities->execute([$tenant_id]); $activityList = $activities->fetchAll(); @@ -138,14 +302,20 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking @@ -168,11 +338,20 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking
-
@@ -205,9 +384,6 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking - - No projects found. Start by adding one. -
@@ -246,16 +422,97 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking - - No labour entries found. - + + +
+
+ +
+
+ Recent Expenses + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
DateSupplierProjectAmountAllocationActions

$ +
+
+
+ % SR&ED +
+ +
+
+
+ +
+
+ Manage Employees + +
+
+ + + + + + + + + + + + + + + + + + + +
NameTeamsWageAccessActions
+
+ +
+ prepare("SELECT t.name FROM teams t JOIN employee_teams et ON t.id = et.team_id WHERE et.employee_id = ?"); + $e_teams->execute([$e['id']]); + $t_names = $e_teams->fetchAll(PDO::FETCH_COLUMN); + foreach ($t_names as $tn) { + echo '' . htmlspecialchars($tn) . ''; + } + ?> + $/h + +
-
Activity Hub
@@ -264,7 +521,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking
  • -
    +
  • @@ -275,10 +532,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking
    - +