From 41b182bfba9c52fbb33a64d519c04ac6ae9aa81a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 7 Mar 2026 13:28:03 +0000 Subject: [PATCH] Autosave: 20260307-132803 --- admin_dashboard.php | 113 +++++++ admin_manage_locations.php | 177 ++++++++++ assets/css/custom.css | 549 +++++++++++-------------------- assets/js/main.js | 44 +-- db/migrations/initial_schema.php | 33 ++ includes/app.php | 222 +++++++++++++ includes/layout.php | 108 ++++++ index.php | 274 ++++++++------- register.php | 200 +++++++++++ shipment_detail.php | 134 ++++++++ shipper_dashboard.php | 164 +++++++++ truck_owner_dashboard.php | 110 +++++++ 12 files changed, 1586 insertions(+), 542 deletions(-) create mode 100644 admin_dashboard.php create mode 100644 admin_manage_locations.php create mode 100644 db/migrations/initial_schema.php create mode 100644 includes/app.php create mode 100644 includes/layout.php create mode 100644 register.php create mode 100644 shipment_detail.php create mode 100644 shipper_dashboard.php create mode 100644 truck_owner_dashboard.php diff --git a/admin_dashboard.php b/admin_dashboard.php new file mode 100644 index 0000000..cc9a9a1 --- /dev/null +++ b/admin_dashboard.php @@ -0,0 +1,113 @@ +prepare("UPDATE shipments SET status = :status WHERE id = :id"); + $stmt->execute([':status' => $status, ':id' => $shipmentId]); + set_flash('success', t('success_status')); + header('Location: ' . url_with_lang('admin_dashboard.php')); + exit; + } +} + +$shipments = []; +try { + $stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 30"); + $shipments = $stmt->fetchAll(); +} catch (Throwable $e) { + $shipments = []; +} + +$flash = get_flash(); + +render_header(t('admin_dashboard'), 'admin'); +?> + +
+
+ +
+
+
+

+

Control shipment status, manage locations, and onboard new users from one place.

+
+
+

Quick actions

+ +
+
+
+

+ total +
+ +
+ + +
+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+
+ +
+
+
+ + diff --git a/admin_manage_locations.php b/admin_manage_locations.php new file mode 100644 index 0000000..b6b9354 --- /dev/null +++ b/admin_manage_locations.php @@ -0,0 +1,177 @@ +exec(" +CREATE TABLE IF NOT EXISTS countries ( + id INT AUTO_INCREMENT PRIMARY KEY, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +"); + +db()->exec(" +CREATE TABLE IF NOT EXISTS cities ( + id INT AUTO_INCREMENT PRIMARY KEY, + country_id INT NOT NULL, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) DEFAULT NULL, + UNIQUE KEY uniq_city_country (country_id, name_en), + CONSTRAINT fk_cities_country FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +"); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_POST['add_country'])) { + $countryNameEn = trim($_POST['country_name_en'] ?? ''); + $countryNameAr = trim($_POST['country_name_ar'] ?? ''); + if ($countryNameEn === '') { + $errors[] = 'Country name (English) is required.'; + } else { + try { + $stmt = db()->prepare("INSERT INTO countries (name_en, name_ar) VALUES (?, ?)"); + $stmt->execute([$countryNameEn, $countryNameAr !== '' ? $countryNameAr : null]); + $flash = 'Country added.'; + } catch (Throwable $e) { + $errors[] = 'Country already exists or could not be saved.'; + } + } + } elseif (isset($_POST['add_city'])) { + $countryId = (int)($_POST['country_id'] ?? 0); + $cityNameEn = trim($_POST['city_name_en'] ?? ''); + $cityNameAr = trim($_POST['city_name_ar'] ?? ''); + if ($countryId <= 0 || $cityNameEn === '') { + $errors[] = 'Please select a country and provide city name (English).'; + } else { + try { + $stmt = db()->prepare("INSERT INTO cities (country_id, name_en, name_ar) VALUES (?, ?, ?)"); + $stmt->execute([$countryId, $cityNameEn, $cityNameAr !== '' ? $cityNameAr : null]); + $flash = 'City added.'; + } catch (Throwable $e) { + $errors[] = 'City already exists or could not be saved.'; + } + } + } +} + +$countryNameExpr = $lang === 'ar' + ? "COALESCE(NULLIF(co.name_ar, ''), co.name_en)" + : "COALESCE(NULLIF(co.name_en, ''), co.name_ar)"; +$countryNameExprNoAlias = $lang === 'ar' + ? "COALESCE(NULLIF(name_ar, ''), name_en)" + : "COALESCE(NULLIF(name_en, ''), name_ar)"; +$cityNameExpr = $lang === 'ar' + ? "COALESCE(NULLIF(c.name_ar, ''), c.name_en)" + : "COALESCE(NULLIF(c.name_en, ''), c.name_ar)"; + +$countries = db()->query("SELECT id, {$countryNameExprNoAlias} AS display_name FROM countries ORDER BY display_name ASC")->fetchAll(); +$cities = db()->query( + "SELECT + {$countryNameExpr} AS country_name, + {$cityNameExpr} AS city_name + FROM cities c + JOIN countries co ON co.id = c.country_id + ORDER BY country_name ASC, city_name ASC + LIMIT 30" +)->fetchAll(); + +render_header('Manage Locations', 'admin'); +?> + +
+
+ +
+
+
+

Country & city setup

+

Define allowed origin and destination options for shipments.

+
+ + +
+ + +
+ + +
+
+
+

Add country

+
+
+ + +
+
+ + +
+ +
+
+
+
+
+

Add city

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+

Recently added cities

+ Back to admin +
+ +

No cities added yet.

+ +
+ + + + + + + + + + + + + + + +
CountryCity
+
+ +
+
+
+ + diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..fe6168d 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,230 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; +:root { + --bg: #f8fafc; + --surface: #ffffff; + --text: #0f172a; + --muted: #64748b; + --border: #e2e8f0; + --primary: #0f172a; + --accent: #2563eb; + --success: #16a34a; + --warning: #f59e0b; + --shadow: 0 10px 30px rgba(15, 23, 42, 0.06); } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; +body.app-body { + background: + radial-gradient(circle at 5% 5%, rgba(37, 99, 235, 0.08), transparent 28%), + radial-gradient(circle at 95% 10%, rgba(14, 165, 233, 0.08), transparent 25%), + var(--bg); + color: var(--text); + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; +} + +.navbar { + backdrop-filter: blur(6px); + background: rgba(255, 255, 255, 0.92) !important; +} + +.navbar-brand { + letter-spacing: 0.02em; +} + +.card, +.panel { + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + box-shadow: var(--shadow); +} + +.hero-card { + border-radius: 18px; + padding: 32px; + background: linear-gradient(135deg, #ffffff 0%, #f8fbff 65%, #eef4ff 100%); + border: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.stat-card { padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5); } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; +.badge-status, +.badge { + display: inline-flex; align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; + gap: 6px; + font-size: 12px; + padding: 6px 11px; + border-radius: 999px; font-weight: 600; - transition: all 0.3s ease; + border: 1px solid transparent; } -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); +.badge-status.posted, +.badge.posted { + background: #e2e8f0; + color: #334155; + border-color: #cbd5e1; } -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; +.badge-status.offered, +.badge.offered { + background: #dbeafe; + color: #1d4ed8; + border-color: #bfdbfe; } -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); +.badge-status.confirmed, +.badge.confirmed { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; } -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); +.badge-status.in_transit, +.badge.in_transit { + background: #fef3c7; + color: #b45309; + border-color: #fde68a; } -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; +.badge-status.delivered, +.badge.delivered { + background: #ede9fe; + color: #6d28d9; + border-color: #ddd6fe; } -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; +.table thead th { + font-size: 12px; text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; + letter-spacing: 0.04em; + color: var(--muted); + border-bottom: 1px solid var(--border); } -.table td { - background: #fff; - padding: 1rem; - border: none; +.table tbody td { + vertical-align: middle; } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +.table tbody tr:hover { + background: #f8fafc; } -.form-group label { +.form-control, +.form-select { + border-radius: 8px; + border: 1px solid var(--border); + padding: 10px 12px; +} + +.form-control:focus, +.form-select:focus { + border-color: #93c5fd; + box-shadow: 0 0 0 0.2rem rgba(37, 99, 235, 0.12); +} + +.form-label { + font-weight: 600; + color: #334155; + margin-bottom: 6px; +} + +.btn-primary { + background: var(--primary); + border-color: var(--primary); +} + +.btn-primary:hover, +.btn-primary:focus { + background: #111827; + border-color: #111827; +} + +.btn { + border-radius: 10px; + font-weight: 600; + padding: 9px 14px; +} + +.btn-outline-dark { + border-color: var(--border); +} + +.section-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 12px; +} + +.muted { + color: var(--muted); +} + +.alert { + border-radius: 8px; +} + +.page-intro { + margin-bottom: 18px; +} + +.table-responsive { + border-radius: 12px; +} + +.admin-sidebar { + position: sticky; + top: 88px; +} + +.admin-nav-link { display: block; - margin-bottom: 0.5rem; + padding: 10px 12px; + border-radius: 10px; + color: #334155; + text-decoration: none; font-weight: 600; - font-size: 0.9rem; + border: 1px solid transparent; } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.admin-nav-link:hover { + background: #eff6ff; + border-color: #dbeafe; + color: #1d4ed8; } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +.admin-nav-link.active { + background: #dbeafe; + border-color: #bfdbfe; + color: #1d4ed8; } -.header-container { - display: flex; - justify-content: space-between; - align-items: center; +[dir="rtl"] .navbar .ms-auto { + margin-left: 0 !important; + margin-right: auto !important; } -.header-links { - display: flex; - gap: 1rem; +[dir="rtl"] .text-end { + text-align: left !important; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +[dir="rtl"] .text-start { + text-align: right !important; } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} +@media (max-width: 991px) { + .hero-card { + padding: 24px; + } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; + .admin-sidebar { + position: static; + } } - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..a862671 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,11 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) + const autoAlerts = document.querySelectorAll('[data-auto-dismiss="true"]'); + if (autoAlerts.length) { + setTimeout(() => { + autoAlerts.forEach((alert) => { + alert.classList.add('fade'); + setTimeout(() => alert.remove(), 500); }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } - }); + }, 4500); + } }); diff --git a/db/migrations/initial_schema.php b/db/migrations/initial_schema.php new file mode 100644 index 0000000..2d4c91d --- /dev/null +++ b/db/migrations/initial_schema.php @@ -0,0 +1,33 @@ +exec(" + CREATE TABLE IF NOT EXISTS countries ( + id INT AUTO_INCREMENT PRIMARY KEY, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS cities ( + id INT AUTO_INCREMENT PRIMARY KEY, + country_id INT NOT NULL, + name_en VARCHAR(255) NOT NULL, + name_ar VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (country_id) REFERENCES countries(id) + ); + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + full_name VARCHAR(255) NOT NULL, + role ENUM('admin', 'shipper', 'truck_owner') NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + "); + echo "Schema updated successfully."; +} catch (PDOException $e) { + echo "Error: " . $e->getMessage(); +} diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..fdb703c --- /dev/null +++ b/includes/app.php @@ -0,0 +1,222 @@ + [ + 'app_name' => 'CargoLink', + 'nav_home' => 'Overview', + 'nav_shipper' => 'Shipper Desk', + 'nav_owner' => 'Truck Owner Desk', + 'nav_admin' => 'Admin Panel', + 'hero_title' => 'Move cargo faster with verified trucks.', + 'hero_subtitle' => 'Post shipments, collect offers, and pay via Thawani or bank transfer. Built for local and nearby cross-border moves.', + 'hero_tagline' => 'Multilingual Logistics Marketplace', + 'cta_shipper' => 'Post a shipment', + 'cta_owner' => 'Find loads', + 'cta_admin' => 'Open admin', + 'stats_shipments' => 'Shipments posted', + 'stats_offers' => 'Active offers', + 'stats_confirmed' => 'Confirmed trips', + 'section_workflow' => 'How it works', + 'recent_shipments' => 'Recent shipments', + 'step_post' => 'Shipper posts cargo details and preferred payment.', + 'step_offer' => 'Truck owners respond with their best rate.', + 'step_confirm' => 'Admin confirms booking and status.', + 'shipper_dashboard' => 'Shipper Dashboard', + 'new_shipment' => 'Create shipment', + 'shipper_name' => 'Shipper name', + 'shipper_company' => 'Company', + 'origin' => 'Origin city', + 'destination' => 'Destination city', + 'cargo' => 'Cargo description', + 'weight' => 'Weight (tons)', + 'pickup_date' => 'Pickup date', + 'delivery_date' => 'Delivery date', + 'payment_method' => 'Payment method', + 'payment_thawani' => 'Thawani online payment', + 'payment_bank' => 'Bank transfer', + 'submit_shipment' => 'Submit shipment', + 'shipments_list' => 'Your latest shipments', + 'status' => 'Status', + 'offer' => 'Best offer', + 'actions' => 'Actions', + 'view' => 'View', + 'owner_dashboard' => 'Truck Owner Dashboard', + 'available_shipments' => 'Available shipments', + 'offer_price' => 'Offer price', + 'offer_owner' => 'Truck owner name', + 'submit_offer' => 'Send offer', + 'admin_dashboard' => 'Admin Dashboard', + 'update_status' => 'Update status', + 'save' => 'Save', + 'shipment_detail' => 'Shipment detail', + 'created_at' => 'Created', + 'best_offer' => 'Best offer', + 'assign_owner' => 'Assigned owner', + 'no_shipments' => 'No shipments yet. Create the first one to get started.', + 'no_offers' => 'No offers yet.', + 'success_shipment' => 'Shipment posted successfully.', + 'success_offer' => 'Offer submitted to the shipper.', + 'success_status' => 'Status updated.', + 'error_required' => 'Please fill in all required fields.', + 'error_invalid' => 'Please enter valid values.', + 'status_posted' => 'Posted', + 'status_offered' => 'Offered', + 'status_confirmed' => 'Confirmed', + 'status_in_transit' => 'In transit', + 'status_delivered' => 'Delivered', + 'footer_note' => 'This is the initial MVP slice. Payments are not yet connected.', + ], + 'ar' => [ + 'app_name' => 'CargoLink', + 'nav_home' => 'نظرة عامة', + 'nav_shipper' => 'لوحة الشاحن', + 'nav_owner' => 'لوحة مالك الشاحنة', + 'nav_admin' => 'لوحة الإدارة', + 'hero_title' => 'انقل شحنتك بسرعة مع شاحنات موثوقة.', + 'hero_subtitle' => 'أنشئ شحنة، استلم عروضاً، وادفع عبر ثواني أو التحويل البنكي.', + 'hero_tagline' => 'منصة لوجستية متعددة اللغات', + 'cta_shipper' => 'إنشاء شحنة', + 'cta_owner' => 'البحث عن الشحنات', + 'cta_admin' => 'الدخول للإدارة', + 'stats_shipments' => 'الشحنات المنشورة', + 'stats_offers' => 'العروض الحالية', + 'stats_confirmed' => 'الرحلات المؤكدة', + 'section_workflow' => 'طريقة العمل', + 'recent_shipments' => 'أحدث الشحنات', + 'step_post' => 'يقوم الشاحن بإدخال تفاصيل الشحنة وطريقة الدفع.', + 'step_offer' => 'يرسل أصحاب الشاحنات أفضل عروضهم.', + 'step_confirm' => 'تؤكد الإدارة الحجز وتحدث الحالة.', + 'shipper_dashboard' => 'لوحة الشاحن', + 'new_shipment' => 'إنشاء شحنة', + 'shipper_name' => 'اسم الشاحن', + 'shipper_company' => 'الشركة', + 'origin' => 'مدينة الانطلاق', + 'destination' => 'مدينة الوصول', + 'cargo' => 'وصف الحمولة', + 'weight' => 'الوزن (طن)', + 'pickup_date' => 'تاريخ الاستلام', + 'delivery_date' => 'تاريخ التسليم', + 'payment_method' => 'طريقة الدفع', + 'payment_thawani' => 'الدفع الإلكتروني عبر ثواني', + 'payment_bank' => 'تحويل بنكي', + 'submit_shipment' => 'إرسال الشحنة', + 'shipments_list' => 'أحدث الشحنات', + 'status' => 'الحالة', + 'offer' => 'أفضل عرض', + 'actions' => 'إجراءات', + 'view' => 'عرض', + 'owner_dashboard' => 'لوحة مالك الشاحنة', + 'available_shipments' => 'الشحنات المتاحة', + 'offer_price' => 'سعر العرض', + 'offer_owner' => 'اسم مالك الشاحنة', + 'submit_offer' => 'إرسال العرض', + 'admin_dashboard' => 'لوحة الإدارة', + 'update_status' => 'تحديث الحالة', + 'save' => 'حفظ', + 'shipment_detail' => 'تفاصيل الشحنة', + 'created_at' => 'تم الإنشاء', + 'best_offer' => 'أفضل عرض', + 'assign_owner' => 'المالك المعتمد', + 'no_shipments' => 'لا توجد شحنات بعد. ابدأ بإنشاء أول شحنة.', + 'no_offers' => 'لا توجد عروض بعد.', + 'success_shipment' => 'تم نشر الشحنة بنجاح.', + 'success_offer' => 'تم إرسال العرض إلى الشاحن.', + 'success_status' => 'تم تحديث الحالة.', + 'error_required' => 'يرجى تعبئة جميع الحقول المطلوبة.', + 'error_invalid' => 'يرجى إدخال قيم صحيحة.', + 'status_posted' => 'منشورة', + 'status_offered' => 'بعرض', + 'status_confirmed' => 'مؤكدة', + 'status_in_transit' => 'قيد النقل', + 'status_delivered' => 'تم التسليم', + 'footer_note' => 'هذه هي النسخة الأولية. الدفع غير متصل بعد.', + ], +]; + +function t(string $key): string +{ + global $translations, $lang; + return $translations[$lang][$key] ?? $key; +} + +function e($value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function ensure_schema(): void +{ + $sql = <<exec($sql); +} + +function set_flash(string $type, string $message): void +{ + $_SESSION['flash'] = ['type' => $type, 'message' => $message]; +} + +function get_flash(): ?array +{ + if (!empty($_SESSION['flash'])) { + $flash = $_SESSION['flash']; + unset($_SESSION['flash']); + return $flash; + } + return null; +} + +function url_with_lang(string $path, array $params = []): string +{ + global $lang; + $params = array_merge(['lang' => $lang], $params); + return $path . '?' . http_build_query($params); +} + +function current_url_with_lang(string $newLang): string +{ + $params = $_GET; + $params['lang'] = $newLang; + $path = basename($_SERVER['PHP_SELF'] ?? 'index.php'); + return $path . '?' . http_build_query($params); +} + +function status_label(string $status): string +{ + $map = [ + 'posted' => t('status_posted'), + 'offered' => t('status_offered'), + 'confirmed' => t('status_confirmed'), + 'in_transit' => t('status_in_transit'), + 'delivered' => t('status_delivered'), + ]; + return $map[$status] ?? $status; +} diff --git a/includes/layout.php b/includes/layout.php new file mode 100644 index 0000000..433c551 --- /dev/null +++ b/includes/layout.php @@ -0,0 +1,108 @@ + + + + + + + <?= e($title) ?> + + + + + + + + + + + + + + +
+ +
+
+
+ + UTC +
+
+ + + + + ['label' => 'Dashboard', 'href' => url_with_lang('admin_dashboard.php')], + 'locations' => ['label' => 'Locations', 'href' => url_with_lang('admin_manage_locations.php')], + 'register' => ['label' => 'User Registration', 'href' => url_with_lang('register.php')], + ]; + ?> + + 0, + 'offers' => 0, + 'confirmed' => 0, +]; + +try { + $pdo = db(); + $stats['shipments'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments")->fetchColumn(); + $stats['offers'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments WHERE offer_price IS NOT NULL")->fetchColumn(); + $stats['confirmed'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments WHERE status IN ('confirmed','in_transit','delivered')")->fetchColumn(); +} catch (Throwable $e) { + // Keep the landing page stable even if DB is unavailable. +} + +$recentShipments = []; +try { + $stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 5"); + $recentShipments = $stmt->fetchAll(); +} catch (Throwable $e) { + $recentShipments = []; +} + +render_header(t('app_name'), 'home'); ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

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

+ +
+
+
+

+

+

+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- Page updated: (UTC) -
- - + + +
+

+
+
+
+
1
+

+
+
+
+
+
2
+

+
+
+
+
+
3
+

+
+
+
+
+ +
+
+

+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+ + diff --git a/register.php b/register.php new file mode 100644 index 0000000..d9b086a --- /dev/null +++ b/register.php @@ -0,0 +1,200 @@ +prepare("INSERT INTO users (email, password, role) VALUES (?, ?, ?)"); + $stmt->execute([$email, $password, $role]); + $userId = (int)db()->lastInsertId(); + + if ($role === 'truck_owner') { + $truckType = trim($_POST['truck_type'] ?? ''); + $loadCapacity = trim($_POST['load_capacity'] ?? ''); + $plateNo = trim($_POST['plate_no'] ?? ''); + + if ($truckType === '' || $loadCapacity === '' || $plateNo === '') { + $errors[] = 'Please complete truck details.'; + } elseif (!is_numeric($loadCapacity)) { + $errors[] = 'Load capacity must be numeric.'; + } + + if (!$errors) { + $uploadDir = __DIR__ . '/uploads/profiles/' . $userId . '/'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0775, true); + } + + $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; + $saveImage = static function (string $tmpName, string $prefix) use ($uploadDir, $allowed): ?string { + $mime = mime_content_type($tmpName) ?: ''; + if (!isset($allowed[$mime])) { + return null; + } + $filename = uniqid($prefix, true) . '.' . $allowed[$mime]; + $target = $uploadDir . $filename; + if (!move_uploaded_file($tmpName, $target)) { + return null; + } + return 'uploads/profiles/' . basename($uploadDir) . '/' . $filename; + }; + + $idCardPaths = []; + foreach (array_slice($_FILES['id_card']['tmp_name'] ?? [], 0, 2) as $tmp) { + if (!is_uploaded_file($tmp)) { + continue; + } + $path = $saveImage($tmp, 'id_'); + if ($path) { + $idCardPaths[] = $path; + } + } + + $regPaths = []; + foreach (array_slice($_FILES['registration']['tmp_name'] ?? [], 0, 2) as $tmp) { + if (!is_uploaded_file($tmp)) { + continue; + } + $path = $saveImage($tmp, 'reg_'); + if ($path) { + $regPaths[] = $path; + } + } + + $truckPic = null; + $truckTmp = $_FILES['truck_picture']['tmp_name'] ?? ''; + if (is_uploaded_file($truckTmp)) { + $truckPic = $saveImage($truckTmp, 'truck_'); + } + + if (count($idCardPaths) < 2 || count($regPaths) < 2 || !$truckPic) { + $errors[] = 'Please upload all required truck-owner images (ID front/back, registration front/back, truck photo).'; + } else { + $profileStmt = db()->prepare( + "INSERT INTO truck_owner_profiles (user_id, truck_type, load_capacity, plate_no, id_card_path, truck_pic_path, registration_path) + VALUES (?, ?, ?, ?, ?, ?, ?)" + ); + $profileStmt->execute([ + $userId, + $truckType, + $loadCapacity, + $plateNo, + json_encode($idCardPaths, JSON_UNESCAPED_SLASHES), + $truckPic, + json_encode($regPaths, JSON_UNESCAPED_SLASHES), + ]); + } + } + } + + if (!$errors) { + $saved = true; + } + } +} + +render_header('Register Account'); +?> + +
+

Create account

+

Register as a shipper or truck owner using a clean onboarding form.

+
+ +
+ +
Registration completed successfully.
+ + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ + Back to admin +
+
+
+ + + + diff --git a/shipment_detail.php b/shipment_detail.php new file mode 100644 index 0000000..46dd63a --- /dev/null +++ b/shipment_detail.php @@ -0,0 +1,134 @@ + 0) { + $stmt = db()->prepare("SELECT * FROM shipments WHERE id = :id"); + $stmt->execute([':id' => $shipmentId]); + $shipment = $stmt->fetch(); +} + +$errors = []; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'submit_offer') { + $offerOwner = trim($_POST['offer_owner'] ?? ''); + $offerPrice = trim($_POST['offer_price'] ?? ''); + if ($offerOwner === '' || $offerPrice === '') { + $errors[] = t('error_required'); + } elseif (!is_numeric($offerPrice)) { + $errors[] = t('error_invalid'); + } else { + $stmt = db()->prepare( + "UPDATE shipments SET offer_owner = :offer_owner, offer_price = :offer_price, status = 'offered' + WHERE id = :id" + ); + $stmt->execute([ + ':offer_owner' => $offerOwner, + ':offer_price' => $offerPrice, + ':id' => $shipmentId, + ]); + set_flash('success', t('success_offer')); + header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId])); + exit; + } +} + +$flash = get_flash(); + +render_header(t('shipment_detail')); +?> + + +
+

+
+ +
+
+
+
+
+

#

+

:

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+

+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+ +
+
+
+
+ + + diff --git a/shipper_dashboard.php b/shipper_dashboard.php new file mode 100644 index 0000000..34fe5de --- /dev/null +++ b/shipper_dashboard.php @@ -0,0 +1,164 @@ +prepare( + "INSERT INTO shipments (shipper_name, shipper_company, origin_city, destination_city, cargo_description, weight_tons, pickup_date, delivery_date, payment_method) + VALUES (:shipper_name, :shipper_company, :origin_city, :destination_city, :cargo_description, :weight_tons, :pickup_date, :delivery_date, :payment_method)" + ); + $stmt->execute([ + ':shipper_name' => $shipperName, + ':shipper_company' => $shipperCompany, + ':origin_city' => $origin, + ':destination_city' => $destination, + ':cargo_description' => $cargo, + ':weight_tons' => $weight, + ':pickup_date' => $pickupDate, + ':delivery_date' => $deliveryDate, + ':payment_method' => $payment, + ]); + $_SESSION['shipper_name'] = $shipperName; + set_flash('success', t('success_shipment')); + header('Location: ' . url_with_lang('shipper_dashboard.php')); + exit; + } +} + +$shipments = []; +try { + $stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 20"); + $shipments = $stmt->fetchAll(); +} catch (Throwable $e) { + $shipments = []; +} + +render_header(t('shipper_dashboard'), 'shipper'); + +$flash = get_flash(); +?> + +
+
+
+

+ +
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+

+ total +
+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+ + diff --git a/truck_owner_dashboard.php b/truck_owner_dashboard.php new file mode 100644 index 0000000..960c175 --- /dev/null +++ b/truck_owner_dashboard.php @@ -0,0 +1,110 @@ +prepare( + "UPDATE shipments SET offer_owner = :offer_owner, offer_price = :offer_price, status = 'offered' + WHERE id = :id AND status IN ('posted','offered')" + ); + $stmt->execute([ + ':offer_owner' => $offerOwner, + ':offer_price' => $offerPrice, + ':id' => $shipmentId, + ]); + if ($stmt->rowCount() > 0) { + set_flash('success', t('success_offer')); + header('Location: ' . url_with_lang('truck_owner_dashboard.php')); + exit; + } else { + $errors[] = t('error_invalid'); + } + } +} + +$shipments = []; +try { + $stmt = db()->query("SELECT * FROM shipments WHERE status IN ('posted','offered') ORDER BY created_at DESC LIMIT 20"); + $shipments = $stmt->fetchAll(); +} catch (Throwable $e) { + $shipments = []; +} + +render_header(t('owner_dashboard'), 'owner'); + +$flash = get_flash(); +?> + +
+
+

+ total +
+ +
+ + +
+ + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + + +
+
+
+
+ +
+ +