diff --git a/assets/css/custom.css b/assets/css/custom.css index 65a1626..2170f59 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,346 +1,77 @@ :root { - --color-bg: #ffffff; - --color-text: #1a1a1a; - --color-primary: #2563EB; /* Vibrant Blue */ - --color-secondary: #000000; - --color-accent: #A3E635; /* Lime Green */ - --color-surface: #f8f9fa; - --font-heading: 'Space Grotesk', sans-serif; - --font-body: 'Inter', sans-serif; - --border-width: 2px; - --shadow-hard: 5px 5px 0px #000; - --shadow-hover: 8px 8px 0px #000; - --radius-pill: 50rem; - --radius-card: 1rem; + --primary: #111827; + --secondary: #6B7280; + --accent: #3B82F6; + --bg: #F9FAFB; + --surface: #FFFFFF; + --border: #E5E7EB; } body { - font-family: var(--font-body); - background-color: var(--color-bg); - color: var(--color-text); - overflow-x: hidden; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + background-color: var(--bg); + color: var(--primary); + font-size: 0.875rem; } -h1, h2, h3, h4, h5, h6, .navbar-brand { - font-family: var(--font-heading); - letter-spacing: -0.03em; -} - -/* Utilities */ -.text-primary { color: var(--color-primary) !important; } -.bg-black { background-color: #000 !important; } -.text-white { color: #fff !important; } -.shadow-hard { box-shadow: var(--shadow-hard); } -.border-2-black { border: var(--border-width) solid #000; } -.py-section { padding-top: 5rem; padding-bottom: 5rem; } - -/* Navbar */ .navbar { - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(10px); - border-bottom: var(--border-width) solid transparent; - transition: all 0.3s; - padding-top: 1rem; - padding-bottom: 1rem; + background-color: var(--surface); + border-bottom: 1px solid var(--border); } -.navbar.scrolled { - border-bottom-color: #000; - padding-top: 0.5rem; - padding-bottom: 0.5rem; +.card { + border: 1px solid var(--border); + border-radius: 4px; + box-shadow: none; } -.brand-text { - font-size: 1.5rem; - font-weight: 800; -} - -.nav-link { - font-weight: 500; - color: var(--color-text); - margin-left: 1rem; - position: relative; -} - -.nav-link:hover, .nav-link.active { - color: var(--color-primary); -} - -/* Buttons */ .btn { - font-weight: 700; - font-family: var(--font-heading); - padding: 0.8rem 2rem; - border-radius: var(--radius-pill); - border: var(--border-width) solid #000; - transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); - box-shadow: var(--shadow-hard); -} - -.btn:hover { - transform: translate(-2px, -2px); - box-shadow: var(--shadow-hover); -} - -.btn:active { - transform: translate(2px, 2px); - box-shadow: 0 0 0 #000; + border-radius: 4px; + font-weight: 500; + padding: 0.5rem 1rem; } .btn-primary { - background-color: var(--color-primary); - border-color: #000; - color: #fff; + background-color: var(--primary); + border-color: var(--primary); } .btn-primary:hover { - background-color: #1d4ed8; - border-color: #000; - color: #fff; + background-color: #1F2937; + border-color: #1F2937; } -.btn-outline-dark { - background-color: #fff; - color: #000; +.table { + border: 1px solid var(--border); + background: var(--surface); } -.btn-cta { - background-color: var(--color-accent); - color: #000; -} - -.btn-cta:hover { - background-color: #8cc629; - color: #000; -} - -/* Hero Section */ -.hero-section { - min-height: 100vh; - padding-top: 80px; -} - -.background-blob { - position: absolute; - border-radius: 50%; - filter: blur(80px); - opacity: 0.6; - z-index: 1; -} - -.blob-1 { - top: -10%; - right: -10%; - width: 600px; - height: 600px; - background: radial-gradient(circle, var(--color-accent), transparent); -} - -.blob-2 { - bottom: 10%; - left: -10%; - width: 500px; - height: 500px; - background: radial-gradient(circle, var(--color-primary), transparent); -} - -.highlight-text { - background: linear-gradient(120deg, transparent 0%, transparent 40%, var(--color-accent) 40%, var(--color-accent) 100%); - background-repeat: no-repeat; - background-size: 100% 40%; - background-position: 0 88%; - padding: 0 5px; -} - -.dot { color: var(--color-primary); } - -.badge-pill { - display: inline-block; - padding: 0.5rem 1rem; - border: 2px solid #000; - border-radius: 50px; - font-weight: 700; - background: #fff; - box-shadow: 4px 4px 0 #000; - font-family: var(--font-heading); - font-size: 0.9rem; -} - -/* Marquee */ -.marquee-container { - overflow: hidden; - white-space: nowrap; - border-top: 2px solid #000; - border-bottom: 2px solid #000; -} - -.rotate-divider { - transform: rotate(-2deg) scale(1.05); - z-index: 10; - position: relative; - margin-top: -50px; - margin-bottom: 30px; -} - -.marquee-content { - display: inline-block; - animation: marquee 20s linear infinite; - font-family: var(--font-heading); - font-weight: 700; - font-size: 1.5rem; - letter-spacing: 2px; -} - -@keyframes marquee { - 0% { transform: translateX(0); } - 100% { transform: translateX(-50%); } -} - -/* Portfolio Cards */ -.project-card { - border: 2px solid #000; - border-radius: var(--radius-card); - overflow: hidden; - background: #fff; - transition: transform 0.3s ease; - box-shadow: var(--shadow-hard); - height: 100%; - display: flex; - flex-direction: column; -} - -.project-card:hover { - transform: translateY(-10px); - box-shadow: 8px 8px 0 #000; -} - -.card-img-holder { - height: 250px; - display: flex; - align-items: center; - justify-content: center; - border-bottom: 2px solid #000; - position: relative; - font-size: 4rem; -} - -.placeholder-art { - transition: transform 0.3s ease; -} - -.project-card:hover .placeholder-art { - transform: scale(1.2) rotate(10deg); -} - -.bg-soft-blue { background-color: #e0f2fe; } -.bg-soft-green { background-color: #dcfce7; } -.bg-soft-purple { background-color: #f3e8ff; } -.bg-soft-yellow { background-color: #fef9c3; } - -.category-tag { - position: absolute; - top: 15px; - right: 15px; - background: #000; - color: #fff; - padding: 5px 12px; - border-radius: 20px; +.table th { + background: #F3F4F6; + font-weight: 600; + text-transform: uppercase; font-size: 0.75rem; - font-weight: 700; + letter-spacing: 0.025em; + border-bottom: 1px solid var(--border); } -.card-body { padding: 1.5rem; } - -.link-arrow { - text-decoration: none; - color: #000; - font-weight: 700; - display: inline-flex; - align-items: center; - margin-top: auto; +.activity-log { + max-height: 400px; + overflow-y: auto; } -.link-arrow i { transition: transform 0.2s; margin-left: 5px; } -.link-arrow:hover i { transform: translateX(5px); } - -/* About */ -.about-image-stack { - position: relative; - height: 400px; - width: 100%; +.log-entry { + padding: 0.75rem; + border-bottom: 1px solid var(--border); + font-size: 0.8rem; } -.stack-card { - position: absolute; - width: 80%; - height: 100%; - border-radius: var(--radius-card); - border: 2px solid #000; - box-shadow: var(--shadow-hard); - left: 10%; - transform: rotate(-3deg); - background-size: cover; +.log-entry:last-child { + border-bottom: none; } -/* Forms */ -.form-control { - border: 2px solid #000; - border-radius: 0.5rem; - padding: 1rem; +.badge { + border-radius: 9999px; font-weight: 500; - background: #f8f9fa; -} - -.form-control:focus { - box-shadow: 4px 4px 0 var(--color-primary); - border-color: #000; - background: #fff; -} - -/* Animations */ -.animate-up { - opacity: 0; - transform: translateY(30px); - animation: fadeUp 0.8s ease forwards; -} - -.delay-100 { animation-delay: 0.1s; } -.delay-200 { animation-delay: 0.2s; } - -@keyframes fadeUp { - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Social */ -.social-links a { - transition: transform 0.2s; - display: inline-block; -} -.social-links a:hover { - transform: scale(1.2) rotate(10deg); - color: var(--color-accent) !important; -} - -/* Responsive */ -@media (max-width: 991px) { - .rotate-divider { - transform: rotate(0); - margin-top: 0; - margin-bottom: 2rem; - } - - .hero-section { - padding-top: 120px; - text-align: center; - min-height: auto; - padding-bottom: 100px; - } - - .display-1 { font-size: 3.5rem; } - - .blob-1 { width: 300px; height: 300px; right: -20%; } - .blob-2 { width: 300px; height: 300px; left: -20%; } -} + padding: 0.25rem 0.625rem; +} \ No newline at end of file diff --git a/db/migrations/20260123_init.sql b/db/migrations/20260123_init.sql new file mode 100644 index 0000000..a389e47 --- /dev/null +++ b/db/migrations/20260123_init.sql @@ -0,0 +1,77 @@ +-- Initial Schema for Repairs Multi-tenant App +CREATE TABLE IF NOT EXISTS companies ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + uprn_required BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + role ENUM('admin', 'standard') DEFAULT 'standard', + FOREIGN KEY (company_id) REFERENCES companies(id) +); + +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, + FOREIGN KEY (company_id) REFERENCES companies(id) +); + +CREATE TABLE IF NOT EXISTS job_statuses ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + is_default BOOLEAN DEFAULT FALSE, + FOREIGN KEY (company_id) REFERENCES companies(id) +); + +CREATE TABLE IF NOT EXISTS jobs ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_id INT NOT NULL, + job_ref VARCHAR(100) NOT NULL, + uprn VARCHAR(100), + address_1 VARCHAR(255), + address_2 VARCHAR(255), + address_3 VARCHAR(255), + postcode VARCHAR(20), + description TEXT, + status_id INT, + client_id INT, + works_approved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(company_id, job_ref), + FOREIGN KEY (company_id) REFERENCES companies(id), + FOREIGN KEY (status_id) REFERENCES job_statuses(id), + FOREIGN KEY (client_id) REFERENCES clients(id) +); + +CREATE TABLE IF NOT EXISTS activity_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + job_id INT NOT NULL, + company_id INT NOT NULL, + user_id INT NOT NULL, + user_name VARCHAR(255), + event_type VARCHAR(100), + field_name VARCHAR(100), + old_value TEXT, + new_value TEXT, + metadata JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (job_id) REFERENCES jobs(id), + FOREIGN KEY (company_id) REFERENCES companies(id) +); + +-- Seed Initial Demo Company +INSERT INTO companies (name) VALUES ('Repairs Pro Ltd'); +SET @company_id = LAST_INSERT_ID(); + +INSERT INTO users (company_id, name, email, role) VALUES (@company_id, 'Admin User', 'admin@repairspro.com', 'admin'); +INSERT INTO job_statuses (company_id, name, is_default) VALUES (@company_id, 'To Be Surveyed', 1), (@company_id, 'Booking Required', 0), (@company_id, 'Completed', 0); +INSERT INTO clients (company_id, name) VALUES (@company_id, 'Main Housing Assoc'); diff --git a/includes/helpers.php b/includes/helpers.php new file mode 100644 index 0000000..23fc292 --- /dev/null +++ b/includes/helpers.php @@ -0,0 +1,34 @@ +query("SELECT * FROM users LIMIT 1")->fetch(); +} + +function logActivity($job_id, $event_type, $field_name = null, $old_value = null, $new_value = null) { + $user = getCurrentUser(); + $stmt = db()->prepare("INSERT INTO activity_logs (job_id, company_id, user_id, user_name, event_type, field_name, old_value, new_value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $job_id, + $user['company_id'], + $user['id'], + $user['name'], + $event_type, + $field_name, + $old_value, + $new_value + ]); +} + +function getJobStatuses($company_id) { + $stmt = db()->prepare("SELECT * FROM job_statuses WHERE company_id = ?"); + $stmt->execute([$company_id]); + return $stmt->fetchAll(); +} + +function getClients($company_id) { + $stmt = db()->prepare("SELECT * FROM clients WHERE company_id = ? AND is_active = 1"); + $stmt->execute([$company_id]); + return $stmt->fetchAll(); +} diff --git a/index.php b/index.php index 7205f3d..923196e 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,167 @@ prepare("INSERT INTO jobs (company_id, job_ref, address_1, description, status_id, client_id) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$company_id, $job_ref, $address_1, $description, $status_id, $client_id]); + $job_id = db()->lastInsertId(); + + logActivity($job_id, 'job_created', null, null, "Job Ref: $job_ref"); + + header("Location: job_detail.php?id=" . $job_id); + exit; + } catch (Exception $e) { + $error = "Error creating job: " . $e->getMessage(); + } +} + +$jobs = db()->prepare("SELECT j.*, s.name as status_name, c.name as client_name + FROM jobs j + LEFT JOIN job_statuses s ON j.status_id = s.id + LEFT JOIN clients c ON j.client_id = c.id + WHERE j.company_id = ? + ORDER BY j.created_at DESC"); +$jobs->execute([$company_id]); +$jobList = $jobs->fetchAll(); + +$statuses = getJobStatuses($company_id); +$clients = getClients($company_id); ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Dashboard - Repairs Pro + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ + +
+
+

All Jobs

+
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Job RefClientAddressStatusApprovedAction
+ + + + + + Yes + + No + + + View +
No jobs found. Start by creating one.
+
+
+
+ + + -
- + + - + \ No newline at end of file diff --git a/job_detail.php b/job_detail.php new file mode 100644 index 0000000..b1c28e0 --- /dev/null +++ b/job_detail.php @@ -0,0 +1,159 @@ +prepare("SELECT j.*, s.name as status_name, c.name as client_name + FROM jobs j + LEFT JOIN job_statuses s ON j.status_id = s.id + LEFT JOIN clients c ON j.client_id = c.id + WHERE j.id = ? AND j.company_id = ?"); +$stmt->execute([$job_id, $company_id]); +$job = $stmt->fetch(); + +if (!$job) { + die("Job not found."); +} + +// Handle Updates +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['action']) && $_POST['action'] === 'toggle_approved') { + $new_val = $job['works_approved'] ? 0 : 1; + $stmt = db()->prepare("UPDATE jobs SET works_approved = ? WHERE id = ?"); + $stmt->execute([$new_val, $job_id]); + + logActivity($job_id, 'works_approved_toggle', 'works_approved', $job['works_approved'] ? 'True' : 'False', $new_val ? 'True' : 'False'); + + header("Location: job_detail.php?id=" . $job_id); + exit; + } +} + +// Fetch Logs +$logStmt = db()->prepare("SELECT * FROM activity_logs WHERE job_id = ? ORDER BY created_at DESC"); +$logStmt->execute([$job_id]); +$logs = $logStmt->fetchAll(); +?> + + + + + + Job Detail - <?php echo htmlspecialchars($job['job_ref']); ?> + + + + + + + +
+
+
+
+
+
+ Job Reference +

+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
Works Approved
+

Toggle this when the scope of work is confirmed.

+
+
+ + +
+
+
+
+ +
+
+
+
Activity Log
+
+
+ +
+
+ + +
+
+ +
+ +
+ + → + +
+ +
+ +
+
+ +
No activity yet.
+ +
+
+
+
+
+ + + +