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

127
setup.php
View File

@ -2,10 +2,21 @@
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') {
if ($step == 1) {
$companyName = $_POST['company_name'] ?? ''; $companyName = $_POST['company_name'] ?? '';
$uprnRequired = isset($_POST['uprn_required']) ? 1 : 0; $uprnRequired = isset($_POST['uprn_required']) ? 1 : 0;
$statuses = $_POST['statuses'] ?? []; $statuses = $_POST['statuses'] ?? [];
@ -14,9 +25,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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 {
@ -26,11 +37,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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);
if ($statusName === '') continue;
$isDefault = ($index === $defaultStatusIndex) ? 1 : 0; $isDefault = ($index === $defaultStatusIndex) ? 1 : 0;
$stmt->execute([$companyId, $statusName, $isDefault, $index]); $stmt->execute([$companyId, $statusName, $isDefault, $index]);
} }
@ -38,43 +51,83 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 3. Insert Folders // 3. Insert Folders
$stmt = db()->prepare("INSERT INTO required_folders (company_id, name) VALUES (?, ?)"); $stmt = db()->prepare("INSERT INTO required_folders (company_id, name) VALUES (?, ?)");
foreach ($folders as $folderName) { foreach ($folders as $folderName) {
if (trim($folderName) === '') continue; $folderName = trim($folderName);
if ($folderName === '') continue;
$stmt->execute([$companyId, $folderName]); $stmt->execute([$companyId, $folderName]);
} }
// 4. Create first Admin user (simplified for demo) // 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 = 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']); $stmt->execute([$companyId, 'Admin User', $adminEmail, password_hash('password123', PASSWORD_DEFAULT), 'admin']);
db()->commit(); 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'] ?? [];
if (empty(array_filter($clientNames, 'trim'))) {
$error = "At least one client is required.";
} else {
try {
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."; $success = "Company setup successfully! You can now log in.";
header('Refresh: 2; URL=index.php'); header('Refresh: 2; URL=index.php'); // Redirect to index
exit;
} catch (Exception $e) { } catch (Exception $e) {
db()->rollBack(); db()->rollBack();
$error = "Database error: " . $e->getMessage(); $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,10 +135,10 @@ 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>
@ -101,7 +154,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<hr> <hr>
<!-- Step 2: Job Statuses -->
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-bold">Job Statuses</label> <label class="form-label fw-bold">Job Statuses</label>
<p class="small text-secondary">Define the workflow stages for your jobs.</p> <p class="small text-secondary">Define the workflow stages for your jobs.</p>
@ -121,7 +173,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<hr> <hr>
<!-- Step 3: Required Folders -->
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-bold">Required Folders</label> <label class="form-label fw-bold">Required Folders</label>
<p class="small text-secondary">These folders will appear on every job automatically.</p> <p class="small text-secondary">These folders will appear on every job automatically.</p>
@ -138,10 +189,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div> </div>
<div class="d-grid gap-2 mt-5"> <div class="d-grid gap-2 mt-5">
<button type="submit" class="btn btn-primary btn-lg">Complete Onboarding</button> <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> </div>
</form> </form>
<?php endif; ?> <?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>