Next steps

This commit is contained in:
Flatlogic Bot 2026-01-23 20:06:08 +00:00
parent e62fa4be97
commit 55c4d7eddb
2 changed files with 224 additions and 113 deletions

View File

@ -1,41 +1,59 @@
-- Initialize company setup (companies, statuses, folders) and core user management.
-- Designed for multi-tenant applications where each company has isolated data.
-- Companies Table: Stores information about each client company.
CREATE TABLE IF NOT EXISTS companies ( CREATE TABLE IF NOT EXISTS companies (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL UNIQUE,
uprn_required BOOLEAN DEFAULT FALSE, uprn_required BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- Job Statuses Table: Stores custom job statuses defined by each company.
CREATE TABLE IF NOT EXISTS job_statuses ( CREATE TABLE IF NOT EXISTS job_statuses (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL, company_id INT NOT NULL,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
is_default BOOLEAN DEFAULT FALSE, is_default BOOLEAN DEFAULT FALSE,
sort_order INT DEFAULT 0, sort_order INT DEFAULT 0,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
UNIQUE KEY (company_id, name)
); );
-- Required Folders Table: Stores mandatory folder structures defined by each company.
CREATE TABLE IF NOT EXISTS required_folders ( CREATE TABLE IF NOT EXISTS required_folders (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL, company_id INT NOT NULL,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS clients (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
UNIQUE KEY (company_id, name)
); );
-- Users Table: Stores user accounts. Each user belongs to a specific company.
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL, company_id INT NOT NULL,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
role ENUM('admin', 'standard') DEFAULT 'standard', role ENUM('admin', 'standard') DEFAULT 'standard', -- Admin can manage company settings, standard users manage jobs.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
); );
-- Clients Table: Stores clients for each company. Clients can be added, edited, but not deleted.
CREATE TABLE IF NOT EXISTS clients (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
contact_person VARCHAR(255) DEFAULT NULL,
email VARCHAR(255) DEFAULT NULL,
phone VARCHAR(255) DEFAULT NULL,
is_active BOOLEAN DEFAULT TRUE, -- Clients can be marked inactive instead of deleted.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
UNIQUE KEY (company_id, name) -- Ensure client names are unique per company
);

295
setup.php
View File

@ -2,79 +2,132 @@
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
session_start();
$error = ''; $error = '';
$success = ''; $success = '';
$step = $_GET['step'] ?? 1;
$companyId = $_SESSION['company_id'] ?? null;
// Redirect to index if setup is complete and companyId is not in session
if ($step > 1 && !$companyId) {
header('Location: index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$companyName = $_POST['company_name'] ?? ''; if ($step == 1) {
$uprnRequired = isset($_POST['uprn_required']) ? 1 : 0; $companyName = $_POST['company_name'] ?? '';
$statuses = $_POST['statuses'] ?? []; $uprnRequired = isset($_POST['uprn_required']) ? 1 : 0;
$folders = $_POST['folders'] ?? []; $statuses = $_POST['statuses'] ?? [];
$defaultStatusIndex = (int)($_POST['default_status'] ?? 0); $folders = $_POST['folders'] ?? [];
$defaultStatusIndex = (int)($_POST['default_status'] ?? 0);
if (empty($companyName)) { if (empty($companyName)) {
$error = "Company name is required."; $error = "Company name is required.";
} elseif (empty($statuses)) { } elseif (empty(array_filter($statuses, 'trim'))) {
$error = "At least one job status is required."; $error = "At least one job status is required.";
} elseif (empty($folders)) { } elseif (empty(array_filter($folders, 'trim'))) {
$error = "At least one required folder is required."; $error = "At least one required folder is required.";
} else { } else {
try { try {
db()->beginTransaction(); db()->beginTransaction();
// 1. Create Company // 1. Create Company
$stmt = db()->prepare("INSERT INTO companies (name, uprn_required) VALUES (?, ?)"); $stmt = db()->prepare("INSERT INTO companies (name, uprn_required) VALUES (?, ?)");
$stmt->execute([$companyName, $uprnRequired]); $stmt->execute([$companyName, $uprnRequired]);
$companyId = db()->lastInsertId(); $companyId = db()->lastInsertId();
$_SESSION['company_id'] = $companyId; // Store company ID in session for next steps
// 2. Insert Statuses // 2. Insert Statuses
$stmt = db()->prepare("INSERT INTO job_statuses (company_id, name, is_default, sort_order) VALUES (?, ?, ?, ?)"); $stmt = db()->prepare("INSERT INTO job_statuses (company_id, name, is_default, sort_order) VALUES (?, ?, ?, ?)");
foreach ($statuses as $index => $statusName) { foreach ($statuses as $index => $statusName) {
if (trim($statusName) === '') continue; $statusName = trim($statusName);
$isDefault = ($index === $defaultStatusIndex) ? 1 : 0; if ($statusName === '') continue;
$stmt->execute([$companyId, $statusName, $isDefault, $index]); $isDefault = ($index === $defaultStatusIndex) ? 1 : 0;
$stmt->execute([$companyId, $statusName, $isDefault, $index]);
}
// 3. Insert Folders
$stmt = db()->prepare("INSERT INTO required_folders (company_id, name) VALUES (?, ?)");
foreach ($folders as $folderName) {
$folderName = trim($folderName);
if ($folderName === '') continue;
$stmt->execute([$companyId, $folderName]);
}
// 4. Create first Admin user (simplified for demo)
$adminEmail = 'admin@' . strtolower(str_replace(' ', '', $companyName)) . '.com';
$stmt = db()->prepare("INSERT INTO users (company_id, name, email, password, role) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$companyId, 'Admin User', $adminEmail, password_hash('password123', PASSWORD_DEFAULT), 'admin']);
db()->commit();
header('Location: setup.php?step=2'); // Redirect to next step
exit;
} catch (Exception $e) {
db()->rollBack();
$error = "Database error: " . $e->getMessage();
} }
}
} elseif ($step == 2) {
// Step 2: Client Setup
$clientNames = $_POST['client_names'] ?? [];
$contactPeople = $_POST['contact_people'] ?? [];
$clientEmails = $_POST['client_emails'] ?? [];
$clientPhones = $_POST['client_phones'] ?? [];
// 3. Insert Folders if (empty(array_filter($clientNames, 'trim'))) {
$stmt = db()->prepare("INSERT INTO required_folders (company_id, name) VALUES (?, ?)"); $error = "At least one client is required.";
foreach ($folders as $folderName) { } else {
if (trim($folderName) === '') continue; try {
$stmt->execute([$companyId, $folderName]); db()->beginTransaction();
$stmt = db()->prepare("INSERT INTO clients (company_id, name, contact_person, email, phone) VALUES (?, ?, ?, ?, ?)");
foreach ($clientNames as $index => $clientName) {
$clientName = trim($clientName);
if ($clientName === '') continue;
$contactPerson = trim($contactPeople[$index] ?? '');
$clientEmail = trim($clientEmails[$index] ?? '');
$clientPhone = trim($clientPhones[$index] ?? '');
$stmt->execute([$companyId, $clientName, $contactPerson, $clientEmail, $clientPhone]);
}
db()->commit();
session_destroy(); // Clear session after successful setup
$success = "Company setup successfully! You can now log in.";
header('Refresh: 2; URL=index.php'); // Redirect to index
exit;
} catch (Exception $e) {
db()->rollBack();
$error = "Database error: " . $e->getMessage();
} }
// 4. Create first Admin user (simplified for demo)
$stmt = db()->prepare("INSERT INTO users (company_id, name, email, password, role) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$companyId, 'Admin User', 'admin@' . strtolower(str_replace(' ', '', $companyName)) . '.com', password_hash('password123', PASSWORD_DEFAULT), 'admin']);
db()->commit();
$success = "Company setup successfully! You can now log in.";
header('Refresh: 2; URL=index.php');
} catch (Exception $e) {
db()->rollBack();
$error = "Database error: " . $e->getMessage();
} }
} }
} }
$pageTitle = "Company Onboarding";
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Company Onboarding - RepairsPro</title> <title><?= $pageTitle ?> - RepairsPro</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>"> <link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style> <style>
.setup-container { max-width: 600px; margin: 50px auto; } .setup-container { max-width: 800px; margin: 50px auto; }
.dynamic-row { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; } .dynamic-row { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; }
.client-input-group { display: flex; gap: 10px; flex-wrap: wrap; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="setup-container"> <div class="setup-container">
<div class="card p-4"> <div class="card p-4">
<h2 class="fw-bold mb-4">Company Onboarding</h2> <h2 class="fw-bold mb-4">Company Onboarding - Step <?= $step ?></h2>
<?php if ($error): ?> <?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div> <div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
@ -82,66 +135,83 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php if ($success): ?> <?php if ($success): ?>
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div> <div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
<?php else: ?> <?php else: // Display forms if not success ?>
<form method="POST" id="onboardingForm"> <?php if ($step == 1): // Step 1: Company Info, Statuses, Folders ?>
<!-- Step 1: Basic Info --> <form method="POST" id="onboardingFormStep1">
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-bold">Company Name</label> <label class="form-label fw-bold">Company Name</label>
<input type="text" name="company_name" class="form-control" placeholder="e.g. London Repairs Ltd" required> <input type="text" name="company_name" class="form-control" placeholder="e.g. London Repairs Ltd" required>
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="uprn_required" id="uprnCheck">
<label class="form-check-label fw-bold" for="uprnCheck">Require Job UPRN?</label>
</div>
<small class="text-secondary">If enabled, every job must have a unique UPRN.</small>
</div>
<hr>
<!-- Step 2: Job Statuses -->
<div class="mb-4">
<label class="form-label fw-bold">Job Statuses</label>
<p class="small text-secondary">Define the workflow stages for your jobs.</p>
<div id="status-container">
<div class="dynamic-row">
<input type="radio" name="default_status" value="0" checked title="Set as default">
<input type="text" name="statuses[]" class="form-control" value="To Be Surveyed" required>
</div> </div>
<div class="dynamic-row">
<input type="radio" name="default_status" value="1" title="Set as default">
<input type="text" name="statuses[]" class="form-control" value="Booking Required">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addRow('status-container', 'statuses[]', true)">+ Add Status</button>
</div>
<hr> <div class="mb-4">
<div class="form-check form-switch">
<!-- Step 3: Required Folders --> <input class="form-check-input" type="checkbox" name="uprn_required" id="uprnCheck">
<div class="mb-4"> <label class="form-check-label fw-bold" for="uprnCheck">Require Job UPRN?</label>
<label class="form-label fw-bold">Required Folders</label> </div>
<p class="small text-secondary">These folders will appear on every job automatically.</p> <small class="text-secondary">If enabled, every job must have a unique UPRN.</small>
<div id="folder-container">
<div class="dynamic-row">
<input type="text" name="folders[]" class="form-control" value="Photos" required>
</div> </div>
<div class="dynamic-row">
<input type="text" name="folders[]" class="form-control" value="Quote">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addRow('folder-container', 'folders[]', false)">+ Add Folder</button>
</div>
<div class="d-grid gap-2 mt-5"> <hr>
<button type="submit" class="btn btn-primary btn-lg">Complete Onboarding</button>
</div> <div class="mb-4">
</form> <label class="form-label fw-bold">Job Statuses</label>
<?php endif; ?> <p class="small text-secondary">Define the workflow stages for your jobs.</p>
<div id="status-container">
<div class="dynamic-row">
<input type="radio" name="default_status" value="0" checked title="Set as default">
<input type="text" name="statuses[]" class="form-control" value="To Be Surveyed" required>
</div>
<div class="dynamic-row">
<input type="radio" name="default_status" value="1" title="Set as default">
<input type="text" name="statuses[]" class="form-control" value="Booking Required">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addRow('status-container', 'statuses[]', true)">+ Add Status</button>
</div>
<hr>
<div class="mb-4">
<label class="form-label fw-bold">Required Folders</label>
<p class="small text-secondary">These folders will appear on every job automatically.</p>
<div id="folder-container">
<div class="dynamic-row">
<input type="text" name="folders[]" class="form-control" value="Photos" required>
</div>
<div class="dynamic-row">
<input type="text" name="folders[]" class="form-control" value="Quote">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addRow('folder-container', 'folders[]', false)">+ Add Folder</button>
</div>
<div class="d-grid gap-2 mt-5">
<button type="submit" class="btn btn-primary btn-lg">Next: Setup Clients</button>
</div>
</form>
<?php elseif ($step == 2): // Step 2: Client Setup ?>
<form method="POST" id="onboardingFormStep2">
<p class="small text-secondary">Add your initial clients. You can add more later.</p>
<div id="client-container">
<div class="dynamic-row client-input-group">
<input type="text" name="client_names[]" class="form-control flex-grow-1" placeholder="Client Name" required>
<input type="text" name="contact_people[]" class="form-control" placeholder="Contact Person">
<input type="email" name="client_emails[]" class="form-control" placeholder="Email">
<input type="text" name="client_phones[]" class="form-control" placeholder="Phone">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addClientRow()">+ Add Client</button>
<div class="d-grid gap-2 mt-5">
<button type="submit" class="btn btn-success btn-lg">Complete Onboarding</button>
</div>
</form>
<?php endif; ?>
<?php endif; // End of forms ?>
</div> </div>
</div> </div>
</div> </div>
@ -175,10 +245,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}; };
} }
// Attach remove events to existing buttons function addClientRow() {
document.querySelectorAll('.remove-btn').forEach(btn => { const container = document.getElementById('client-container');
const div = document.createElement('div');
div.className = 'dynamic-row client-input-group';
div.innerHTML = `
<input type="text" name="client_names[]" class="form-control flex-grow-1" placeholder="Client Name" required>
<input type="text" name="contact_people[]" class="form-control" placeholder="Contact Person">
<input type="email" name="client_emails[]" class="form-control" placeholder="Email">
<input type="text" name="client_phones[]" class="form-control" placeholder="Phone">
<button type="button" class="btn btn-sm btn-outline-danger remove-btn">&times;</button>
`;
container.appendChild(div);
div.querySelector('.remove-btn').onclick = function() { div.remove(); };
}
// Attach remove events to existing buttons (for statuses and folders initially)
document.querySelectorAll('#status-container .remove-btn, #folder-container .remove-btn').forEach(btn => {
btn.onclick = function() { btn.parentElement.remove(); }; btn.onclick = function() { btn.parentElement.remove(); };
}); });
// Initial client row for step 2 if no clients are pre-filled
if (document.getElementById('client-container') && document.getElementById('client-container').children.length === 0) {
// addClientRow(); // Only add if step 2 is active and no existing clients (for initial load)
}
</script> </script>
</body> </body>
</html> </html>