diff --git a/db/migrations/20260215_create_clients_table.sql b/db/migrations/20260215_create_clients_table.sql
new file mode 100644
index 0000000..0f06e57
--- /dev/null
+++ b/db/migrations/20260215_create_clients_table.sql
@@ -0,0 +1,15 @@
+-- Create clients table
+CREATE TABLE IF NOT EXISTS clients (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ tenant_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255),
+ phone VARCHAR(50),
+ address TEXT,
+ city VARCHAR(100),
+ province_state VARCHAR(100),
+ postal_code VARCHAR(20),
+ country VARCHAR(100),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX (tenant_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/employees.php b/employees.php
index 7346ac0..4669a9e 100644
--- a/employees.php
+++ b/employees.php
@@ -12,24 +12,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
$position = $_POST['position'] ?? '';
$start_date = $_POST['start_date'] ?? date('Y-m-d');
$is_limited = isset($_POST['is_limited']) ? 1 : 0;
+ $phone = $_POST['phone'] ?? '';
+ $password = $_POST['password'] ?? '';
+ $force_password_change = isset($_POST['force_password_change']) ? 1 : 0;
$initial_wage = (float)($_POST['initial_wage'] ?? 0);
$team_ids = $_POST['teams'] ?? [];
if ($first_name && $last_name) {
$user_id = null;
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) {
- $stmt = db()->prepare("SELECT id FROM users WHERE email = ?");
- $stmt->execute([$email]);
- $user_id = (int)($stmt->fetchColumn() ?: null);
- }
+ $hashed_password = $password ? password_hash($password, PASSWORD_DEFAULT) : null;
+ $stmt = db()->prepare("INSERT INTO users (tenant_id, name, email, phone, password, require_password_change, role)
+ VALUES (?, ?, ?, ?, ?, ?, 'staff')
+ ON DUPLICATE KEY UPDATE
+ phone = VALUES(phone),
+ password = COALESCE(VALUES(password), password),
+ require_password_change = VALUES(require_password_change)");
+ $stmt->execute([$tenant_id, "$first_name $last_name", $email, $phone, $hashed_password, $force_password_change]);
+
+ $stmt = db()->prepare("SELECT id FROM users WHERE email = ? AND tenant_id = ?");
+ $stmt->execute([$email, $tenant_id]);
+ $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"]);
+ $stmt = db()->prepare("INSERT INTO employees (tenant_id, first_name, last_name, email, phone, position, start_date, is_limited, user_id, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$tenant_id, $first_name, $last_name, $email, $phone, $position, $start_date, $is_limited, $user_id, "$first_name $last_name"]);
$employee_id = (int)db()->lastInsertId();
if ($initial_wage > 0) {
@@ -53,6 +60,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_employee'])) {
}
// Fetch Data
+$stmt = db()->prepare("SELECT pref_key, pref_value FROM system_preferences WHERE tenant_id = ?");
+$stmt->execute([$tenant_id]);
+$prefs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
+
$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
@@ -156,6 +167,10 @@ include __DIR__ . '/includes/header.php';
+
+
+
+
@@ -179,12 +194,34 @@ include __DIR__ . '/includes/header.php';
-
+
+
diff --git a/import_clients.php b/import_clients.php
new file mode 100644
index 0000000..5c851c3
--- /dev/null
+++ b/import_clients.php
@@ -0,0 +1,158 @@
+beginTransaction();
+ try {
+ $stmt = db()->prepare("INSERT INTO clients (tenant_id, name, email, phone, address, city, province_state, postal_code, country) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+ while (($row = fgetcsv($handle)) !== false) {
+ $rowCount++;
+ $data = [];
+ foreach ($expected as $col) {
+ $data[$col] = isset($columnMap[$col]) && isset($row[$columnMap[$col]]) ? trim($row[$columnMap[$col]]) : '';
+ }
+
+ if (empty($data['name'])) {
+ $skippedCount++;
+ $results[] = "Row $rowCount: Skipped (Missing Name)";
+ continue;
+ }
+
+ // Simple validation: email
+ if (!empty($data['email']) && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
+ $results[] = "Row $rowCount: Warning (Invalid Email: " . htmlspecialchars($data['email']) . ")";
+ }
+
+ $stmt->execute([
+ 1, // Hardcoded tenant_id for now as per app convention
+ $data['name'],
+ $data['email'],
+ $data['phone'],
+ $data['address'],
+ $data['city'],
+ $data['province_state'],
+ $data['postal_code'],
+ $data['country']
+ ]);
+ $importCount++;
+ }
+ db()->commit();
+ $message = "Import completed successfully. $importCount clients imported, $skippedCount skipped.";
+ } catch (Exception $e) {
+ db()->rollBack();
+ $error = 'Database error: ' . $e->getMessage();
+ }
+ }
+ fclose($handle);
+ }
+}
+
+include 'includes/header.php';
+?>
+
+
+
+
Import Clients
+
+ Download Template
+
+
+
+
+
+ = $message ?>
+
+
+
+
+
+
+ = $error ?>
+
+
+
+
+
+
+
+
+
+
+
+ - Download the CSV template using the button above.
+ - Fill in your client data, ensuring the name field is not empty.
+ - Save the file as a CSV (Comma Separated Values).
+ - Upload the file using the form on the left.
+ - Review any warnings or errors that appear after processing.
+
+
+ Existing clients with the same name will be added as new entries.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/import_labour.php b/import_labour.php
new file mode 100644
index 0000000..a8f6892
--- /dev/null
+++ b/import_labour.php
@@ -0,0 +1,185 @@
+query("SELECT id, code FROM projects WHERE tenant_id = 1");
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $projects[$row['code']] = $row['id'];
+
+ $employees = [];
+ $stmt = db()->query("SELECT id, email FROM employees WHERE tenant_id = 1");
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $employees[$row['email']] = $row['id'];
+
+ $labourTypes = [];
+ $stmt = db()->query("SELECT id, name FROM labour_types");
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $labourTypes[strtolower($row['name'])] = $row['id'];
+
+ $evidenceTypes = [];
+ $stmt = db()->query("SELECT id, name FROM evidence_types");
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $evidenceTypes[strtolower($row['name'])] = $row['id'];
+
+ $rowCount = 0;
+ $importCount = 0;
+ $skippedCount = 0;
+
+ db()->beginTransaction();
+ try {
+ $stmt = db()->prepare("INSERT INTO labour_entries (tenant_id, project_id, employee_id, entry_date, hours, labour_type_id, evidence_type_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+
+ while (($row = fgetcsv($handle)) !== false) {
+ $rowCount++;
+ $data = [];
+ foreach ($expected as $col) {
+ $data[$col] = isset($columnMap[$col]) && isset($row[$columnMap[$col]]) ? trim($row[$columnMap[$col]]) : '';
+ }
+
+ $rowErrors = [];
+
+ $projectId = $projects[$data['project_code']] ?? null;
+ $employeeId = $employees[$data['employee_email']] ?? null;
+ $labourTypeId = $labourTypes[strtolower($data['labour_type'])] ?? null;
+ $evidenceTypeId = $evidenceTypes[strtolower($data['evidence_type'])] ?? 5; // Default to 'None'
+
+ if (!$projectId) $rowErrors[] = "Invalid Project Code: " . $data['project_code'];
+ if (!$employeeId) $rowErrors[] = "Invalid Employee Email: " . $data['employee_email'];
+ if (!$labourTypeId) $rowErrors[] = "Invalid Labour Type: " . $data['labour_type'];
+ if (empty($data['date']) || !strtotime($data['date'])) $rowErrors[] = "Invalid Date: " . $data['date'];
+ if (!is_numeric($data['hours'])) $rowErrors[] = "Invalid Hours: " . $data['hours'];
+
+ if (!empty($rowErrors)) {
+ $skippedCount++;
+ $results[] = "Row $rowCount: Skipped (" . implode(', ', $rowErrors) . ")";
+ continue;
+ }
+
+ $stmt->execute([
+ 1,
+ $projectId,
+ $employeeId,
+ date('Y-m-d', strtotime($data['date'])),
+ $data['hours'],
+ $labourTypeId,
+ $evidenceTypeId,
+ $data['notes']
+ ]);
+ $importCount++;
+ }
+ db()->commit();
+ $message = "Import completed successfully. $importCount activities imported, $skippedCount skipped.";
+ } catch (Exception $e) {
+ db()->rollBack();
+ $error = 'Database error: ' . $e->getMessage();
+ }
+ }
+ fclose($handle);
+ }
+}
+
+include 'includes/header.php';
+?>
+
+
+
+
Import Labour Activities
+
+ Download Template
+
+
+
+
+
+ = $message ?>
+
+
+
+
+
+
+ = $error ?>
+
+
+
+
+
+
+
+
+
+
+
+ - Download the CSV template using the button above.
+ - Ensure Project Code and Employee Email match existing records.
+ - Date should be in YYYY-MM-DD format.
+ - Labour Type must match one of:
+
+ - Experimental Development
+ - Technical Support
+ - Technical Planning
+
+
+ - Upload and review the logs for any skipped rows.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/includes/header.php b/includes/header.php
index fc0107d..8fb1562 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -66,8 +66,17 @@ $currentPage = basename($_SERVER['PHP_SELF']);
-
- Settings
+
+
+ Settings
+
+
diff --git a/samples/clients_template.csv b/samples/clients_template.csv
new file mode 100644
index 0000000..90a61ef
--- /dev/null
+++ b/samples/clients_template.csv
@@ -0,0 +1,3 @@
+name,email,phone,address,city,province_state,postal_code,country
+Acme Corp,contact@acme.com,555-0123,123 Industrial Way,Tech City,ON,M5V 2N2,Canada
+Global Tech,info@globaltech.io,555-9876,456 Innovation Blvd,Futureville,QC,H3B 1A1,Canada
diff --git a/samples/labour_template.csv b/samples/labour_template.csv
new file mode 100644
index 0000000..04fdd07
--- /dev/null
+++ b/samples/labour_template.csv
@@ -0,0 +1,4 @@
+project_code,employee_email,date,hours,labour_type,evidence_type,notes
+PROJ-001,john.doe@example.com,2026-02-01,7.5,Experimental Development,Logbooks,Analysis of component A
+PROJ-001,jane.smith@example.com,2026-02-01,4.0,Technical Support,Test Results,Testing component B
+PROJ-002,john.doe@example.com,2026-02-02,6.0,Technical Planning,Design Documents,Drafting architecture
diff --git a/settings.php b/settings.php
index 4f6eb12..a2a7f3e 100644
--- a/settings.php
+++ b/settings.php
@@ -230,6 +230,28 @@ include __DIR__ . '/includes/header.php';
+
+
+
+
+
+
+
Import legacy data or bulk records from other systems using CSV templates.
+
+
+
+
diff --git a/system_preferences.php b/system_preferences.php
new file mode 100644
index 0000000..da18782
--- /dev/null
+++ b/system_preferences.php
@@ -0,0 +1,113 @@
+ $_POST['pwd_min_length'] ?? '8',
+ 'pwd_require_upper' => isset($_POST['pwd_require_upper']) ? '1' : '0',
+ 'pwd_require_lower' => isset($_POST['pwd_require_lower']) ? '1' : '0',
+ 'pwd_require_numbers' => isset($_POST['pwd_require_numbers']) ? '1' : '0',
+ 'pwd_no_common_words' => isset($_POST['pwd_no_common_words']) ? '1' : '0'
+ ];
+
+ foreach ($prefs as $key => $val) {
+ $stmt = db()->prepare("INSERT INTO system_preferences (tenant_id, pref_key, pref_value) VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value)");
+ $stmt->execute([$tenant_id, $key, $val]);
+ }
+
+ $stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
+ $stmt->execute([$tenant_id, 'Settings Updated', 'Updated system preferences and password requirements']);
+
+ header("Location: system_preferences.php?success=1");
+ exit;
+}
+
+// Fetch current preferences
+$stmt = db()->prepare("SELECT pref_key, pref_value FROM system_preferences WHERE tenant_id = ?");
+$stmt->execute([$tenant_id]);
+$prefs = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
+
+$pageTitle = "SR&ED Manager - System Preferences";
+include __DIR__ . '/includes/header.php';
+?>
+
+
+
+
+
+
System Preferences
+
+ Preferences saved successfully
+
+
+
+
+
+
+
+
+
diff --git a/uploads/6991f572867cb.png b/uploads/6991f572867cb.png
new file mode 100644
index 0000000..ad2d2cd
Binary files /dev/null and b/uploads/6991f572867cb.png differ
diff --git a/uploads/thumb_6991f572867cb.png b/uploads/thumb_6991f572867cb.png
new file mode 100644
index 0000000..2073d98
Binary files /dev/null and b/uploads/thumb_6991f572867cb.png differ