Autosave: 20260307-132803

This commit is contained in:
Flatlogic Bot 2026-03-07 13:28:03 +00:00
parent c4179e0064
commit 41b182bfba
12 changed files with 1586 additions and 542 deletions

113
admin_dashboard.php Normal file
View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
ensure_schema();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_status') {
$shipmentId = (int) ($_POST['shipment_id'] ?? 0);
$status = $_POST['status'] ?? 'posted';
$allowed = ['posted', 'offered', 'confirmed', 'in_transit', 'delivered'];
if ($shipmentId <= 0 || !in_array($status, $allowed, true)) {
$errors[] = t('error_invalid');
} else {
$stmt = db()->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');
?>
<div class="row g-4">
<div class="col-lg-3">
<?php render_admin_sidebar('dashboard'); ?>
</div>
<div class="col-lg-9">
<div class="page-intro">
<h1 class="section-title mb-1"><?= e(t('admin_dashboard')) ?></h1>
<p class="muted mb-0">Control shipment status, manage locations, and onboard new users from one place.</p>
</div>
<div class="panel p-4 mb-4">
<h2 class="h5 mb-3">Quick actions</h2>
<div class="d-flex flex-wrap gap-2">
<a href="<?= e(url_with_lang('admin_manage_locations.php')) ?>" class="btn btn-outline-dark">Manage Locations</a>
<a href="<?= e(url_with_lang('register.php')) ?>" class="btn btn-primary">Register New User</a>
</div>
</div>
<div class="panel p-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="section-title h4 mb-0"><?= e(t('admin_dashboard')) ?></h2>
<span class="small text-muted"><?= e(count($shipments)) ?> total</span>
</div>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<?php if (!$shipments): ?>
<p class="muted mb-0"><?= e(t('no_shipments')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th><?= e(t('shipper_company')) ?></th>
<th><?= e(t('origin')) ?></th>
<th><?= e(t('destination')) ?></th>
<th><?= e(t('payment_method')) ?></th>
<th><?= e(t('status')) ?></th>
<th><?= e(t('update_status')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($shipments as $row): ?>
<tr>
<td><?= e($row['shipper_company']) ?></td>
<td><?= e($row['origin_city']) ?></td>
<td><?= e($row['destination_city']) ?></td>
<td><?= e($row['payment_method'] === 'bank_transfer' ? t('payment_bank') : t('payment_thawani')) ?></td>
<td><span class="badge <?= e($row['status']) ?>"><?= e(status_label($row['status'])) ?></span></td>
<td>
<form class="d-flex gap-2" method="post">
<input type="hidden" name="action" value="update_status">
<input type="hidden" name="shipment_id" value="<?= e($row['id']) ?>">
<select class="form-select form-select-sm" name="status">
<option value="posted" <?= $row['status'] === 'posted' ? 'selected' : '' ?>><?= e(t('status_posted')) ?></option>
<option value="offered" <?= $row['status'] === 'offered' ? 'selected' : '' ?>><?= e(t('status_offered')) ?></option>
<option value="confirmed" <?= $row['status'] === 'confirmed' ? 'selected' : '' ?>><?= e(t('status_confirmed')) ?></option>
<option value="in_transit" <?= $row['status'] === 'in_transit' ? 'selected' : '' ?>><?= e(t('status_in_transit')) ?></option>
<option value="delivered" <?= $row['status'] === 'delivered' ? 'selected' : '' ?>><?= e(t('status_delivered')) ?></option>
</select>
<button class="btn btn-sm btn-primary" type="submit"><?= e(t('save')) ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

177
admin_manage_locations.php Normal file
View File

@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
$errors = [];
$flash = null;
db()->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');
?>
<div class="row g-4">
<div class="col-lg-3">
<?php render_admin_sidebar('locations'); ?>
</div>
<div class="col-lg-9">
<div class="page-intro">
<h1 class="section-title mb-1">Country & city setup</h1>
<p class="muted mb-0">Define allowed origin and destination options for shipments.</p>
</div>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<div class="row g-4">
<div class="col-lg-6">
<div class="panel p-4 h-100">
<h2 class="h5 mb-3">Add country</h2>
<form method="post">
<div class="mb-3">
<label class="form-label" for="country_name_en">Country name (EN)</label>
<input id="country_name_en" type="text" name="country_name_en" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="country_name_ar">Country name (AR)</label>
<input id="country_name_ar" type="text" name="country_name_ar" class="form-control">
</div>
<button type="submit" name="add_country" class="btn btn-primary">Add country</button>
</form>
</div>
</div>
<div class="col-lg-6">
<div class="panel p-4 h-100">
<h2 class="h5 mb-3">Add city</h2>
<form method="post">
<div class="mb-3">
<label class="form-label" for="country_id">Country</label>
<select id="country_id" name="country_id" class="form-select" required>
<option value="">Select country</option>
<?php foreach ($countries as $country): ?>
<option value="<?= e($country['id']) ?>"><?= e($country['display_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="city_name_en">City name (EN)</label>
<input id="city_name_en" type="text" name="city_name_en" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label" for="city_name_ar">City name (AR)</label>
<input id="city_name_ar" type="text" name="city_name_ar" class="form-control">
</div>
<button type="submit" name="add_city" class="btn btn-primary">Add city</button>
</form>
</div>
</div>
</div>
<div class="panel p-4 mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h5 mb-0">Recently added cities</h2>
<a class="btn btn-sm btn-outline-dark" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">Back to admin</a>
</div>
<?php if (!$cities): ?>
<p class="muted mb-0">No cities added yet.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table mb-0">
<thead>
<tr>
<th>Country</th>
<th>City</th>
</tr>
</thead>
<tbody>
<?php foreach ($cities as $city): ?>
<tr>
<td><?= e($city['country_name']) ?></td>
<td><?= e($city['city_name']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

View File

@ -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;
}

View File

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

View File

@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
try {
$pdo->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();
}

222
includes/app.php Normal file
View File

@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$lang = $_GET['lang'] ?? ($_SESSION['lang'] ?? 'en');
$lang = $lang === 'ar' ? 'ar' : 'en';
$_SESSION['lang'] = $lang;
$dir = $lang === 'ar' ? 'rtl' : 'ltr';
$translations = [
'en' => [
'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 = <<<SQL
CREATE TABLE IF NOT EXISTS shipments (
id INT AUTO_INCREMENT PRIMARY KEY,
shipper_name VARCHAR(120) NOT NULL,
shipper_company VARCHAR(120) NOT NULL,
origin_city VARCHAR(120) NOT NULL,
destination_city VARCHAR(120) NOT NULL,
cargo_description VARCHAR(255) NOT NULL,
weight_tons DECIMAL(10,2) NOT NULL,
pickup_date DATE NOT NULL,
delivery_date DATE NOT NULL,
payment_method ENUM('thawani','bank_transfer') NOT NULL DEFAULT 'thawani',
status ENUM('posted','offered','confirmed','in_transit','delivered') NOT NULL DEFAULT 'posted',
offer_price DECIMAL(10,2) DEFAULT NULL,
offer_owner VARCHAR(120) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL;
db()->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;
}

108
includes/layout.php Normal file
View File

@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/app.php';
function render_header(string $title, string $active = ''): void
{
global $lang, $dir;
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$toggleLang = $lang === 'ar' ? 'en' : 'ar';
$toggleLabel = $lang === 'ar' ? 'EN' : 'AR';
?>
<!doctype html>
<html lang="<?= e($lang) ?>" dir="<?= e($dir) ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= e($title) ?></title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= e($projectDescription) ?>" />
<meta property="og:description" content="<?= e($projectDescription) ?>" />
<meta property="twitter:description" content="<?= e($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= e($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
</head>
<body class="app-body">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top">
<div class="container">
<a class="navbar-brand fw-semibold" href="<?= e(url_with_lang('index.php')) ?>">
<?= e(t('app_name')) ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link <?= $active === 'home' ? 'active' : '' ?>" href="<?= e(url_with_lang('index.php')) ?>">
<?= e(t('nav_home')) ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $active === 'shipper' ? 'active' : '' ?>" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>">
<?= e(t('nav_shipper')) ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $active === 'owner' ? 'active' : '' ?>" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>">
<?= e(t('nav_owner')) ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $active === 'admin' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">
<?= e(t('nav_admin')) ?>
</a>
</li>
</ul>
<a class="btn btn-sm btn-outline-dark" href="<?= e(current_url_with_lang($toggleLang)) ?>">
<?= e($toggleLabel) ?>
</a>
</div>
</div>
</nav>
<main class="container py-4">
<?php
}
function render_footer(): void
{
?>
</main>
<footer class="border-top py-3">
<div class="container d-flex flex-column flex-md-row justify-content-between align-items-center small text-muted">
<span><?= e(t('footer_note')) ?></span>
<span><?= e(date('Y-m-d H:i')) ?> UTC</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>
<?php
}
function render_admin_sidebar(string $active = 'dashboard'): void
{
$items = [
'dashboard' => ['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')],
];
?>
<aside class="admin-sidebar panel p-3">
<h2 class="h6 mb-3">Admin Panel</h2>
<nav class="nav flex-column gap-1">
<?php foreach ($items as $key => $item): ?>
<a class="admin-nav-link <?= $active === $key ? 'active' : '' ?>" href="<?= e($item['href']) ?>">
<?= e($item['label']) ?>
</a>
<?php endforeach; ?>
</nav>
</aside>
<?php
}

274
index.php
View File

@ -1,150 +1,134 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
require_once __DIR__ . '/includes/layout.php';
ensure_schema();
$stats = [
'shipments' => 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');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<section class="hero-card mb-4">
<div class="row align-items-center g-4">
<div class="col-lg-7">
<p class="text-uppercase small fw-semibold text-muted mb-2"><?= e(t('hero_tagline')) ?></p>
<h1 class="display-6 fw-semibold"><?= e(t('hero_title')) ?></h1>
<p class="muted mt-3"><?= e(t('hero_subtitle')) ?></p>
<div class="d-flex flex-wrap gap-2 mt-4">
<a class="btn btn-primary" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>"><?= e(t('cta_shipper')) ?></a>
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>"><?= e(t('cta_owner')) ?></a>
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('admin_dashboard.php')) ?>"><?= e(t('cta_admin')) ?></a>
</div>
</div>
<div class="col-lg-5">
<div class="row g-3">
<div class="col-12">
<div class="stat-card">
<div class="small text-muted"><?= e(t('stats_shipments')) ?></div>
<div class="fs-4 fw-semibold"><?= e($stats['shipments']) ?></div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="small text-muted"><?= e(t('stats_offers')) ?></div>
<div class="fs-5 fw-semibold"><?= e($stats['offers']) ?></div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="small text-muted"><?= e(t('stats_confirmed')) ?></div>
<div class="fs-5 fw-semibold"><?= e($stats['confirmed']) ?></div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
</section>
<section class="mb-4">
<h2 class="section-title"><?= e(t('section_workflow')) ?></h2>
<div class="row g-3">
<div class="col-md-4">
<div class="panel p-3 h-100">
<div class="fw-semibold mb-1">1</div>
<p class="muted mb-0"><?= e(t('step_post')) ?></p>
</div>
</div>
<div class="col-md-4">
<div class="panel p-3 h-100">
<div class="fw-semibold mb-1">2</div>
<p class="muted mb-0"><?= e(t('step_offer')) ?></p>
</div>
</div>
<div class="col-md-4">
<div class="panel p-3 h-100">
<div class="fw-semibold mb-1">3</div>
<p class="muted mb-0"><?= e(t('step_confirm')) ?></p>
</div>
</div>
</div>
</section>
<section class="panel p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="section-title mb-0"><?= e(t('recent_shipments')) ?></h2>
<a class="small text-decoration-none" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>"><?= e(t('cta_shipper')) ?></a>
</div>
<?php if (!$recentShipments): ?>
<div class="text-muted"><?= e(t('no_shipments')) ?></div>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th><?= e(t('shipper_company')) ?></th>
<th><?= e(t('origin')) ?></th>
<th><?= e(t('destination')) ?></th>
<th><?= e(t('status')) ?></th>
<th><?= e(t('actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($recentShipments as $row): ?>
<tr>
<td><?= e($row['shipper_company']) ?></td>
<td><?= e($row['origin_city']) ?></td>
<td><?= e($row['destination_city']) ?></td>
<td><span class="badge-status <?= e($row['status']) ?>"><?= e(status_label($row['status'])) ?></span></td>
<td>
<a class="btn btn-sm btn-outline-dark" href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>">
<?= e(t('view')) ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<?php render_footer(); ?>

200
register.php Normal file
View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
$errors = [];
$saved = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$role = $_POST['role'] ?? 'shipper';
$email = trim($_POST['email'] ?? '');
$passwordRaw = (string)($_POST['password'] ?? '');
if (!in_array($role, ['shipper', 'truck_owner'], true)) {
$errors[] = 'Invalid role selected.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Please provide a valid email address.';
}
if (strlen($passwordRaw) < 6) {
$errors[] = 'Password must be at least 6 characters.';
}
if (!$errors) {
$password = password_hash($passwordRaw, PASSWORD_DEFAULT);
$stmt = db()->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');
?>
<div class="page-intro">
<h1 class="section-title mb-1">Create account</h1>
<p class="muted mb-0">Register as a shipper or truck owner using a clean onboarding form.</p>
</div>
<div class="panel p-4">
<?php if ($saved): ?>
<div class="alert alert-success">Registration completed successfully.</div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" id="regForm" novalidate>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="role">Role</label>
<select name="role" id="role" class="form-select" onchange="toggleFields()" required>
<option value="shipper">Shipper</option>
<option value="truck_owner">Truck Owner</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="email">Email</label>
<input type="email" name="email" id="email" class="form-control" required>
</div>
<div class="col-md-4">
<label class="form-label" for="password">Password</label>
<input type="password" name="password" id="password" class="form-control" minlength="6" required>
</div>
</div>
<div id="truckFields" class="mt-4" style="display:none;">
<h2 class="h5 mb-3">Truck owner details</h2>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label" for="truck_type">Truck type</label>
<input type="text" name="truck_type" id="truck_type" class="form-control">
</div>
<div class="col-md-4">
<label class="form-label" for="load_capacity">Load capacity (tons)</label>
<input type="number" name="load_capacity" id="load_capacity" class="form-control" step="0.01" min="0.1">
</div>
<div class="col-md-4">
<label class="form-label" for="plate_no">Plate number</label>
<input type="text" name="plate_no" id="plate_no" class="form-control">
</div>
<div class="col-md-4">
<label class="form-label" for="id_card">ID card (front & back)</label>
<input type="file" name="id_card[]" id="id_card" class="form-control" accept="image/png,image/jpeg,image/webp" multiple>
</div>
<div class="col-md-4">
<label class="form-label" for="truck_picture">Clear truck photo</label>
<input type="file" name="truck_picture" id="truck_picture" class="form-control" accept="image/png,image/jpeg,image/webp">
</div>
<div class="col-md-4">
<label class="form-label" for="registration">Truck registration (front & back)</label>
<input type="file" name="registration[]" id="registration" class="form-control" accept="image/png,image/jpeg,image/webp" multiple>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" class="btn btn-primary">Create account</button>
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">Back to admin</a>
</div>
</form>
</div>
<script>
function toggleFields() {
const role = document.getElementById('role').value;
const truckFields = document.getElementById('truckFields');
const isOwner = role === 'truck_owner';
truckFields.style.display = isOwner ? 'block' : 'none';
truckFields.querySelectorAll('input').forEach((input) => {
input.required = isOwner;
});
}
toggleFields();
</script>
<?php render_footer(); ?>

134
shipment_detail.php Normal file
View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
ensure_schema();
$shipmentId = (int) ($_GET['id'] ?? 0);
$shipment = null;
if ($shipmentId > 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'));
?>
<?php if (!$shipment): ?>
<div class="panel p-4">
<p class="muted mb-0"><?= e(t('no_shipments')) ?></p>
</div>
<?php else: ?>
<div class="row g-4">
<div class="col-lg-7">
<div class="panel p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h2 class="section-title mb-1"><?= e(t('shipment_detail')) ?> #<?= e($shipment['id']) ?></h2>
<p class="muted mb-0"><?= e(t('created_at')) ?>: <?= e($shipment['created_at']) ?></p>
</div>
<span class="badge-status <?= e($shipment['status']) ?>"><?= e(status_label($shipment['status'])) ?></span>
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="small text-muted"><?= e(t('shipper_company')) ?></div>
<div class="fw-semibold"><?= e($shipment['shipper_company']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('shipper_name')) ?></div>
<div class="fw-semibold"><?= e($shipment['shipper_name']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('origin')) ?></div>
<div class="fw-semibold"><?= e($shipment['origin_city']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('destination')) ?></div>
<div class="fw-semibold"><?= e($shipment['destination_city']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('cargo')) ?></div>
<div class="fw-semibold"><?= e($shipment['cargo_description']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('weight')) ?></div>
<div class="fw-semibold"><?= e($shipment['weight_tons']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('pickup_date')) ?></div>
<div class="fw-semibold"><?= e($shipment['pickup_date']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('delivery_date')) ?></div>
<div class="fw-semibold"><?= e($shipment['delivery_date']) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('payment_method')) ?></div>
<div class="fw-semibold"><?= e($shipment['payment_method'] === 'bank_transfer' ? t('payment_bank') : t('payment_thawani')) ?></div>
</div>
<div class="col-md-6">
<div class="small text-muted"><?= e(t('best_offer')) ?></div>
<div class="fw-semibold"><?= $shipment['offer_price'] ? e($shipment['offer_price'] . ' / ' . ($shipment['offer_owner'] ?? '')) : e(t('no_offers')) ?></div>
</div>
</div>
<div class="mt-4">
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>"><?= e(t('nav_owner')) ?></a>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="panel p-4">
<h3 class="section-title"><?= e(t('submit_offer')) ?></h3>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="submit_offer">
<div class="mb-3">
<label class="form-label"><?= e(t('offer_owner')) ?></label>
<input class="form-control" name="offer_owner" required>
</div>
<div class="mb-3">
<label class="form-label"><?= e(t('offer_price')) ?></label>
<input class="form-control" name="offer_price" type="number" step="0.01" min="0.1" required>
</div>
<button class="btn btn-primary w-100" type="submit"><?= e(t('submit_offer')) ?></button>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php render_footer(); ?>

164
shipper_dashboard.php Normal file
View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
ensure_schema();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_shipment') {
$shipperName = trim($_POST['shipper_name'] ?? '');
$shipperCompany = trim($_POST['shipper_company'] ?? '');
$origin = trim($_POST['origin_city'] ?? '');
$destination = trim($_POST['destination_city'] ?? '');
$cargo = trim($_POST['cargo_description'] ?? '');
$weight = trim($_POST['weight_tons'] ?? '');
$pickupDate = trim($_POST['pickup_date'] ?? '');
$deliveryDate = trim($_POST['delivery_date'] ?? '');
$payment = $_POST['payment_method'] ?? 'thawani';
if ($shipperName === '' || $shipperCompany === '' || $origin === '' || $destination === '' || $cargo === '' || $weight === '' || $pickupDate === '' || $deliveryDate === '') {
$errors[] = t('error_required');
} elseif (!is_numeric($weight)) {
$errors[] = t('error_invalid');
}
if (!$errors) {
$stmt = db()->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();
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="panel p-3">
<h2 class="section-title"><?= e(t('new_shipment')) ?></h2>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="create_shipment">
<div class="mb-3">
<label class="form-label"><?= e(t('shipper_name')) ?></label>
<input class="form-control" name="shipper_name" value="<?= e($_SESSION['shipper_name'] ?? '') ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= e(t('shipper_company')) ?></label>
<input class="form-control" name="shipper_company" required>
</div>
<div class="mb-3">
<label class="form-label"><?= e(t('origin')) ?></label>
<input class="form-control" name="origin_city" required>
</div>
<div class="mb-3">
<label class="form-label"><?= e(t('destination')) ?></label>
<input class="form-control" name="destination_city" required>
</div>
<div class="mb-3">
<label class="form-label"><?= e(t('cargo')) ?></label>
<input class="form-control" name="cargo_description" required>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label"><?= e(t('weight')) ?></label>
<input class="form-control" name="weight_tons" type="number" step="0.01" min="0.1" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= e(t('payment_method')) ?></label>
<select class="form-select" name="payment_method">
<option value="thawani"><?= e(t('payment_thawani')) ?></option>
<option value="bank_transfer"><?= e(t('payment_bank')) ?></option>
</select>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-6">
<label class="form-label"><?= e(t('pickup_date')) ?></label>
<input class="form-control" name="pickup_date" type="date" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= e(t('delivery_date')) ?></label>
<input class="form-control" name="delivery_date" type="date" required>
</div>
</div>
<button class="btn btn-primary w-100 mt-4" type="submit"><?= e(t('submit_shipment')) ?></button>
</form>
</div>
</div>
<div class="col-lg-7">
<div class="panel p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="section-title mb-0"><?= e(t('shipments_list')) ?></h2>
<span class="small text-muted"><?= e(count($shipments)) ?> total</span>
</div>
<?php if (!$shipments): ?>
<p class="muted mb-0"><?= e(t('no_shipments')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th><?= e(t('origin')) ?></th>
<th><?= e(t('destination')) ?></th>
<th><?= e(t('status')) ?></th>
<th><?= e(t('offer')) ?></th>
<th><?= e(t('actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($shipments as $row): ?>
<tr>
<td><?= e($row['origin_city']) ?></td>
<td><?= e($row['destination_city']) ?></td>
<td><span class="badge-status <?= e($row['status']) ?>"><?= e(status_label($row['status'])) ?></span></td>
<td><?= $row['offer_price'] ? e($row['offer_price'] . ' / ' . ($row['offer_owner'] ?? '')) : e(t('no_offers')) ?></td>
<td>
<a class="btn btn-sm btn-outline-dark" href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>">
<?= e(t('view')) ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

110
truck_owner_dashboard.php Normal file
View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
ensure_schema();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'submit_offer') {
$shipmentId = (int) ($_POST['shipment_id'] ?? 0);
$offerOwner = trim($_POST['offer_owner'] ?? '');
$offerPrice = trim($_POST['offer_price'] ?? '');
if ($shipmentId <= 0 || $offerOwner === '' || $offerPrice === '') {
$errors[] = t('error_required');
} elseif (!is_numeric($offerPrice)) {
$errors[] = t('error_invalid');
}
if (!$errors) {
$stmt = db()->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();
?>
<div class="panel p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="section-title mb-0"><?= e(t('available_shipments')) ?></h2>
<span class="small text-muted"><?= e(count($shipments)) ?> total</span>
</div>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<?php if (!$shipments): ?>
<p class="muted mb-0"><?= e(t('no_shipments')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th><?= e(t('shipper_company')) ?></th>
<th><?= e(t('origin')) ?></th>
<th><?= e(t('destination')) ?></th>
<th><?= e(t('weight')) ?></th>
<th><?= e(t('offer')) ?></th>
<th><?= e(t('actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($shipments as $row): ?>
<tr>
<td><?= e($row['shipper_company']) ?></td>
<td><?= e($row['origin_city']) ?></td>
<td><?= e($row['destination_city']) ?></td>
<td><?= e($row['weight_tons']) ?></td>
<td><?= $row['offer_price'] ? e($row['offer_price'] . ' / ' . ($row['offer_owner'] ?? '')) : e(t('no_offers')) ?></td>
<td>
<form class="d-flex flex-column gap-2" method="post">
<input type="hidden" name="action" value="submit_offer">
<input type="hidden" name="shipment_id" value="<?= e($row['id']) ?>">
<input class="form-control form-control-sm" name="offer_owner" placeholder="<?= e(t('offer_owner')) ?>" required>
<input class="form-control form-control-sm" name="offer_price" type="number" step="0.01" min="0.1" placeholder="<?= e(t('offer_price')) ?>" required>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-primary" type="submit"><?= e(t('submit_offer')) ?></button>
<a class="btn btn-sm btn-outline-dark" href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>">
<?= e(t('view')) ?>
</a>
</div>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php render_footer(); ?>