Import functions
This commit is contained in:
parent
0f6b05982a
commit
5ba9886549
15
db/migrations/20260215_create_clients_table.sql
Normal file
15
db/migrations/20260215_create_clients_table.sql
Normal file
@ -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;
|
||||
@ -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';
|
||||
<label class="form-label small fw-bold">Email</label>
|
||||
<input type="email" name="email" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label small fw-bold">Telephone (for 2FA)</label>
|
||||
<input type="text" name="phone" class="form-control" placeholder="+1234567890">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label small fw-bold">Position</label>
|
||||
<input type="text" name="position" class="form-control">
|
||||
@ -179,12 +194,34 @@ include __DIR__ . '/includes/header.php';
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="is_limited" id="limitedCheck" checked>
|
||||
|
||||
<div class="col-12 mb-3">
|
||||
<div class="form-check form-switch p-3 border rounded">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="is_limited" id="limitedCheck" checked onchange="togglePasswordFields()">
|
||||
<label class="form-check-label small fw-bold" for="limitedCheck">Limited Web Reporting (Cannot Login)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="passwordSection" style="display: none;">
|
||||
<div class="row p-3 border rounded bg-light mx-1 mb-3">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label small fw-bold">Password</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="password" id="employeePassword" class="form-control" placeholder="Set or generate password">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="generatePassword()">Generate</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="force_password_change" id="forceChange" checked>
|
||||
<label class="form-check-label small fw-bold" for="forceChange">Require change</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="extra-small text-muted">Passwords must meet complexity requirements defined in System Preferences.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
@ -195,4 +232,29 @@ include __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePasswordFields() {
|
||||
const isLimited = document.getElementById('limitedCheck').checked;
|
||||
document.getElementById('passwordSection').style.display = isLimited ? 'none' : 'block';
|
||||
if (!isLimited) {
|
||||
document.getElementById('employeePassword').required = true;
|
||||
} else {
|
||||
document.getElementById('employeePassword').required = false;
|
||||
document.getElementById('employeePassword').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function generatePassword() {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||
let retVal = "";
|
||||
for (let i = 0, n = charset.length; i < 12; ++i) {
|
||||
retVal += charset.charAt(Math.floor(Math.random() * n));
|
||||
}
|
||||
document.getElementById('employeePassword').value = retVal;
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
document.addEventListener('DOMContentLoaded', togglePasswordFields);
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
158
import_clients.php
Normal file
158
import_clients.php
Normal file
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
$pageTitle = 'Import Clients';
|
||||
|
||||
$message = '';
|
||||
$error = '';
|
||||
$results = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
$file = $_FILES['csv_file'];
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$error = 'File upload failed.';
|
||||
} else {
|
||||
$handle = fopen($file['tmp_name'], 'r');
|
||||
$header = fgetcsv($handle);
|
||||
|
||||
// Expected columns: name, email, phone, address, city, province_state, postal_code, country
|
||||
$expected = ['name', 'email', 'phone', 'address', 'city', 'province_state', 'postal_code', 'country'];
|
||||
|
||||
if (!$header || count(array_intersect($expected, $header)) < 1) {
|
||||
$error = 'Invalid CSV format. Please use the provided template.';
|
||||
} else {
|
||||
$columnMap = array_flip($header);
|
||||
$rowCount = 0;
|
||||
$importCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
db()->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';
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-file-earmark-arrow-up me-2"></i>Import Clients</h2>
|
||||
<a href="samples/clients_template.csv" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-download me-1"></i>Download Template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= $message ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<?= $error ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<form action="import_clients.php" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-4">
|
||||
<label for="csv_file" class="form-label fw-bold">Select CSV File</label>
|
||||
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
<div class="form-text mt-2">
|
||||
Max file size: 2MB. Only .csv files are allowed.
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 py-2">
|
||||
<i class="bi bi-upload me-2"></i>Upload and Import
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0">Import Instructions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ol class="small text-muted">
|
||||
<li>Download the CSV template using the button above.</li>
|
||||
<li>Fill in your client data, ensuring the <strong>name</strong> field is not empty.</li>
|
||||
<li>Save the file as a CSV (Comma Separated Values).</li>
|
||||
<li>Upload the file using the form on the left.</li>
|
||||
<li>Review any warnings or errors that appear after processing.</li>
|
||||
</ol>
|
||||
<div class="alert alert-info py-2 small mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i> Existing clients with the same name will be added as new entries.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0">Import Logs</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush small">
|
||||
<?php foreach ($results as $res): ?>
|
||||
<div class="list-group-item px-0 border-0 py-1">
|
||||
<i class="bi bi-dot me-1"></i><?= $res ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
185
import_labour.php
Normal file
185
import_labour.php
Normal file
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
$pageTitle = 'Import Labour Activities';
|
||||
|
||||
$message = '';
|
||||
$error = '';
|
||||
$results = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
||||
$file = $_FILES['csv_file'];
|
||||
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$error = 'File upload failed.';
|
||||
} else {
|
||||
$handle = fopen($file['tmp_name'], 'r');
|
||||
$header = fgetcsv($handle);
|
||||
|
||||
$expected = ['project_code', 'employee_email', 'date', 'hours', 'labour_type', 'evidence_type', 'notes'];
|
||||
|
||||
if (!$header || count(array_intersect($expected, $header)) < 1) {
|
||||
$error = 'Invalid CSV format. Please use the provided template.';
|
||||
} else {
|
||||
$columnMap = array_flip($header);
|
||||
|
||||
// Pre-fetch lookups
|
||||
$projects = [];
|
||||
$stmt = db()->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';
|
||||
?>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-clock-history me-2"></i>Import Labour Activities</h2>
|
||||
<a href="samples/labour_template.csv" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-download me-1"></i>Download Template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= $message ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<?= $error ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<form action="import_labour.php" method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-4">
|
||||
<label for="csv_file" class="form-label fw-bold">Select CSV File</label>
|
||||
<input type="file" class="form-control" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
<div class="form-text mt-2">
|
||||
Max file size: 5MB. Only .csv files are allowed.
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 py-2">
|
||||
<i class="bi bi-upload me-2"></i>Upload and Import
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0">Import Instructions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ol class="small text-muted">
|
||||
<li>Download the CSV template using the button above.</li>
|
||||
<li>Ensure <strong>Project Code</strong> and <strong>Employee Email</strong> match existing records.</li>
|
||||
<li><strong>Date</strong> should be in YYYY-MM-DD format.</li>
|
||||
<li><strong>Labour Type</strong> must match one of:
|
||||
<ul class="mb-0">
|
||||
<li>Experimental Development</li>
|
||||
<li>Technical Support</li>
|
||||
<li>Technical Planning</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Upload and review the logs for any skipped rows.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($results)): ?>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0">Import Logs</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush small">
|
||||
<?php foreach ($results as $res): ?>
|
||||
<div class="list-group-item px-0 border-0 py-1">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i><?= $res ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
@ -66,8 +66,17 @@ $currentPage = basename($_SERVER['PHP_SELF']);
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="files.php">Files</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $currentPage === 'settings.php' ? 'active' : '' ?>" href="settings.php">Settings</a>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['settings.php', 'system_preferences.php', 'import_clients.php', 'import_labour.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
|
||||
Settings
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="settings.php">Datasets</a></li>
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="system_preferences.php">System Preferences</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="import_clients.php">Import Clients</a></li>
|
||||
<li><a class="dropdown-menu-item dropdown-item" href="import_labour.php">Import Labour</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
|
||||
3
samples/clients_template.csv
Normal file
3
samples/clients_template.csv
Normal file
@ -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
|
||||
|
4
samples/labour_template.csv
Normal file
4
samples/labour_template.csv
Normal file
@ -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
|
||||
|
22
settings.php
22
settings.php
@ -230,6 +230,28 @@ include __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Management -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<span class="fw-bold">Data Management & Imports</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted mb-4">Import legacy data or bulk records from other systems using CSV templates.</p>
|
||||
<div class="d-grid gap-3">
|
||||
<a href="import_clients.php" class="btn btn-outline-secondary d-flex justify-content-between align-items-center py-2">
|
||||
<span><i class="bi bi-people me-2"></i>Import Clients</span>
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
<a href="import_labour.php" class="btn btn-outline-secondary d-flex justify-content-between align-items-center py-2">
|
||||
<span><i class="bi bi-clock-history me-2"></i>Import Labour Activities</span>
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
113
system_preferences.php
Normal file
113
system_preferences.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* System Preferences - Manage Password Requirements and Security
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$tenant_id = 1;
|
||||
|
||||
// Handle Form Submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$prefs = [
|
||||
'pwd_min_length' => $_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';
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold mb-0">System Preferences</h2>
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<span class="badge bg-success py-2 px-3">Preferences saved successfully</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">Password Requirements</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold small">Minimum Characters</label>
|
||||
<input type="number" name="pwd_min_length" class="form-control" value="<?= htmlspecialchars($prefs['pwd_min_length'] ?? '8') ?>" min="4" max="32">
|
||||
<div class="form-text extra-small text-muted">Passwords shorter than this will be rejected.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch p-3 border rounded">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_upper" id="reqUpper" <?= ($prefs['pwd_require_upper'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold small" for="reqUpper">Require Uppercase Letters</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch p-3 border rounded">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_lower" id="reqLower" <?= ($prefs['pwd_require_lower'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold small" for="reqLower">Require Lowercase Letters</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch p-3 border rounded">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_require_numbers" id="reqNumbers" <?= ($prefs['pwd_require_numbers'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold small" for="reqNumbers">Require Numbers</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch p-3 border rounded">
|
||||
<input class="form-check-input ms-0 me-2" type="checkbox" name="pwd_no_common_words" id="noCommon" <?= ($prefs['pwd_no_common_words'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label fw-bold small" for="noCommon">Don't Allow Common Words</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">Authentication & 2FA</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-0">2FA settings are currently managed per-user in the Employee management section. Telephone numbers provided there will be used for SMS-based verification factors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<a href="settings.php" class="btn btn-light px-4">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-4">Save Preferences</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/includes/footer.php'; ?>
|
||||
BIN
uploads/6991f572867cb.png
Normal file
BIN
uploads/6991f572867cb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
BIN
uploads/thumb_6991f572867cb.png
Normal file
BIN
uploads/thumb_6991f572867cb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
Loading…
x
Reference in New Issue
Block a user