Import functions

This commit is contained in:
Flatlogic Bot 2026-02-15 16:51:04 +00:00
parent 0f6b05982a
commit 5ba9886549
11 changed files with 586 additions and 15 deletions

View 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;

View File

@ -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
View 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
View 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'; ?>

View File

@ -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">

View 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
1 name email phone address city province_state postal_code country
2 Acme Corp contact@acme.com 555-0123 123 Industrial Way Tech City ON M5V 2N2 Canada
3 Global Tech info@globaltech.io 555-9876 456 Innovation Blvd Futureville QC H3B 1A1 Canada

View 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
1 project_code employee_email date hours labour_type evidence_type notes
2 PROJ-001 john.doe@example.com 2026-02-01 7.5 Experimental Development Logbooks Analysis of component A
3 PROJ-001 jane.smith@example.com 2026-02-01 4.0 Technical Support Test Results Testing component B
4 PROJ-002 john.doe@example.com 2026-02-02 6.0 Technical Planning Design Documents Drafting architecture

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB