Autosave: 20260307-172929
This commit is contained in:
parent
41b182bfba
commit
8bc413c26a
226
admin_cities.php
Normal file
226
admin_cities.php
Normal file
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
$editCityId = isset($_GET['edit_city']) ? (int)$_GET['edit_city'] : 0;
|
||||
|
||||
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_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.';
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['update_city'])) {
|
||||
$cityId = (int)($_POST['city_id'] ?? 0);
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
$cityNameEn = trim($_POST['city_name_en'] ?? '');
|
||||
$cityNameAr = trim($_POST['city_name_ar'] ?? '');
|
||||
if ($cityId <= 0 || $countryId <= 0 || $cityNameEn === '') {
|
||||
$errors[] = 'City ID, country and English city name are required.';
|
||||
} else {
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE cities SET country_id = ?, name_en = ?, name_ar = ? WHERE id = ?");
|
||||
$stmt->execute([$countryId, $cityNameEn, $cityNameAr !== '' ? $cityNameAr : null, $cityId]);
|
||||
$flash = 'City updated.';
|
||||
$editCityId = 0;
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = 'City could not be updated.';
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['delete_city'])) {
|
||||
$cityId = (int)($_POST['city_id'] ?? 0);
|
||||
if ($cityId <= 0) {
|
||||
$errors[] = 'Invalid city selected.';
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM cities WHERE id = ?");
|
||||
$stmt->execute([$cityId]);
|
||||
$flash = 'City deleted.';
|
||||
$editCityId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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, name_en, name_ar, {$countryNameExprNoAlias} AS display_name FROM countries ORDER BY display_name ASC")->fetchAll();
|
||||
$cities = db()->query(
|
||||
"SELECT
|
||||
c.id,
|
||||
c.country_id,
|
||||
c.name_en,
|
||||
c.name_ar,
|
||||
{$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 200"
|
||||
)->fetchAll();
|
||||
|
||||
$editingCity = null;
|
||||
if ($editCityId > 0) {
|
||||
foreach ($cities as $city) {
|
||||
if ((int)$city['id'] === $editCityId) {
|
||||
$editingCity = $city;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_header('Manage Cities', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('cities'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro">
|
||||
<h1 class="section-title mb-1">Cities</h1>
|
||||
<p class="muted mb-0">Manage cities and map each city to its country.</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="panel p-4">
|
||||
<h2 class="h5 mb-3">Add city</h2>
|
||||
<form method="post" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<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="col-md-4">
|
||||
<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="col-md-4">
|
||||
<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>
|
||||
<div class="col-12">
|
||||
<button type="submit" name="add_city" class="btn btn-primary">Add city</button>
|
||||
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('admin_countries.php')) ?>">Go to countries</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel p-4 mt-4">
|
||||
<h2 class="h5 mb-2">Cities list</h2>
|
||||
|
||||
<?php if ($editingCity): ?>
|
||||
<form method="post" class="row g-2 align-items-end mb-3">
|
||||
<input type="hidden" name="city_id" value="<?= e((string)$editingCity['id']) ?>">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">Country</label>
|
||||
<select class="form-select form-select-sm" name="country_id" required>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= e((string)$country['id']) ?>" <?= (int)$country['id'] === (int)$editingCity['country_id'] ? 'selected' : '' ?>>
|
||||
<?= e($country['display_name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">City (EN)</label>
|
||||
<input class="form-control form-control-sm" type="text" name="city_name_en" value="<?= e($editingCity['name_en']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">City (AR)</label>
|
||||
<input class="form-control form-control-sm" type="text" name="city_name_ar" value="<?= e((string)($editingCity['name_ar'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" name="update_city" class="btn btn-sm btn-primary">Save</button>
|
||||
<a href="<?= e(url_with_lang('admin_cities.php')) ?>" class="btn btn-sm btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?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>ID</th>
|
||||
<th>Country</th>
|
||||
<th>City (EN)</th>
|
||||
<th>City (AR)</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($cities as $city): ?>
|
||||
<tr>
|
||||
<td><?= e((string)$city['id']) ?></td>
|
||||
<td><?= e($city['country_name']) ?></td>
|
||||
<td><?= e($city['name_en']) ?></td>
|
||||
<td><?= e((string)($city['name_ar'] ?? '-')) ?></td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= e(url_with_lang('admin_cities.php', ['edit_city' => (int)$city['id']])) ?>">Edit</a>
|
||||
<form method="post" class="d-inline" onsubmit="return confirm('Delete this city?');">
|
||||
<input type="hidden" name="city_id" value="<?= e((string)$city['id']) ?>">
|
||||
<button type="submit" name="delete_city" class="btn btn-sm btn-outline-danger">Del</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
183
admin_countries.php
Normal file
183
admin_countries.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
$editCountryId = isset($_GET['edit_country']) ? (int)$_GET['edit_country'] : 0;
|
||||
|
||||
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['update_country'])) {
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
$countryNameEn = trim($_POST['country_name_en'] ?? '');
|
||||
$countryNameAr = trim($_POST['country_name_ar'] ?? '');
|
||||
if ($countryId <= 0 || $countryNameEn === '') {
|
||||
$errors[] = 'Country ID and English name are required.';
|
||||
} else {
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE countries SET name_en = ?, name_ar = ? WHERE id = ?");
|
||||
$stmt->execute([$countryNameEn, $countryNameAr !== '' ? $countryNameAr : null, $countryId]);
|
||||
$flash = 'Country updated.';
|
||||
$editCountryId = 0;
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = 'Country could not be updated.';
|
||||
}
|
||||
}
|
||||
} elseif (isset($_POST['delete_country'])) {
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
if ($countryId <= 0) {
|
||||
$errors[] = 'Invalid country selected.';
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM countries WHERE id = ?");
|
||||
$stmt->execute([$countryId]);
|
||||
$flash = 'Country deleted.';
|
||||
$editCountryId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$countryNameExprNoAlias = $lang === 'ar'
|
||||
? "COALESCE(NULLIF(name_ar, ''), name_en)"
|
||||
: "COALESCE(NULLIF(name_en, ''), name_ar)";
|
||||
$countries = db()->query("SELECT id, name_en, name_ar, {$countryNameExprNoAlias} AS display_name FROM countries ORDER BY display_name ASC")->fetchAll();
|
||||
|
||||
$editingCountry = null;
|
||||
if ($editCountryId > 0) {
|
||||
foreach ($countries as $country) {
|
||||
if ((int)$country['id'] === $editCountryId) {
|
||||
$editingCountry = $country;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_header('Manage Countries', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('countries'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro">
|
||||
<h1 class="section-title mb-1">Countries</h1>
|
||||
<p class="muted mb-0">Manage the list of allowed countries for shipment routes.</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="panel p-4">
|
||||
<h2 class="h5 mb-3">Add country</h2>
|
||||
<form method="post" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<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="col-md-6">
|
||||
<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>
|
||||
<div class="col-12">
|
||||
<button type="submit" name="add_country" class="btn btn-primary">Add country</button>
|
||||
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('admin_cities.php')) ?>">Go to cities</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel p-4 mt-4">
|
||||
<h2 class="h5 mb-2">Countries list</h2>
|
||||
|
||||
<?php if ($editingCountry): ?>
|
||||
<form method="post" class="row g-2 align-items-end mb-3">
|
||||
<input type="hidden" name="country_id" value="<?= e((string)$editingCountry['id']) ?>">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Country (EN)</label>
|
||||
<input class="form-control form-control-sm" type="text" name="country_name_en" value="<?= e($editingCountry['name_en']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Country (AR)</label>
|
||||
<input class="form-control form-control-sm" type="text" name="country_name_ar" value="<?= e((string)($editingCountry['name_ar'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-4 d-flex gap-2">
|
||||
<button type="submit" name="update_country" class="btn btn-sm btn-primary">Save</button>
|
||||
<a href="<?= e(url_with_lang('admin_countries.php')) ?>" class="btn btn-sm btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$countries): ?>
|
||||
<p class="muted mb-0">No countries added yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Country (EN)</th>
|
||||
<th>Country (AR)</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<tr>
|
||||
<td><?= e((string)$country['id']) ?></td>
|
||||
<td><?= e($country['name_en']) ?></td>
|
||||
<td><?= e((string)($country['name_ar'] ?? '-')) ?></td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= e(url_with_lang('admin_countries.php', ['edit_country' => (int)$country['id']])) ?>">Edit</a>
|
||||
<form method="post" class="d-inline" onsubmit="return confirm('Delete this country and its cities?');">
|
||||
<input type="hidden" name="country_id" value="<?= e((string)$country['id']) ?>">
|
||||
<button type="submit" name="delete_country" class="btn btn-sm btn-outline-danger">Del</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
@ -48,7 +48,8 @@ render_header(t('admin_dashboard'), 'admin');
|
||||
<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('admin_countries.php')) ?>" class="btn btn-outline-dark">Manage Countries</a>
|
||||
<a href="<?= e(url_with_lang('admin_cities.php')) ?>" class="btn btn-outline-dark">Manage Cities</a>
|
||||
<a href="<?= e(url_with_lang('register.php')) ?>" class="btn btn-primary">Register New User</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,177 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
require_once __DIR__ . '/includes/app.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(); ?>
|
||||
header('Location: ' . url_with_lang('admin_countries.php'), true, 302);
|
||||
exit;
|
||||
|
||||
210
admin_shipper_edit.php
Normal file
210
admin_shipper_edit.php
Normal file
@ -0,0 +1,210 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$userId = (int)($_GET['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
header('Location: admin_shippers.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
|
||||
// Fetch Shipper Profile
|
||||
$stmt = db()->prepare("
|
||||
SELECT u.id, u.email, u.full_name, u.status, u.role,
|
||||
p.company_name, p.phone, p.address_line, p.country_id, p.city_id
|
||||
FROM users u
|
||||
LEFT JOIN shipper_profiles p ON u.id = p.user_id
|
||||
WHERE u.id = ? AND u.role = 'shipper'
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$shipper = $stmt->fetch();
|
||||
|
||||
if (!$shipper) {
|
||||
header('Location: admin_shippers.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$countries = db()->query("SELECT id, name_en, name_ar FROM countries ORDER BY name_en ASC")->fetchAll();
|
||||
$cities = db()->query("SELECT id, country_id, name_en, name_ar FROM cities ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$fullName = trim($_POST['full_name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
$cityId = (int)($_POST['city_id'] ?? 0);
|
||||
$addressLine = trim($_POST['address_line'] ?? '');
|
||||
$companyName = trim($_POST['company_name'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
|
||||
if ($fullName === '') $errors[] = 'Full name is required.';
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Valid email is required.';
|
||||
if ($phone === '') $errors[] = 'Phone number is required.';
|
||||
if ($companyName === '') $errors[] = 'Company name is required.';
|
||||
if (!in_array($status, ['pending', 'active', 'rejected'], true)) $errors[] = 'Invalid status.';
|
||||
|
||||
if ($countryId <= 0 || $cityId <= 0) {
|
||||
$errors[] = 'Please select country and city.';
|
||||
} else {
|
||||
$cityCheck = db()->prepare("SELECT COUNT(*) FROM cities WHERE id = ? AND country_id = ?");
|
||||
$cityCheck->execute([$cityId, $countryId]);
|
||||
if ((int)$cityCheck->fetchColumn() === 0) {
|
||||
$errors[] = 'Selected city does not belong to selected country.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
try {
|
||||
db()->beginTransaction();
|
||||
|
||||
$stmtUser = db()->prepare("UPDATE users SET full_name = ?, email = ?, status = ? WHERE id = ? AND role = 'shipper'");
|
||||
$stmtUser->execute([$fullName, $email, $status, $userId]);
|
||||
|
||||
$stmtProfile = db()->prepare("
|
||||
UPDATE shipper_profiles
|
||||
SET company_name = ?, phone = ?, address_line = ?, country_id = ?, city_id = ?
|
||||
WHERE user_id = ?
|
||||
");
|
||||
$stmtProfile->execute([$companyName, $phone, $addressLine, $countryId, $cityId, $userId]);
|
||||
|
||||
db()->commit();
|
||||
$flash = 'Shipper profile updated successfully.';
|
||||
|
||||
// Refresh data
|
||||
$shipper['full_name'] = $fullName;
|
||||
$shipper['email'] = $email;
|
||||
$shipper['status'] = $status;
|
||||
$shipper['company_name'] = $companyName;
|
||||
$shipper['phone'] = $phone;
|
||||
$shipper['address_line'] = $addressLine;
|
||||
$shipper['country_id'] = $countryId;
|
||||
$shipper['city_id'] = $cityId;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
db()->rollBack();
|
||||
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
$errors[] = 'This email is already in use by another account.';
|
||||
} else {
|
||||
$errors[] = 'Failed to update shipper profile. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_header('Edit Shipper', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('shippers'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<a href="admin_shippers.php" class="text-decoration-none small text-muted mb-2 d-inline-block">← Back to Shippers</a>
|
||||
<h1 class="section-title mb-1">Edit Shipper</h1>
|
||||
<p class="muted mb-0">Update profile information for <?= e($shipper['full_name']) ?>.</p>
|
||||
</div>
|
||||
</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('<br>', $errors)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel p-4">
|
||||
<form method="post">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="full_name">Full Name</label>
|
||||
<input type="text" name="full_name" id="full_name" class="form-control" value="<?= e((string)$shipper['full_name']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email" name="email" id="email" class="form-control" value="<?= e((string)$shipper['email']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="company_name">Company Name</label>
|
||||
<input type="text" name="company_name" id="company_name" class="form-control" value="<?= e((string)$shipper['company_name']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Phone</label>
|
||||
<input type="text" name="phone" id="phone" class="form-control" value="<?= e((string)$shipper['phone']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="country_id">Country</label>
|
||||
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||
<option value="">Select country</option>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= e((string)$country['id']) ?>" <?= (string)$shipper['country_id'] === (string)$country['id'] ? 'selected' : '' ?>>
|
||||
<?= e($lang === 'ar' && !empty($country['name_ar']) ? $country['name_ar'] : $country['name_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="city_id">City</label>
|
||||
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e((string)$shipper['city_id']) ?>">
|
||||
<option value="">Select city</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="address_line">Address Line</label>
|
||||
<input type="text" name="address_line" id="address_line" class="form-control" value="<?= e((string)$shipper['address_line']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="status">Account Status</label>
|
||||
<select name="status" id="status" class="form-select" required>
|
||||
<option value="pending" <?= $shipper['status'] === 'pending' ? 'selected' : '' ?>>Pending</option>
|
||||
<option value="active" <?= $shipper['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="rejected" <?= $shipper['status'] === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="admin_shippers.php" class="btn btn-outline-dark ms-2">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
|
||||
function syncCities() {
|
||||
const countryId = document.getElementById('country_id').value;
|
||||
const citySelect = document.getElementById('city_id');
|
||||
const selectedValue = citySelect.dataset.selected || '';
|
||||
citySelect.innerHTML = '<option value="">Select city</option>';
|
||||
|
||||
allCities.forEach((city) => {
|
||||
if (String(city.country_id) !== String(countryId)) {
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = city.id;
|
||||
option.textContent = <?= $lang === 'ar' ? '(city.name_ar || city.name_en)' : '(city.name_en || city.name_ar)' ?>;
|
||||
if (String(city.id) === String(selectedValue)) {
|
||||
option.selected = true;
|
||||
}
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
// clear selected after first render
|
||||
citySelect.dataset.selected = '';
|
||||
}
|
||||
syncCities();
|
||||
</script>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
202
admin_shippers.php
Normal file
202
admin_shippers.php
Normal file
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
|
||||
// Handle action (Approve / Reject / Delete if necessary)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'], $_POST['user_id'])) {
|
||||
$userId = (int)$_POST['user_id'];
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'approve') {
|
||||
db()->prepare("UPDATE users SET status = 'active' WHERE id = ? AND role = 'shipper'")->execute([$userId]);
|
||||
$flash = 'Shipper approved successfully.';
|
||||
} elseif ($action === 'reject') {
|
||||
db()->prepare("UPDATE users SET status = 'rejected' WHERE id = ? AND role = 'shipper'")->execute([$userId]);
|
||||
$flash = 'Shipper rejected.';
|
||||
} elseif ($action === 'delete') {
|
||||
db()->prepare("DELETE FROM shipper_profiles WHERE user_id = ?")->execute([$userId]);
|
||||
db()->prepare("DELETE FROM users WHERE id = ? AND role = 'shipper'")->execute([$userId]);
|
||||
$flash = 'Shipper deleted.';
|
||||
}
|
||||
}
|
||||
|
||||
// Search and Pagination parameters
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$whereClause = "u.role = 'shipper'";
|
||||
$params = [];
|
||||
|
||||
if ($q !== '') {
|
||||
$whereClause .= " AND (u.full_name LIKE ? OR u.email LIKE ? OR p.company_name LIKE ?)";
|
||||
$likeQ = "%$q%";
|
||||
$params = [$likeQ, $likeQ, $likeQ];
|
||||
}
|
||||
|
||||
// Total count
|
||||
$countSql = "
|
||||
SELECT COUNT(*)
|
||||
FROM users u
|
||||
LEFT JOIN shipper_profiles p ON u.id = p.user_id
|
||||
WHERE $whereClause
|
||||
";
|
||||
$stmt = db()->prepare($countSql);
|
||||
$stmt->execute($params);
|
||||
$total = (int)$stmt->fetchColumn();
|
||||
$totalPages = (int)ceil($total / $limit);
|
||||
|
||||
// Fetch shippers
|
||||
$sql = "
|
||||
SELECT u.id, u.email, u.full_name, u.status, u.created_at,
|
||||
p.company_name, p.phone, p.address_line,
|
||||
c.name_en AS country_name,
|
||||
ci.name_en AS city_name
|
||||
FROM users u
|
||||
LEFT JOIN shipper_profiles p ON u.id = p.user_id
|
||||
LEFT JOIN countries c ON p.country_id = c.id
|
||||
LEFT JOIN cities ci ON p.city_id = ci.id
|
||||
WHERE $whereClause
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$shippers = $stmt->fetchAll();
|
||||
|
||||
render_header('Manage Shippers', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('shippers'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<h1 class="section-title mb-1">Shippers</h1>
|
||||
<p class="muted mb-0">Manage registered shippers.</p>
|
||||
</div>
|
||||
|
||||
<form method="get" class="d-flex mt-3 mt-md-0 gap-2">
|
||||
<input type="text" name="q" class="form-control form-control-sm" placeholder="Search name, email, company..." value="<?= e($q) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Search</button>
|
||||
<?php if ($q): ?>
|
||||
<a href="?q=" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel p-0">
|
||||
<?php if (!$shippers && $q): ?>
|
||||
<div class="p-4"><p class="muted mb-0">No shippers found matching your search.</p></div>
|
||||
<?php elseif (!$shippers): ?>
|
||||
<div class="p-4"><p class="muted mb-0">No shippers registered yet.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 align-middle table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-4">ID</th>
|
||||
<th>Name / Company</th>
|
||||
<th>Contact</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-4">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($shippers as $shipper): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><?= e((string)$shipper['id']) ?></td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark"><?= e($shipper['full_name']) ?></div>
|
||||
<div class="text-muted small"><?= e((string)$shipper['company_name']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><a href="mailto:<?= e($shipper['email']) ?>" class="text-decoration-none"><?= e($shipper['email']) ?></a></div>
|
||||
<div class="text-muted small"><?= e((string)$shipper['phone']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<?= e((string)$shipper['city_name']) ?>, <?= e((string)$shipper['country_name']) ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($shipper['status'] === 'active'): ?>
|
||||
<span class="badge bg-success-subtle text-success">Active</span>
|
||||
<?php elseif ($shipper['status'] === 'pending'): ?>
|
||||
<span class="badge bg-warning-subtle text-warning">Pending</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger-subtle text-danger"><?= e(ucfirst($shipper['status'] ?? 'unknown')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="d-inline-flex gap-1 align-items-center">
|
||||
<a href="admin_shipper_edit.php?id=<?= e((string)$shipper[id]) ?>" class="btn btn-sm btn-light border text-primary" title="Edit Shipper">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
||||
</svg>
|
||||
</a>
|
||||
<form method="post" class="d-inline m-0 p-0">
|
||||
<input type="hidden" name="user_id" value="<?= e((string)$shipper['id']) ?>">
|
||||
<?php if ($shipper['status'] !== 'active'): ?>
|
||||
<button type="submit" name="action" value="approve" class="btn btn-sm btn-success" title="Approve">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16">
|
||||
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($shipper['status'] !== 'rejected'): ?>
|
||||
<button type="submit" name="action" value="reject" class="btn btn-sm btn-warning" title="Reject">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="submit" name="action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete this shipper forever?');" title="Delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="px-4 py-3 border-top d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small">Showing <?= count($shippers) ?> of <?= $total ?> shippers</span>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $page - 1 ?>">Previous</a>
|
||||
</li>
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $page + 1 ?>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
236
admin_truck_owner_edit.php
Normal file
236
admin_truck_owner_edit.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$userId = (int)($_GET['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
header('Location: admin_truck_owners.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
|
||||
// Fetch Truck Owner Profile
|
||||
$stmt = db()->prepare("
|
||||
SELECT u.id, u.email, u.full_name, u.status, u.role,
|
||||
p.phone, p.address_line, p.country_id, p.city_id,
|
||||
p.truck_type, p.load_capacity, p.plate_no
|
||||
FROM users u
|
||||
LEFT JOIN truck_owner_profiles p ON u.id = p.user_id
|
||||
WHERE u.id = ? AND u.role = 'truck_owner'
|
||||
");
|
||||
$stmt->execute([$userId]);
|
||||
$owner = $stmt->fetch();
|
||||
|
||||
if (!$owner) {
|
||||
header('Location: admin_truck_owners.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$countries = db()->query("SELECT id, name_en, name_ar FROM countries ORDER BY name_en ASC")->fetchAll();
|
||||
$cities = db()->query("SELECT id, country_id, name_en, name_ar FROM cities ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$fullName = trim($_POST['full_name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
$cityId = (int)($_POST['city_id'] ?? 0);
|
||||
$addressLine = trim($_POST['address_line'] ?? '');
|
||||
|
||||
$truckType = trim($_POST['truck_type'] ?? '');
|
||||
$loadCapacity = trim($_POST['load_capacity'] ?? '');
|
||||
$plateNo = trim($_POST['plate_no'] ?? '');
|
||||
$status = trim($_POST['status'] ?? '');
|
||||
|
||||
if ($fullName === '') $errors[] = 'Full name is required.';
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Valid email is required.';
|
||||
if ($phone === '') $errors[] = 'Phone number is required.';
|
||||
if (!in_array($status, ['pending', 'active', 'rejected'], true)) $errors[] = 'Invalid status.';
|
||||
|
||||
if ($truckType === '' || $loadCapacity === '' || $plateNo === '') {
|
||||
$errors[] = 'Truck type, load capacity, and plate number are required.';
|
||||
} elseif (!is_numeric($loadCapacity) || (float)$loadCapacity <= 0) {
|
||||
$errors[] = 'Load capacity must be a positive number.';
|
||||
}
|
||||
|
||||
if ($countryId <= 0 || $cityId <= 0) {
|
||||
$errors[] = 'Please select country and city.';
|
||||
} else {
|
||||
$cityCheck = db()->prepare("SELECT COUNT(*) FROM cities WHERE id = ? AND country_id = ?");
|
||||
$cityCheck->execute([$cityId, $countryId]);
|
||||
if ((int)$cityCheck->fetchColumn() === 0) {
|
||||
$errors[] = 'Selected city does not belong to selected country.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
try {
|
||||
db()->beginTransaction();
|
||||
|
||||
$stmtUser = db()->prepare("UPDATE users SET full_name = ?, email = ?, status = ? WHERE id = ? AND role = 'truck_owner'");
|
||||
$stmtUser->execute([$fullName, $email, $status, $userId]);
|
||||
|
||||
$stmtProfile = db()->prepare("
|
||||
UPDATE truck_owner_profiles
|
||||
SET phone = ?, address_line = ?, country_id = ?, city_id = ?,
|
||||
truck_type = ?, load_capacity = ?, plate_no = ?
|
||||
WHERE user_id = ?
|
||||
");
|
||||
$stmtProfile->execute([$phone, $addressLine, $countryId, $cityId, $truckType, $loadCapacity, $plateNo, $userId]);
|
||||
|
||||
db()->commit();
|
||||
$flash = 'Truck Owner profile updated successfully.';
|
||||
|
||||
// Refresh data
|
||||
$owner['full_name'] = $fullName;
|
||||
$owner['email'] = $email;
|
||||
$owner['status'] = $status;
|
||||
$owner['phone'] = $phone;
|
||||
$owner['address_line'] = $addressLine;
|
||||
$owner['country_id'] = $countryId;
|
||||
$owner['city_id'] = $cityId;
|
||||
$owner['truck_type'] = $truckType;
|
||||
$owner['load_capacity'] = $loadCapacity;
|
||||
$owner['plate_no'] = $plateNo;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
db()->rollBack();
|
||||
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
$errors[] = 'This email is already in use by another account.';
|
||||
} else {
|
||||
$errors[] = 'Failed to update truck owner profile. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_header('Edit Truck Owner', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('truck_owners'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<a href="admin_truck_owners.php" class="text-decoration-none small text-muted mb-2 d-inline-block">← Back to Truck Owners</a>
|
||||
<h1 class="section-title mb-1">Edit Truck Owner</h1>
|
||||
<p class="muted mb-0">Update profile information for <?= e($owner['full_name']) ?>.</p>
|
||||
</div>
|
||||
</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('<br>', $errors)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel p-4">
|
||||
<form method="post">
|
||||
<h5 class="mb-3">Personal Details</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="full_name">Full Name</label>
|
||||
<input type="text" name="full_name" id="full_name" class="form-control" value="<?= e((string)$owner['full_name']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email" name="email" id="email" class="form-control" value="<?= e((string)$owner['email']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Phone</label>
|
||||
<input type="text" name="phone" id="phone" class="form-control" value="<?= e((string)$owner['phone']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="status">Account Status</label>
|
||||
<select name="status" id="status" class="form-select" required>
|
||||
<option value="pending" <?= $owner['status'] === 'pending' ? 'selected' : '' ?>>Pending</option>
|
||||
<option value="active" <?= $owner['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="rejected" <?= $owner['status'] === 'rejected' ? 'selected' : '' ?>>Rejected</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">Location</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="country_id">Country</label>
|
||||
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||
<option value="">Select country</option>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= e((string)$country['id']) ?>" <?= (string)$owner['country_id'] === (string)$country['id'] ? 'selected' : '' ?>>
|
||||
<?= e($lang === 'ar' && !empty($country['name_ar']) ? $country['name_ar'] : $country['name_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="city_id">City</label>
|
||||
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e((string)$owner['city_id']) ?>">
|
||||
<option value="">Select city</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="address_line">Address Line</label>
|
||||
<input type="text" name="address_line" id="address_line" class="form-control" value="<?= e((string)$owner['address_line']) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">Truck Details</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<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" value="<?= e((string)$owner['truck_type']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="load_capacity">Load Capacity (tons)</label>
|
||||
<input type="number" step="0.01" min="0.1" name="load_capacity" id="load_capacity" class="form-control" value="<?= e((string)$owner['load_capacity']) ?>" required>
|
||||
</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" value="<?= e((string)$owner['plate_no']) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="admin_truck_owners.php" class="btn btn-outline-dark ms-2">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
|
||||
function syncCities() {
|
||||
const countryId = document.getElementById('country_id').value;
|
||||
const citySelect = document.getElementById('city_id');
|
||||
const selectedValue = citySelect.dataset.selected || '';
|
||||
citySelect.innerHTML = '<option value="">Select city</option>';
|
||||
|
||||
allCities.forEach((city) => {
|
||||
if (String(city.country_id) !== String(countryId)) {
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = city.id;
|
||||
option.textContent = <?= $lang === 'ar' ? '(city.name_ar || city.name_en)' : '(city.name_en || city.name_ar)' ?>;
|
||||
if (String(city.id) === String(selectedValue)) {
|
||||
option.selected = true;
|
||||
}
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
// clear selected after first render
|
||||
citySelect.dataset.selected = '';
|
||||
}
|
||||
syncCities();
|
||||
</script>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
261
admin_truck_owners.php
Normal file
261
admin_truck_owners.php
Normal file
@ -0,0 +1,261 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
|
||||
$errors = [];
|
||||
$flash = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'], $_POST['user_id'])) {
|
||||
$userId = (int)$_POST['user_id'];
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'approve') {
|
||||
db()->prepare("UPDATE users SET status = 'active' WHERE id = ? AND role = 'truck_owner'")->execute([$userId]);
|
||||
$flash = 'Truck Owner approved successfully.';
|
||||
} elseif ($action === 'reject') {
|
||||
db()->prepare("UPDATE users SET status = 'rejected' WHERE id = ? AND role = 'truck_owner'")->execute([$userId]);
|
||||
$flash = 'Truck Owner rejected.';
|
||||
} elseif ($action === 'delete') {
|
||||
db()->prepare("DELETE FROM truck_owner_profiles WHERE user_id = ?")->execute([$userId]);
|
||||
db()->prepare("DELETE FROM users WHERE id = ? AND role = 'truck_owner'")->execute([$userId]);
|
||||
$flash = 'Truck Owner deleted.';
|
||||
}
|
||||
}
|
||||
|
||||
// Search and Pagination parameters
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$whereClause = "u.role = 'truck_owner'";
|
||||
$params = [];
|
||||
|
||||
if ($q !== '') {
|
||||
$whereClause .= " AND (u.full_name LIKE ? OR u.email LIKE ? OR p.plate_no LIKE ? OR p.truck_type LIKE ?)";
|
||||
$likeQ = "%$q%";
|
||||
$params = [$likeQ, $likeQ, $likeQ, $likeQ];
|
||||
}
|
||||
|
||||
// Total count
|
||||
$countSql = "
|
||||
SELECT COUNT(*)
|
||||
FROM users u
|
||||
LEFT JOIN truck_owner_profiles p ON u.id = p.user_id
|
||||
WHERE $whereClause
|
||||
";
|
||||
$stmt = db()->prepare($countSql);
|
||||
$stmt->execute($params);
|
||||
$total = (int)$stmt->fetchColumn();
|
||||
$totalPages = (int)ceil($total / $limit);
|
||||
|
||||
// Fetch truck owners
|
||||
$sql = "
|
||||
SELECT u.id, u.email, u.full_name, u.status, u.created_at,
|
||||
p.phone, p.truck_type, p.load_capacity, p.plate_no,
|
||||
p.id_card_path, p.truck_pic_path, p.registration_path,
|
||||
c.name_en AS country_name,
|
||||
ci.name_en AS city_name
|
||||
FROM users u
|
||||
LEFT JOIN truck_owner_profiles p ON u.id = p.user_id
|
||||
LEFT JOIN countries c ON p.country_id = c.id
|
||||
LEFT JOIN cities ci ON p.city_id = ci.id
|
||||
WHERE $whereClause
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
";
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$owners = $stmt->fetchAll();
|
||||
|
||||
render_header('Manage Truck Owners', 'admin');
|
||||
?>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-3">
|
||||
<?php render_admin_sidebar('truck_owners'); ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="page-intro d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<h1 class="section-title mb-1">Truck Owners</h1>
|
||||
<p class="muted mb-0">Review registrations and approve truck owners.</p>
|
||||
</div>
|
||||
|
||||
<form method="get" class="d-flex mt-3 mt-md-0 gap-2">
|
||||
<input type="text" name="q" class="form-control form-control-sm" placeholder="Search name, email, plate..." value="<?= e($q) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Search</button>
|
||||
<?php if ($q): ?>
|
||||
<a href="?q=" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel p-0">
|
||||
<?php if (!$owners && $q): ?>
|
||||
<div class="p-4"><p class="muted mb-0">No truck owners found matching your search.</p></div>
|
||||
<?php elseif (!$owners): ?>
|
||||
<div class="p-4"><p class="muted mb-0">No truck owners registered yet.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 align-middle table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-4">ID</th>
|
||||
<th>Name / Email</th>
|
||||
<th>Truck Info</th>
|
||||
<th>Documents</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end pe-4">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($owners as $owner): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><?= e((string)$owner['id']) ?></td>
|
||||
<td>
|
||||
<div class="fw-bold text-dark"><?= e($owner['full_name']) ?></div>
|
||||
<div class="text-muted small"><a href="mailto:<?= e($owner['email']) ?>" class="text-decoration-none"><?= e($owner['email']) ?></a></div>
|
||||
<div class="text-muted small"><?= e((string)$owner['phone']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><strong>Type:</strong> <?= e((string)$owner['truck_type']) ?></div>
|
||||
<div><strong>Cap:</strong> <?= e((string)$owner['load_capacity']) ?>t</div>
|
||||
<div><strong>Plate:</strong> <?= e((string)$owner['plate_no']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#docsModal<?= $owner['id'] ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||
</svg>
|
||||
View Docs
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($owner['status'] === 'active'): ?>
|
||||
<span class="badge bg-success-subtle text-success">Active</span>
|
||||
<?php elseif ($owner['status'] === 'pending'): ?>
|
||||
<span class="badge bg-warning-subtle text-warning">Pending</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger-subtle text-danger"><?= e(ucfirst($owner['status'] ?? 'unknown')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="d-inline-flex gap-1 align-items-center">
|
||||
<a href="admin_truck_owner_edit.php?id=<?= e((string)$owner[id]) ?>" class="btn btn-sm btn-light border text-primary" title="Edit Owner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
||||
</svg>
|
||||
</a>
|
||||
<form method="post" class="d-inline m-0 p-0">
|
||||
<input type="hidden" name="user_id" value="<?= e((string)$owner['id']) ?>">
|
||||
<?php if ($owner['status'] !== 'active'): ?>
|
||||
<button type="submit" name="action" value="approve" class="btn btn-sm btn-success" title="Approve">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16">
|
||||
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($owner['status'] !== 'rejected'): ?>
|
||||
<button type="submit" name="action" value="reject" class="btn btn-sm btn-warning" title="Reject">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="submit" name="action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete this truck owner forever?');" title="Delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="px-4 py-3 border-top d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted small">Showing <?= count($owners) ?> of <?= $total ?> truck owners</span>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $page - 1 ?>">Previous</a>
|
||||
</li>
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||
<a class="page-link" href="?q=<?= urlencode($q) ?>&page=<?= $page + 1 ?>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($owners as $owner): ?>
|
||||
<?php
|
||||
$idCards = json_decode($owner['id_card_path'] ?? '[]', true) ?: [];
|
||||
$regs = json_decode($owner['registration_path'] ?? '[]', true) ?: [];
|
||||
$pic = $owner['truck_pic_path'];
|
||||
?>
|
||||
<div class="modal fade" id="docsModal<?= $owner['id'] ?>" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Documents for <?= e($owner['full_name']) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>ID Card</h6>
|
||||
<div class="d-flex gap-2 mb-3 overflow-auto">
|
||||
<?php foreach ($idCards as $path): ?>
|
||||
<a href="<?= e('/' . $path) ?>" target="_blank">
|
||||
<img src="<?= e('/' . $path) ?>" alt="ID Card" class="img-thumbnail" style="max-height: 150px;">
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<h6>Truck Registration</h6>
|
||||
<div class="d-flex gap-2 mb-3 overflow-auto">
|
||||
<?php foreach ($regs as $path): ?>
|
||||
<a href="<?= e('/' . $path) ?>" target="_blank">
|
||||
<img src="<?= e('/' . $path) ?>" alt="Registration" class="img-thumbnail" style="max-height: 150px;">
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<h6>Truck Picture</h6>
|
||||
<?php if ($pic): ?>
|
||||
<div>
|
||||
<a href="<?= e('/' . $pic) ?>" target="_blank">
|
||||
<img src="<?= e('/' . $pic) ?>" alt="Truck Pic" class="img-thumbnail" style="max-height: 250px;">
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">No picture uploaded.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
@ -1,57 +1,110 @@
|
||||
:root {
|
||||
--bg: #f8fafc;
|
||||
--bg: #f4f7f6;
|
||||
--surface: #ffffff;
|
||||
--text: #0f172a;
|
||||
--text: #1e293b;
|
||||
--muted: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--primary: #0f172a;
|
||||
--accent: #2563eb;
|
||||
--success: #16a34a;
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--accent: #0ea5e9;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||
--shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
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%),
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.05), transparent 40%),
|
||||
radial-gradient(circle at bottom right, rgba(14, 165, 233, 0.05), transparent 40%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
backdrop-filter: blur(6px);
|
||||
background: rgba(255, 255, 255, 0.92) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.85) !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--primary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card,
|
||||
.panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
border-radius: 16px;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
position: relative;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
||||
border: 1px solid var(--border);
|
||||
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);
|
||||
.hero-img-container {
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
padding: 60px 40px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.motivation-box {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.motivation-box h3 {
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #eff6ff;
|
||||
color: var(--primary);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
background: var(--surface);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .fs-4 {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-status,
|
||||
@ -59,8 +112,8 @@ body.app-body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
padding: 6px 11px;
|
||||
font-size: 13px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
@ -68,49 +121,47 @@ body.app-body {
|
||||
|
||||
.badge-status.posted,
|
||||
.badge.posted {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
border-color: #cbd5e1;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.badge-status.offered,
|
||||
.badge.offered {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
border-color: #bfdbfe;
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
.badge-status.confirmed,
|
||||
.badge.confirmed {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
border-color: #bbf7d0;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.badge-status.in_transit,
|
||||
.badge.in_transit {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
border-color: #fde68a;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.badge-status.delivered,
|
||||
.badge.delivered {
|
||||
background: #ede9fe;
|
||||
color: #6d28d9;
|
||||
border-color: #ddd6fe;
|
||||
background: #f3e8ff;
|
||||
color: #9333ea;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 2px solid #f1f5f9;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
vertical-align: middle;
|
||||
padding: 16px 8px;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
@ -119,48 +170,62 @@ body.app-body {
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 0 0 0.2rem rgba(37, 99, 235, 0.12);
|
||||
background: white;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
color: #475569;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
background: #111827;
|
||||
border-color: #111827;
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
padding: 9px 14px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-outline-dark {
|
||||
border-color: var(--border);
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.btn-outline-dark:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.muted {
|
||||
@ -168,42 +233,35 @@ body.app-body {
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.page-intro {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
position: sticky;
|
||||
top: 88px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.admin-nav-link {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
color: #334155;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-nav-link:hover {
|
||||
background: #eff6ff;
|
||||
border-color: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.admin-nav-link.active {
|
||||
background: #dbeafe;
|
||||
background: #eff6ff;
|
||||
color: var(--primary);
|
||||
border-color: #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
[dir="rtl"] .navbar .ms-auto {
|
||||
@ -220,11 +278,16 @@ body.app-body {
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.hero-card {
|
||||
padding: 24px;
|
||||
.hero-content {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.hero-img-container {
|
||||
min-height: 250px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
assets/images/sample_id.jpg
Normal file
1
assets/images/sample_id.jpg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?><svg width='400' height='300' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' fill='#ddd'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='24' fill='#555'>Sample Document</text></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
1
assets/images/sample_reg.jpg
Normal file
1
assets/images/sample_reg.jpg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?><svg width='400' height='300' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' fill='#ddd'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='24' fill='#555'>Sample Document</text></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
1
assets/images/sample_truck.jpg
Normal file
1
assets/images/sample_truck.jpg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?><svg width='400' height='300' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' fill='#ddd'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='24' fill='#555'>Sample Document</text></svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@ -22,6 +22,8 @@ $translations = [
|
||||
'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',
|
||||
'register_shipper' => 'Register as Shipper',
|
||||
'register_owner' => 'Register as Truck Owner',
|
||||
'cta_shipper' => 'Post a shipment',
|
||||
'cta_owner' => 'Find loads',
|
||||
'cta_admin' => 'Open admin',
|
||||
@ -77,6 +79,7 @@ $translations = [
|
||||
'status_in_transit' => 'In transit',
|
||||
'status_delivered' => 'Delivered',
|
||||
'footer_note' => 'This is the initial MVP slice. Payments are not yet connected.',
|
||||
'marketing_title_1' => 'For Shippers', 'marketing_desc_1' => 'Find the right truck for your cargo quickly and securely. Post your load and get offers instantly.', 'marketing_title_2' => 'For Truck Owners', 'marketing_desc_2' => 'Maximize your earnings and eliminate empty miles. Browse available shipments and offer your rate.', 'motivation_phrase' => 'Empowering the logistics of tomorrow.', 'why_choose_us' => 'Why Choose CargoLink?', 'feature_1_title' => 'Fast Matching', 'feature_1_desc' => 'Connect with available trucks or shipments in minutes.', 'feature_2_title' => 'Secure Payments', 'feature_2_desc' => 'Your transactions are protected with security.', 'feature_3_title' => 'Verified Users', 'feature_3_desc' => 'We verify all truck owners to ensure peace of mind.',
|
||||
],
|
||||
'ar' => [
|
||||
'app_name' => 'CargoLink',
|
||||
@ -87,6 +90,8 @@ $translations = [
|
||||
'hero_title' => 'انقل شحنتك بسرعة مع شاحنات موثوقة.',
|
||||
'hero_subtitle' => 'أنشئ شحنة، استلم عروضاً، وادفع عبر ثواني أو التحويل البنكي.',
|
||||
'hero_tagline' => 'منصة لوجستية متعددة اللغات',
|
||||
'register_shipper' => 'التسجيل كشاحن',
|
||||
'register_owner' => 'التسجيل كمالك شاحنة',
|
||||
'cta_shipper' => 'إنشاء شحنة',
|
||||
'cta_owner' => 'البحث عن الشحنات',
|
||||
'cta_admin' => 'الدخول للإدارة',
|
||||
@ -142,6 +147,7 @@ $translations = [
|
||||
'status_in_transit' => 'قيد النقل',
|
||||
'status_delivered' => 'تم التسليم',
|
||||
'footer_note' => 'هذه هي النسخة الأولية. الدفع غير متصل بعد.',
|
||||
'marketing_title_1' => 'للشاحنين', 'marketing_desc_1' => 'ابحث عن الشاحنة المناسبة لحمولتك بسرعة وأمان.', 'marketing_title_2' => 'لأصحاب الشاحنات', 'marketing_desc_2' => 'عظّم أرباحك وتجنب العودة فارغاً.', 'motivation_phrase' => 'تمكين الخدمات اللوجستية للمستقبل.', 'why_choose_us' => 'لماذا تختار كارجو لينك؟', 'feature_1_title' => 'مطابقة سريعة', 'feature_1_desc' => 'تواصل مع الشاحنات المتاحة في دقائق.', 'feature_2_title' => 'مدفوعات آمنة', 'feature_2_desc' => 'معاملاتك محمية بأعلى معايير الأمان.', 'feature_3_title' => 'مستخدمون موثوقون', 'feature_3_desc' => 'نقوم بالتحقق من جميع أصحاب الشاحنات لضمان راحتك.',
|
||||
],
|
||||
];
|
||||
|
||||
@ -177,6 +183,53 @@ CREATE TABLE IF NOT EXISTS shipments (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
SQL;
|
||||
db()->exec($sql);
|
||||
try { db()->exec("ALTER TABLE users ADD COLUMN status ENUM('pending','active','rejected') NOT NULL DEFAULT 'active'"); } catch (Exception $e) {}
|
||||
|
||||
|
||||
db()->exec(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','shipper','truck_owner') NOT NULL,
|
||||
status ENUM('pending','active','rejected') NOT NULL DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
db()->exec(
|
||||
"CREATE TABLE IF NOT EXISTS shipper_profiles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE,
|
||||
company_name VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(40) NOT NULL,
|
||||
country_id INT NULL,
|
||||
city_id INT NULL,
|
||||
address_line VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_shipper_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
db()->exec(
|
||||
"CREATE TABLE IF NOT EXISTS truck_owner_profiles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE,
|
||||
phone VARCHAR(40) NOT NULL,
|
||||
country_id INT NULL,
|
||||
city_id INT NULL,
|
||||
address_line VARCHAR(255) NOT NULL,
|
||||
truck_type VARCHAR(120) NOT NULL,
|
||||
load_capacity DECIMAL(10,2) NOT NULL,
|
||||
plate_no VARCHAR(80) NOT NULL,
|
||||
id_card_path TEXT NOT NULL,
|
||||
truck_pic_path VARCHAR(255) NOT NULL,
|
||||
registration_path TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_owner_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
}
|
||||
|
||||
function set_flash(string $type, string $message): void
|
||||
|
||||
@ -28,55 +28,114 @@ function render_header(string $title, string $active = ''): void
|
||||
<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">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top shadow-sm py-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold" href="<?= e(url_with_lang('index.php')) ?>">
|
||||
<a class="navbar-brand fs-4" href="<?= e(url_with_lang('index.php')) ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-box-seam me-1 text-primary" viewBox="0 0 16 16">
|
||||
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2zm3.564 1.426L5.596 5 8 5.961 14.154 3.5zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.84L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"/>
|
||||
</svg>
|
||||
<?= e(t('app_name')) ?>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
|
||||
<button class="navbar-toggler border-0" 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">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 fw-semibold">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $active === 'home' ? 'active' : '' ?>" href="<?= e(url_with_lang('index.php')) ?>">
|
||||
<a class="nav-link <?= $active === 'home' ? 'active text-primary' : '' ?>" 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 class="d-flex align-items-center gap-3">
|
||||
<div class="dropdown">
|
||||
<a class="text-decoration-none text-muted fw-semibold dropdown-toggle" href="#" id="loginDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Demo Login
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0 mt-3" aria-labelledby="loginDropdown" style="border-radius: 12px;">
|
||||
<li>
|
||||
<a class="dropdown-item py-2 <?= $active === 'shipper' ? 'active' : '' ?>" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>">
|
||||
<?= e(t('nav_shipper')) ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item py-2 <?= $active === 'owner' ? 'active' : '' ?>" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>">
|
||||
<?= e(t('nav_owner')) ?>
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item py-2 <?= $active === 'admin' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">
|
||||
<?= e(t('nav_admin')) ?>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-sm btn-outline-dark rounded-pill px-3 fw-bold" href="<?= e(current_url_with_lang($toggleLang)) ?>">
|
||||
<?= e($toggleLabel) ?>
|
||||
</a>
|
||||
|
||||
<a class="btn btn-primary btn-sm rounded-pill px-4 fw-bold shadow-sm d-none d-lg-block" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container py-4">
|
||||
<main class="container py-5">
|
||||
<?php
|
||||
}
|
||||
|
||||
function render_footer(): void
|
||||
{
|
||||
global $lang;
|
||||
?>
|
||||
</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>
|
||||
<footer class="bg-white border-top py-5 mt-5">
|
||||
<div class="container">
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-box-seam me-2 text-primary" viewBox="0 0 16 16">
|
||||
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2zm3.564 1.426L5.596 5 8 5.961 14.154 3.5zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.84L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"/>
|
||||
</svg>
|
||||
<h5 class="fw-bold mb-0 text-dark"><?= e(t('app_name')) ?></h5>
|
||||
</div>
|
||||
<p class="text-muted small pe-md-4">
|
||||
<?= e(t('motivation_phrase')) ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-2 offset-md-2">
|
||||
<h6 class="fw-bold mb-3">Company</h6>
|
||||
<ul class="list-unstyled text-muted small">
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">About Us</a></li>
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">Careers</a></li>
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="fw-bold mb-3">Resources</h6>
|
||||
<ul class="list-unstyled text-muted small">
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">Help Center</a></li>
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">Terms of Service</a></li>
|
||||
<li class="mb-2"><a href="#" class="text-decoration-none text-muted">Privacy Policy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="fw-bold mb-3">Language</h6>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= e(current_url_with_lang('en')) ?>" class="text-decoration-none <?= $lang === 'en' ? 'text-primary fw-bold' : 'text-muted' ?>">EN</a>
|
||||
<span class="text-muted">|</span>
|
||||
<a href="<?= e(current_url_with_lang('ar')) ?>" class="text-decoration-none <?= $lang === 'ar' ? 'text-primary fw-bold' : 'text-muted' ?>">AR</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center pt-4 border-top small text-muted">
|
||||
<span>© <?= date('Y') ?> <?= e(t('app_name')) ?>. All rights reserved.</span>
|
||||
<span class="mt-2 mt-md-0"><?= e(t('footer_note')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@ -90,13 +149,16 @@ 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')],
|
||||
'countries' => ['label' => 'Countries', 'href' => url_with_lang('admin_countries.php')],
|
||||
'cities' => ['label' => 'Cities', 'href' => url_with_lang('admin_cities.php')],
|
||||
'shippers' => ['label' => 'Shippers', 'href' => url_with_lang('admin_shippers.php')],
|
||||
'truck_owners' => ['label' => 'Truck Owners', 'href' => url_with_lang('admin_truck_owners.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">
|
||||
<aside class="admin-sidebar panel p-4 shadow-sm border-0">
|
||||
<h2 class="h5 fw-bold mb-4">Admin Panel</h2>
|
||||
<nav class="nav flex-column gap-2">
|
||||
<?php foreach ($items as $key => $item): ?>
|
||||
<a class="admin-nav-link <?= $active === $key ? 'active' : '' ?>" href="<?= e($item['href']) ?>">
|
||||
<?= e($item['label']) ?>
|
||||
@ -105,4 +167,4 @@ function render_admin_sidebar(string $active = 'dashboard'): void
|
||||
</nav>
|
||||
</aside>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
258
index.php
258
index.php
@ -31,36 +31,142 @@ try {
|
||||
render_header(t('app_name'), 'home');
|
||||
?>
|
||||
|
||||
<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 class="hero-section mb-5">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6 d-flex flex-column justify-content-center">
|
||||
<div class="hero-content">
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary mb-3 px-3 py-2 fs-6 rounded-pill">
|
||||
<?= e(t('hero_tagline')) ?>
|
||||
</span>
|
||||
<h1 class="display-5 fw-bold mb-4" style="line-height: 1.2;">
|
||||
<?= e(t('hero_title')) ?>
|
||||
</h1>
|
||||
<p class="fs-5 text-muted mb-5">
|
||||
<?= e(t('hero_subtitle')) ?>
|
||||
</p>
|
||||
<div class="d-flex flex-column flex-sm-row gap-3">
|
||||
<a class="btn btn-primary btn-lg shadow-sm" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-box-seam me-2" viewBox="0 0 16 16">
|
||||
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2zm3.564 1.426L5.596 5 8 5.961 14.154 3.5zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.84L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z"/>
|
||||
</svg>
|
||||
<?= e(t('register_shipper')) ?>
|
||||
</a>
|
||||
<a class="btn btn-outline-primary btn-lg bg-white shadow-sm" href="<?= e(url_with_lang('register.php', ['role' => 'truck_owner'])) ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-truck me-2" viewBox="0 0 16 16">
|
||||
<path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h9A1.5 1.5 0 0 1 12 3.5V5h1.02a1.5 1.5 0 0 1 1.17.563l1.481 1.85a1.5 1.5 0 0 1 .329.938V10.5a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 1 1-4 0H5a2 2 0 1 1-3.998-.085A1.5 1.5 0 0 1 0 10.5v-7zm1.294 7.456A1.999 1.999 0 0 1 4.732 11h5.536a2.01 2.01 0 0 1 .732-.732V3.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .294.456zM12 10a2 2 0 0 1 1.732 1h.768a.5.5 0 0 0 .5-.5V8.35a.5.5 0 0 0-.11-.312l-1.48-1.85A.5.5 0 0 0 13.02 6H12v4zm-9 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm9 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
</svg>
|
||||
<?= e(t('register_owner')) ?>
|
||||
</a>
|
||||
</div>
|
||||
</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 class="col-lg-6">
|
||||
<div class="hero-img-container" style="background-image: url('https://picsum.photos/id/1073/800/600');"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-5 g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="fs-4"><?= e($stats['shipments']) ?>+</div>
|
||||
<div class="text-muted fw-semibold mt-1"><?= e(t('stats_shipments')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="fs-4"><?= e($stats['offers']) ?>+</div>
|
||||
<div class="text-muted fw-semibold mt-1"><?= e(t('stats_offers')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="fs-4"><?= e($stats['confirmed']) ?>+</div>
|
||||
<div class="text-muted fw-semibold mt-1"><?= e(t('stats_confirmed')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="mb-5">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="display-6 fw-bold mb-3"><?= e(t('why_choose_us')) ?></h2>
|
||||
<p class="text-muted fs-5 mx-auto" style="max-width: 600px;"><?= e(t('motivation_phrase')) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="panel p-4 h-100 text-center border-0 shadow-sm">
|
||||
<div class="feature-icon mx-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-lightning-charge" viewBox="0 0 16 16">
|
||||
<path d="M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09zM4.157 8.5H7a.5.5 0 0 1 .478.647L6.11 13.59l5.732-6.09H9a.5.5 0 0 1-.478-.647L9.89 2.41 4.157 8.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3"><?= e(t('feature_1_title')) ?></h4>
|
||||
<p class="text-muted mb-0"><?= e(t('feature_1_desc')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel p-4 h-100 text-center border-0 shadow-sm">
|
||||
<div class="feature-icon mx-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-shield-check" viewBox="0 0 16 16">
|
||||
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
|
||||
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3"><?= e(t('feature_2_title')) ?></h4>
|
||||
<p class="text-muted mb-0"><?= e(t('feature_2_desc')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="panel p-4 h-100 text-center border-0 shadow-sm">
|
||||
<div class="feature-icon mx-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-people" viewBox="0 0 16 16">
|
||||
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="fw-bold mb-3"><?= e(t('feature_3_title')) ?></h4>
|
||||
<p class="text-muted mb-0"><?= e(t('feature_3_desc')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-5">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-md-6 order-md-2">
|
||||
<img src="https://picsum.photos/id/1015/600/400" alt="Logistics" class="img-fluid rounded-4 shadow-sm" style="width: 100%; object-fit: cover; height: 350px;">
|
||||
</div>
|
||||
<div class="col-md-6 order-md-1">
|
||||
<div class="p-lg-4">
|
||||
<h2 class="display-6 fw-bold mb-4"><?= e(t('section_workflow')) ?></h2>
|
||||
|
||||
<div class="d-flex mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center fs-5 fw-bold" style="width: 45px; height: 45px;">1</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<h5 class="fw-bold mb-1"><?= e(t('step_post')) ?></h5>
|
||||
<p class="text-muted mb-0"><?= e(t('marketing_desc_1')) ?></p>
|
||||
</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 class="d-flex mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center fs-5 fw-bold" style="width: 45px; height: 45px;">2</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<h5 class="fw-bold mb-1"><?= e(t('step_offer')) ?></h5>
|
||||
<p class="text-muted mb-0"><?= e(t('marketing_desc_2')) ?></p>
|
||||
</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 class="d-flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center fs-5 fw-bold" style="width: 45px; height: 45px;">3</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<h5 class="fw-bold mb-1"><?= e(t('step_confirm')) ?></h5>
|
||||
<p class="text-muted mb-0">Secure booking and track the delivery until completion.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,67 +174,55 @@ render_header(t('app_name'), 'home');
|
||||
</div>
|
||||
</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">
|
||||
<?php if ($recentShipments): ?>
|
||||
<section class="panel p-4 mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<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>
|
||||
<a class="btn btn-outline-primary btn-sm rounded-pill px-3" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>"><?= 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>
|
||||
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentShipments as $row): ?>
|
||||
<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>
|
||||
<td class="fw-semibold"><?= e($row['shipper_company']) ?></td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-geo-alt me-2 text-muted" viewBox="0 0 16 16"><path d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A31.493 31.493 0 0 1 8 14.58a31.481 31.481 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94zM8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10z"/><path d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></svg>
|
||||
<?= e($row['origin_city']) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-geo-alt-fill me-2 text-primary" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg>
|
||||
<?= e($row['destination_city']) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge-status <?= e($row['status']) ?>"><?= e(status_label($row['status'])) ?></span></td>
|
||||
</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; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
<div class="motivation-box mb-5">
|
||||
<h3 class="display-6 fw-bold mb-3">Ready to transform your logistics?</h3>
|
||||
<p class="fs-5 text-white-50 mb-4 mx-auto" style="max-width: 600px;">Join our platform today to find reliable trucks or secure the best shipments in the market.</p>
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||
<a class="btn btn-light btn-lg px-4 text-primary fw-bold" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>"><?= e(t('register_shipper')) ?></a>
|
||||
<a class="btn btn-outline-light btn-lg px-4 fw-bold" href="<?= e(url_with_lang('register.php', ['role' => 'truck_owner'])) ?>"><?= e(t('register_owner')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
314
register.php
314
register.php
@ -2,50 +2,124 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
ensure_schema();
|
||||
|
||||
$errors = [];
|
||||
$saved = false;
|
||||
$saved_role = '';
|
||||
$values = [
|
||||
'role' => $_GET['role'] ?? 'shipper',
|
||||
'full_name' => '',
|
||||
'email' => '',
|
||||
'phone' => '',
|
||||
'country_id' => '',
|
||||
'city_id' => '',
|
||||
'address_line' => '',
|
||||
'company_name' => '',
|
||||
'truck_type' => '',
|
||||
'load_capacity' => '',
|
||||
'plate_no' => '',
|
||||
];
|
||||
|
||||
$countries = db()->query("SELECT id, name_en, name_ar FROM countries ORDER BY name_en ASC")->fetchAll();
|
||||
$cities = db()->query("SELECT id, country_id, name_en, name_ar FROM cities ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$role = $_POST['role'] ?? 'shipper';
|
||||
$fullName = trim($_POST['full_name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$countryId = (int)($_POST['country_id'] ?? 0);
|
||||
$cityId = (int)($_POST['city_id'] ?? 0);
|
||||
$addressLine = trim($_POST['address_line'] ?? '');
|
||||
$companyName = trim($_POST['company_name'] ?? '');
|
||||
$passwordRaw = (string)($_POST['password'] ?? '');
|
||||
|
||||
$values = [
|
||||
'role' => $role,
|
||||
'full_name' => $fullName,
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'country_id' => $countryId > 0 ? (string)$countryId : '',
|
||||
'city_id' => $cityId > 0 ? (string)$cityId : '',
|
||||
'address_line' => $addressLine,
|
||||
'company_name' => $companyName,
|
||||
'truck_type' => trim($_POST['truck_type'] ?? ''),
|
||||
'load_capacity' => trim($_POST['load_capacity'] ?? ''),
|
||||
'plate_no' => trim($_POST['plate_no'] ?? ''),
|
||||
];
|
||||
|
||||
if (!in_array($role, ['shipper', 'truck_owner'], true)) {
|
||||
$errors[] = 'Invalid role selected.';
|
||||
}
|
||||
if ($fullName === '') {
|
||||
$errors[] = 'Full name is required.';
|
||||
}
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = 'Please provide a valid email address.';
|
||||
}
|
||||
if ($phone === '') {
|
||||
$errors[] = 'Phone number is required.';
|
||||
}
|
||||
if ($countryId <= 0 || $cityId <= 0) {
|
||||
$errors[] = 'Please select country and city.';
|
||||
} else {
|
||||
$cityCheck = db()->prepare("SELECT COUNT(*) FROM cities WHERE id = ? AND country_id = ?");
|
||||
$cityCheck->execute([$cityId, $countryId]);
|
||||
if ((int)$cityCheck->fetchColumn() === 0) {
|
||||
$errors[] = 'Selected city does not belong to selected country.';
|
||||
}
|
||||
}
|
||||
if ($addressLine === '') {
|
||||
$errors[] = 'Address is required.';
|
||||
}
|
||||
if (strlen($passwordRaw) < 6) {
|
||||
$errors[] = 'Password must be at least 6 characters.';
|
||||
}
|
||||
if ($role === 'shipper' && $companyName === '') {
|
||||
$errors[] = 'Company name is required for shipper registration.';
|
||||
}
|
||||
|
||||
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();
|
||||
$pdo = db();
|
||||
|
||||
if ($role === 'truck_owner') {
|
||||
$truckType = trim($_POST['truck_type'] ?? '');
|
||||
$loadCapacity = trim($_POST['load_capacity'] ?? '');
|
||||
$plateNo = trim($_POST['plate_no'] ?? '');
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($truckType === '' || $loadCapacity === '' || $plateNo === '') {
|
||||
$errors[] = 'Please complete truck details.';
|
||||
} elseif (!is_numeric($loadCapacity)) {
|
||||
$errors[] = 'Load capacity must be numeric.';
|
||||
}
|
||||
$status = ($role === 'truck_owner') ? 'pending' : 'active';
|
||||
$stmt = $pdo->prepare("INSERT INTO users (email, password, full_name, role, status) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$email, $password, $fullName, $role, $status]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
|
||||
if ($role === 'shipper') {
|
||||
$shipperStmt = $pdo->prepare(
|
||||
"INSERT INTO shipper_profiles (user_id, company_name, phone, country_id, city_id, address_line)
|
||||
VALUES (?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$shipperStmt->execute([$userId, $companyName, $phone, $countryId, $cityId, $addressLine]);
|
||||
} else {
|
||||
$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) || (float)$loadCapacity <= 0) {
|
||||
$errors[] = 'Load capacity must be numeric and greater than zero.';
|
||||
}
|
||||
|
||||
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 {
|
||||
$maxSize = 8 * 1024 * 1024;
|
||||
$saveImage = static function (string $tmpName, int $size, string $prefix) use ($uploadDir, $allowed, $maxSize): ?string {
|
||||
if ($size <= 0 || $size > $maxSize) {
|
||||
return null;
|
||||
}
|
||||
$mime = mime_content_type($tmpName) ?: '';
|
||||
if (!isset($allowed[$mime])) {
|
||||
return null;
|
||||
@ -59,42 +133,47 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
};
|
||||
|
||||
$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;
|
||||
}
|
||||
if (is_uploaded_file($_FILES['id_card_front']['tmp_name'] ?? '')) {
|
||||
$path = $saveImage($_FILES['id_card_front']['tmp_name'], (int)$_FILES['id_card_front']['size'], 'id_front_');
|
||||
if ($path) $idCardPaths[] = $path;
|
||||
}
|
||||
if (is_uploaded_file($_FILES['id_card_back']['tmp_name'] ?? '')) {
|
||||
$path = $saveImage($_FILES['id_card_back']['tmp_name'], (int)$_FILES['id_card_back']['size'], 'id_back_');
|
||||
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;
|
||||
}
|
||||
if (is_uploaded_file($_FILES['truck_reg_front']['tmp_name'] ?? '')) {
|
||||
$path = $saveImage($_FILES['truck_reg_front']['tmp_name'], (int)$_FILES['truck_reg_front']['size'], 'reg_front_');
|
||||
if ($path) $regPaths[] = $path;
|
||||
}
|
||||
if (is_uploaded_file($_FILES['truck_reg_back']['tmp_name'] ?? '')) {
|
||||
$path = $saveImage($_FILES['truck_reg_back']['tmp_name'], (int)$_FILES['truck_reg_back']['size'], 'reg_back_');
|
||||
if ($path) $regPaths[] = $path;
|
||||
}
|
||||
|
||||
$truckPic = null;
|
||||
$truckTmp = $_FILES['truck_picture']['tmp_name'] ?? '';
|
||||
if (is_uploaded_file($truckTmp)) {
|
||||
$truckPic = $saveImage($truckTmp, 'truck_');
|
||||
$truckSize = (int)($_FILES['truck_picture']['size'] ?? 0);
|
||||
$truckPic = $saveImage($truckTmp, $truckSize, '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 (?, ?, ?, ?, ?, ?, ?)"
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$ownerStmt = $pdo->prepare(
|
||||
"INSERT INTO truck_owner_profiles (user_id, phone, country_id, city_id, address_line, truck_type, load_capacity, plate_no, id_card_path, truck_pic_path, registration_path)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
$profileStmt->execute([
|
||||
$ownerStmt->execute([
|
||||
$userId,
|
||||
$phone,
|
||||
$countryId,
|
||||
$cityId,
|
||||
$addressLine,
|
||||
$truckType,
|
||||
$loadCapacity,
|
||||
$plateNo,
|
||||
@ -104,25 +183,55 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$saved = true;
|
||||
if ($errors) {
|
||||
$pdo->rollBack();
|
||||
} else {
|
||||
$pdo->commit();
|
||||
$saved = true;
|
||||
$saved_role = $role;
|
||||
$values = [
|
||||
'role' => $_GET['role'] ?? 'shipper',
|
||||
'full_name' => '',
|
||||
'email' => '',
|
||||
'phone' => '',
|
||||
'country_id' => '',
|
||||
'city_id' => '',
|
||||
'address_line' => '',
|
||||
'company_name' => '',
|
||||
'truck_type' => '',
|
||||
'load_capacity' => '',
|
||||
'plate_no' => '',
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
$errors[] = 'This email is already registered.';
|
||||
} else {
|
||||
$errors[] = 'Registration failed. Please try again.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_header('Register Account');
|
||||
render_header('Shipper & Truck Owner Registration');
|
||||
?>
|
||||
|
||||
<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>
|
||||
<h1 class="section-title mb-1">Create your logistics account</h1>
|
||||
<p class="muted mb-0">Shippers and truck owners can self-register with full profile details.</p>
|
||||
</div>
|
||||
|
||||
<div class="panel p-4">
|
||||
<?php if ($saved): ?>
|
||||
<div class="alert alert-success">Registration completed successfully.</div>
|
||||
<?php if ($saved_role === 'truck_owner'): ?>
|
||||
<div class="alert alert-success">Registration completed successfully. Your account is pending admin approval.</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-success">Registration completed successfully.</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($errors): ?>
|
||||
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
|
||||
@ -130,21 +239,60 @@ render_header('Register Account');
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="regForm" novalidate>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<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>
|
||||
<option value="shipper" <?= $values['role'] === 'shipper' ? 'selected' : '' ?>>Shipper</option>
|
||||
<option value="truck_owner" <?= $values['role'] === 'truck_owner' ? 'selected' : '' ?>>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 class="col-md-3">
|
||||
<label class="form-label" for="full_name">Full name</label>
|
||||
<input type="text" name="full_name" id="full_name" class="form-control" value="<?= e($values['full_name']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email" name="email" id="email" class="form-control" value="<?= e($values['email']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control" minlength="6" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="phone">Phone</label>
|
||||
<input type="text" name="phone" id="phone" class="form-control" value="<?= e($values['phone']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="country_id">Country</label>
|
||||
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||
<option value="">Select country</option>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= e((string)$country['id']) ?>" <?= $values['country_id'] === (string)$country['id'] ? 'selected' : '' ?>>
|
||||
<?= e($lang === 'ar' && !empty($country['name_ar']) ? $country['name_ar'] : $country['name_en']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="city_id">City</label>
|
||||
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e($values['city_id']) ?>">
|
||||
<option value="">Select city</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="address_line">Address</label>
|
||||
<input type="text" name="address_line" id="address_line" class="form-control" value="<?= e($values['address_line']) ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="shipperFields" class="mt-4">
|
||||
<h2 class="h5 mb-3">Shipper details</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="company_name">Company name</label>
|
||||
<input type="text" name="company_name" id="company_name" class="form-control" value="<?= e($values['company_name']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="truckFields" class="mt-4" style="display:none;">
|
||||
@ -152,28 +300,37 @@ render_header('Register Account');
|
||||
<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">
|
||||
<input type="text" name="truck_type" id="truck_type" class="form-control" value="<?= e($values['truck_type']) ?>">
|
||||
</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">
|
||||
<input type="number" name="load_capacity" id="load_capacity" class="form-control" step="0.01" min="0.1" value="<?= e($values['load_capacity']) ?>">
|
||||
</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">
|
||||
<input type="text" name="plate_no" id="plate_no" class="form-control" value="<?= e($values['plate_no']) ?>">
|
||||
</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 class="col-md-6 mt-3">
|
||||
<label class="form-label" for="id_card_front">ID card (Front Face)</label>
|
||||
<input type="file" name="id_card_front" id="id_card_front" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="truck_picture">Clear truck photo</label>
|
||||
<div class="col-md-6 mt-3">
|
||||
<label class="form-label" for="id_card_back">ID card (Back Face)</label>
|
||||
<input type="file" name="id_card_back" id="id_card_back" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<label class="form-label" for="truck_reg_front">Truck Registration (Front Face)</label>
|
||||
<input type="file" name="truck_reg_front" id="truck_reg_front" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<label class="form-label" for="truck_reg_back">Truck Registration (Back Face)</label>
|
||||
<input type="file" name="truck_reg_back" id="truck_reg_back" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||
</div>
|
||||
<div class="col-md-12 mt-3">
|
||||
<label class="form-label" for="truck_picture">Clear Truck Photo (showing plate number)</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>
|
||||
|
||||
@ -185,16 +342,45 @@ render_header('Register Account');
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
|
||||
function syncCities() {
|
||||
const countryId = document.getElementById('country_id').value;
|
||||
const citySelect = document.getElementById('city_id');
|
||||
const selectedValue = citySelect.dataset.selected || '';
|
||||
citySelect.innerHTML = '<option value="">Select city</option>';
|
||||
|
||||
allCities.forEach((city) => {
|
||||
if (String(city.country_id) !== String(countryId)) {
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = city.id;
|
||||
option.textContent = <?= $lang === 'ar' ? '(city.name_ar || city.name_en)' : '(city.name_en || city.name_ar)' ?>;
|
||||
if (String(city.id) === String(selectedValue)) {
|
||||
option.selected = true;
|
||||
}
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.dataset.selected = '';
|
||||
}
|
||||
|
||||
function toggleFields() {
|
||||
const role = document.getElementById('role').value;
|
||||
const truckFields = document.getElementById('truckFields');
|
||||
const shipperFields = document.getElementById('shipperFields');
|
||||
const companyInput = document.getElementById('company_name');
|
||||
const isOwner = role === 'truck_owner';
|
||||
shipperFields.style.display = isOwner ? 'none' : 'block';
|
||||
truckFields.style.display = isOwner ? 'block' : 'none';
|
||||
truckFields.querySelectorAll('input').forEach((input) => {
|
||||
|
||||
companyInput.required = !isOwner;
|
||||
truckFields.querySelectorAll('input[type="text"], input[type="number"]').forEach((input) => {
|
||||
input.required = isOwner;
|
||||
});
|
||||
}
|
||||
syncCities();
|
||||
toggleFields();
|
||||
</script>
|
||||
|
||||
<?php render_footer(); ?>
|
||||
<?php render_footer(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user