diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..0d3fda7
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,120 @@
+body {
+ background-color: #f8fafc;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ color: #0f172a;
+ font-size: 0.875rem;
+}
+
+.navbar {
+ background-color: #ffffff;
+ border-bottom: 1px solid #e2e8f0;
+ padding: 0.75rem 1.5rem;
+}
+
+.navbar-brand {
+ font-weight: 700;
+ color: #0f172a;
+ letter-spacing: -0.025em;
+}
+
+.nav-link {
+ color: #475569;
+ font-weight: 500;
+}
+
+.nav-link:hover {
+ color: #3b82f6;
+}
+
+.dropdown-menu {
+ border: 1px solid #e2e8f0;
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ border-radius: 0.375rem;
+ padding: 0.5rem;
+}
+
+.dropdown-item {
+ border-radius: 0.25rem;
+ padding: 0.5rem 0.75rem;
+ color: #475569;
+}
+
+.dropdown-item:hover {
+ background-color: #f1f5f9;
+ color: #3b82f6;
+}
+
+.card {
+ background-color: #ffffff;
+ border: 1px solid #e2e8f0;
+ border-radius: 0.375rem;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+}
+
+.card-header {
+ background-color: #ffffff;
+ border-bottom: 1px solid #e2e8f0;
+ padding: 1rem 1.5rem;
+ font-weight: 600;
+}
+
+.btn-primary {
+ background-color: #3b82f6;
+ border-color: #3b82f6;
+ font-weight: 500;
+}
+
+.btn-primary:hover {
+ background-color: #2563eb;
+ border-color: #2563eb;
+}
+
+.table {
+ margin-bottom: 0;
+}
+
+.table th {
+ background-color: #f8fafc;
+ color: #64748b;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ padding: 0.75rem 1.5rem;
+ border-top: none;
+}
+
+.table td {
+ padding: 1rem 1.5rem;
+ vertical-align: middle;
+}
+
+.status-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 9999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.status-active { background-color: #dcfce7; color: #166534; }
+.status-on-hold { background-color: #fef9c3; color: #854d0e; }
+.status-completed { background-color: #dbeafe; color: #1e40af; }
+
+.activity-feed {
+ list-style: none;
+ padding: 0;
+}
+
+.activity-item {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #f1f5f9;
+}
+
+.activity-item:last-child {
+ border-bottom: none;
+}
+
+.activity-time {
+ font-size: 0.75rem;
+ color: #94a3b8;
+}
diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..e92c915
--- /dev/null
+++ b/db/migrations/001_initial_schema.sql
@@ -0,0 +1,43 @@
+-- Initial Schema for SR&ED Manager
+CREATE TABLE IF NOT EXISTS tenants (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) UNIQUE NOT NULL,
+ role ENUM('global_admin', 'tenant_admin', 'staff') DEFAULT 'staff',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE SET NULL
+);
+
+CREATE TABLE IF NOT EXISTS projects (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ code VARCHAR(50) NOT NULL,
+ status ENUM('active', 'completed', 'on_hold') DEFAULT 'active',
+ start_date DATE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS activity_log (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ user_id INT,
+ action VARCHAR(255) NOT NULL,
+ details TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Seed Initial Demo Data
+INSERT IGNORE INTO tenants (id, name) VALUES (1, 'Acme Research Corp');
+INSERT IGNORE INTO users (id, tenant_id, name, email, role) VALUES (1, 1, 'John Manager', 'john@acme.com', 'tenant_admin');
+INSERT IGNORE INTO projects (tenant_id, name, code, start_date) VALUES (1, 'Project Alpha: Quantum AI', 'PA-001', '2025-01-01');
+INSERT IGNORE INTO activity_log (tenant_id, user_id, action, details) VALUES (1, 1, 'System Setup', 'Initial tenant environment created.');
diff --git a/db/migrations/002_labour_module.sql b/db/migrations/002_labour_module.sql
new file mode 100644
index 0000000..fc7ea51
--- /dev/null
+++ b/db/migrations/002_labour_module.sql
@@ -0,0 +1,74 @@
+-- Migration for Labour Module and Centralized Attachments
+
+-- Table for Employees
+CREATE TABLE IF NOT EXISTS employees (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255),
+ position VARCHAR(255),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Table for Labour Types (e.g., Experimental Development, Technical Support)
+CREATE TABLE IF NOT EXISTS labour_types (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Table for Objective Evidence Types (e.g., Logbooks, Test Results, Design Docs)
+CREATE TABLE IF NOT EXISTS evidence_types (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Table for Labour Entries
+CREATE TABLE IF NOT EXISTS labour_entries (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ project_id INT NOT NULL,
+ employee_id INT NOT NULL,
+ entry_date DATE NOT NULL,
+ hours DECIMAL(10, 2) NOT NULL,
+ labour_type_id INT,
+ evidence_type_id INT,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
+ FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
+ FOREIGN KEY (labour_type_id) REFERENCES labour_types(id) ON DELETE SET NULL,
+ FOREIGN KEY (evidence_type_id) REFERENCES evidence_types(id) ON DELETE SET NULL
+);
+
+-- Centralized Attachments Table
+CREATE TABLE IF NOT EXISTS attachments (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ entity_type VARCHAR(50) NOT NULL, -- 'labour_entry', 'project', 'expense', etc.
+ entity_id INT NOT NULL,
+ file_name VARCHAR(255) NOT NULL,
+ file_path VARCHAR(255) NOT NULL,
+ file_size INT,
+ mime_type VARCHAR(100),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
+);
+
+-- Seed some initial settings for the demo tenant
+INSERT IGNORE INTO employees (tenant_id, name, position) VALUES (1, 'Alice Smith', 'Lead Researcher');
+INSERT IGNORE INTO employees (tenant_id, name, position) VALUES (1, 'Bob Jones', 'Developer');
+
+INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Experimental Development');
+INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Technical Support');
+INSERT IGNORE INTO labour_types (tenant_id, name) VALUES (1, 'Technical Planning');
+
+INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Logbooks');
+INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Test Results');
+INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Design Documents');
+INSERT IGNORE INTO evidence_types (tenant_id, name) VALUES (1, 'Source Code Commits');
diff --git a/index.php b/index.php
index 7205f3d..f1183ab 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,387 @@
prepare("INSERT INTO projects (tenant_id, name, code, start_date) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$tenant_id, $name, $code, $start_date]);
+
+ // Log Activity
+ $stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
+ $stmt->execute([$tenant_id, 'Project Created', "Added project: $name ($code)"]);
+
+ header("Location: index.php?success=1");
+ exit;
+ }
+}
+
+// Handle Add Labour
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) {
+ $project_id = (int)($_POST['project_id'] ?? 0);
+ $employee_id = (int)($_POST['employee_id'] ?? 0);
+ $entry_date = $_POST['entry_date'] ?? date('Y-m-d');
+ $hours = (float)($_POST['hours'] ?? 0);
+ $labour_type_id = (int)($_POST['labour_type_id'] ?? 0);
+ $evidence_type_id = (int)($_POST['evidence_type_id'] ?? 0);
+ $notes = $_POST['notes'] ?? '';
+
+ if ($project_id && $employee_id && $hours > 0) {
+ $stmt = db()->prepare("INSERT INTO labour_entries (tenant_id, project_id, employee_id, entry_date, hours, labour_type_id, evidence_type_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$tenant_id, $project_id, $employee_id, $entry_date, $hours, $labour_type_id, $evidence_type_id, $notes]);
+ $labour_entry_id = (int)db()->lastInsertId();
+
+ // Handle File Uploads
+ if (!empty($_FILES['attachments']['name'][0])) {
+ foreach ($_FILES['attachments']['tmp_name'] as $key => $tmp_name) {
+ $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 (?, 'labour_entry', ?, ?, ?, ?, ?)");
+ $stmt->execute([$tenant_id, $labour_entry_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, 'Labour Added', "Logged $hours hours for employee ID $employee_id"]);
+
+ header("Location: index.php?success=labour");
+ 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->execute([$tenant_id]);
+$employeeList = $employees->fetchAll();
+
+$labourTypes = db()->prepare("SELECT * FROM labour_types WHERE tenant_id = ? ORDER BY name");
+$labourTypes->execute([$tenant_id]);
+$labourTypeList = $labourTypes->fetchAll();
+
+$evidenceTypes = db()->prepare("SELECT * FROM evidence_types WHERE tenant_id = ? ORDER BY name");
+$evidenceTypes->execute([$tenant_id]);
+$evidenceTypeList = $evidenceTypes->fetchAll();
+
+$labourEntries = db()->prepare("
+ SELECT le.*, p.name as project_name, e.name as employee_name, lt.name as labour_type, et.name as evidence_type
+ FROM labour_entries le
+ JOIN projects p ON le.project_id = p.id
+ JOIN employees e ON le.employee_id = e.id
+ LEFT JOIN labour_types lt ON le.labour_type_id = lt.id
+ LEFT JOIN evidence_types et ON le.evidence_type_id = et.id
+ WHERE le.tenant_id = ?
+ ORDER BY le.entry_date DESC, le.created_at DESC
+");
+$labourEntries->execute([$tenant_id]);
+$labourList = $labourEntries->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();
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'SR&ED Project Tracking Software';
?>
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ SR&ED Manager - Dashboard
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+
+
+
+
+
+
+
+
+ = $_GET['success'] === 'labour' ? 'Labour entry successfully saved.' : 'Project successfully added to the tracker.' ?>
+
+
+
+
+
+
+
+
+
+
+ | Project Name |
+ Code |
+ Start Date |
+ Status |
+ Actions |
+
+
+
+
+
+ | = htmlspecialchars($p['name']) ?> |
+ = htmlspecialchars($p['code']) ?> |
+ = $p['start_date'] ?> |
+ = ucfirst($p['status']) ?> |
+
+
+ |
+
+
+
+ | No projects found. Start by adding one. |
+
+
+
+
+
+
+
+
+
+
+
+
+ | Date |
+ Employee |
+ Project |
+ Hours |
+ Type / Evidence |
+ Actions |
+
+
+
+
+
+ | = $l['entry_date'] ?> |
+ = htmlspecialchars($l['employee_name']) ?> |
+ = htmlspecialchars($l['project_name']) ?> |
+ = number_format((float)$l['hours'], 2) ?> h |
+
+ = htmlspecialchars($l['labour_type'] ?? 'N/A') ?>
+
+ |
+
+
+ |
+
+
+
+ | No labour entries found. |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
= htmlspecialchars($a['action']) ?>
+
+ = date('M d, H:i', strtotime($a['created_at'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+