Autosave: 20260307-132803
This commit is contained in:
parent
c4179e0064
commit
41b182bfba
113
admin_dashboard.php
Normal file
113
admin_dashboard.php
Normal 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
177
admin_manage_locations.php
Normal 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(); ?>
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
33
db/migrations/initial_schema.php
Normal file
33
db/migrations/initial_schema.php
Normal 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
222
includes/app.php
Normal 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
108
includes/layout.php
Normal 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
274
index.php
@ -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
200
register.php
Normal 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
134
shipment_detail.php
Normal 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
164
shipper_dashboard.php
Normal 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
110
truck_owner_dashboard.php
Normal 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(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user