diff --git a/admin_cities.php b/admin_cities.php
new file mode 100644
index 0000000..017b2a7
--- /dev/null
+++ b/admin_cities.php
@@ -0,0 +1,226 @@
+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');
+?>
+
+
+
+
+
+
+
+
Cities
+
Manage cities and map each city to its country.
+
+
+
+
= e($flash) ?>
+
+
+
= e(implode(' ', $errors)) ?>
+
+
+
+
+
+
Cities list
+
+
+
+
+
+
+
No cities added yet.
+
+
+
+
+
+ | ID |
+ Country |
+ City (EN) |
+ City (AR) |
+ Action |
+
+
+
+
+
+ | = e((string)$city['id']) ?> |
+ = e($city['country_name']) ?> |
+ = e($city['name_en']) ?> |
+ = e((string)($city['name_ar'] ?? '-')) ?> |
+
+ Edit
+
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin_countries.php b/admin_countries.php
new file mode 100644
index 0000000..1597c7e
--- /dev/null
+++ b/admin_countries.php
@@ -0,0 +1,183 @@
+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');
+?>
+
+
+
+
+
+
+
+
Countries
+
Manage the list of allowed countries for shipment routes.
+
+
+
+
= e($flash) ?>
+
+
+
= e(implode(' ', $errors)) ?>
+
+
+
+
+
+
Countries list
+
+
+
+
+
+
+
No countries added yet.
+
+
+
+
+
+ | ID |
+ Country (EN) |
+ Country (AR) |
+ Action |
+
+
+
+
+
+ | = e((string)$country['id']) ?> |
+ = e($country['name_en']) ?> |
+ = e((string)($country['name_ar'] ?? '-')) ?> |
+
+ Edit
+
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin_dashboard.php b/admin_dashboard.php
index cc9a9a1..ecb188d 100644
--- a/admin_dashboard.php
+++ b/admin_dashboard.php
@@ -48,7 +48,8 @@ render_header(t('admin_dashboard'), 'admin');
diff --git a/admin_manage_locations.php b/admin_manage_locations.php
index b6b9354..e57f73e 100644
--- a/admin_manage_locations.php
+++ b/admin_manage_locations.php
@@ -1,177 +1,7 @@
exec("
-CREATE TABLE IF NOT EXISTS countries (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name_en VARCHAR(255) NOT NULL,
- name_ar VARCHAR(255) DEFAULT NULL
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-");
-
-db()->exec("
-CREATE TABLE IF NOT EXISTS cities (
- id INT AUTO_INCREMENT PRIMARY KEY,
- country_id INT NOT NULL,
- name_en VARCHAR(255) NOT NULL,
- name_ar VARCHAR(255) DEFAULT NULL,
- UNIQUE KEY uniq_city_country (country_id, name_en),
- CONSTRAINT fk_cities_country FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-");
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (isset($_POST['add_country'])) {
- $countryNameEn = trim($_POST['country_name_en'] ?? '');
- $countryNameAr = trim($_POST['country_name_ar'] ?? '');
- if ($countryNameEn === '') {
- $errors[] = 'Country name (English) is required.';
- } else {
- try {
- $stmt = db()->prepare("INSERT INTO countries (name_en, name_ar) VALUES (?, ?)");
- $stmt->execute([$countryNameEn, $countryNameAr !== '' ? $countryNameAr : null]);
- $flash = 'Country added.';
- } catch (Throwable $e) {
- $errors[] = 'Country already exists or could not be saved.';
- }
- }
- } elseif (isset($_POST['add_city'])) {
- $countryId = (int)($_POST['country_id'] ?? 0);
- $cityNameEn = trim($_POST['city_name_en'] ?? '');
- $cityNameAr = trim($_POST['city_name_ar'] ?? '');
- if ($countryId <= 0 || $cityNameEn === '') {
- $errors[] = 'Please select a country and provide city name (English).';
- } else {
- try {
- $stmt = db()->prepare("INSERT INTO cities (country_id, name_en, name_ar) VALUES (?, ?, ?)");
- $stmt->execute([$countryId, $cityNameEn, $cityNameAr !== '' ? $cityNameAr : null]);
- $flash = 'City added.';
- } catch (Throwable $e) {
- $errors[] = 'City already exists or could not be saved.';
- }
- }
- }
-}
-
-$countryNameExpr = $lang === 'ar'
- ? "COALESCE(NULLIF(co.name_ar, ''), co.name_en)"
- : "COALESCE(NULLIF(co.name_en, ''), co.name_ar)";
-$countryNameExprNoAlias = $lang === 'ar'
- ? "COALESCE(NULLIF(name_ar, ''), name_en)"
- : "COALESCE(NULLIF(name_en, ''), name_ar)";
-$cityNameExpr = $lang === 'ar'
- ? "COALESCE(NULLIF(c.name_ar, ''), c.name_en)"
- : "COALESCE(NULLIF(c.name_en, ''), c.name_ar)";
-
-$countries = db()->query("SELECT id, {$countryNameExprNoAlias} AS display_name FROM countries ORDER BY display_name ASC")->fetchAll();
-$cities = db()->query(
- "SELECT
- {$countryNameExpr} AS country_name,
- {$cityNameExpr} AS city_name
- FROM cities c
- JOIN countries co ON co.id = c.country_id
- ORDER BY country_name ASC, city_name ASC
- LIMIT 30"
-)->fetchAll();
-
-render_header('Manage Locations', 'admin');
-?>
-
-
-
-
-
-
-
-
Country & city setup
-
Define allowed origin and destination options for shipments.
-
-
-
-
= e($flash) ?>
-
-
-
= e(implode(' ', $errors)) ?>
-
-
-
-
-
-
-
Recently added cities
-
Back to admin
-
-
-
No cities added yet.
-
-
-
-
-
- | Country |
- City |
-
-
-
-
-
- | = e($city['country_name']) ?> |
- = e($city['city_name']) ?> |
-
-
-
-
-
-
-
-
-
-
-
+header('Location: ' . url_with_lang('admin_countries.php'), true, 302);
+exit;
diff --git a/admin_shipper_edit.php b/admin_shipper_edit.php
new file mode 100644
index 0000000..6c4a376
--- /dev/null
+++ b/admin_shipper_edit.php
@@ -0,0 +1,210 @@
+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');
+?>
+
+
+
+
+
+
+
+
+
← Back to Shippers
+
Edit Shipper
+
Update profile information for = e($shipper['full_name']) ?>.
+
+
+
+
+
= e($flash) ?>
+
+
+
= e(implode('
', $errors)) ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin_shippers.php b/admin_shippers.php
new file mode 100644
index 0000000..e39ccb8
--- /dev/null
+++ b/admin_shippers.php
@@ -0,0 +1,202 @@
+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');
+?>
+
+
+
+
+
+
+
+
+
Shippers
+
Manage registered shippers.
+
+
+
+
+
+
+
= e($flash) ?>
+
+
+
+
+
No shippers found matching your search.
+
+
No shippers registered yet.
+
+
+
+
+
+ | ID |
+ Name / Company |
+ Contact |
+ Location |
+ Status |
+ Action |
+
+
+
+
+
+ | = e((string)$shipper['id']) ?> |
+
+ = e($shipper['full_name']) ?>
+ = e((string)$shipper['company_name']) ?>
+ |
+
+
+ = e((string)$shipper['phone']) ?>
+ |
+
+ = e((string)$shipper['city_name']) ?>, = e((string)$shipper['country_name']) ?>
+ |
+
+
+ Active
+
+ Pending
+
+ = e(ucfirst($shipper['status'] ?? 'unknown')) ?>
+
+ |
+
+
+ |
+
+
+
+
+
+
+ 1): ?>
+
+ Showing = count($shippers) ?> of = $total ?> shippers
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin_truck_owner_edit.php b/admin_truck_owner_edit.php
new file mode 100644
index 0000000..0fa8b14
--- /dev/null
+++ b/admin_truck_owner_edit.php
@@ -0,0 +1,236 @@
+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');
+?>
+
+
+
+
+
+
+
+
+
+
= e($flash) ?>
+
+
+
= e(implode('
', $errors)) ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin_truck_owners.php b/admin_truck_owners.php
new file mode 100644
index 0000000..5c6201d
--- /dev/null
+++ b/admin_truck_owners.php
@@ -0,0 +1,261 @@
+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');
+?>
+
+
+
+
+
+
+
+
+
Truck Owners
+
Review registrations and approve truck owners.
+
+
+
+
+
+
+
= e($flash) ?>
+
+
+
+
+
No truck owners found matching your search.
+
+
No truck owners registered yet.
+
+
+
+
+
+ | ID |
+ Name / Email |
+ Truck Info |
+ Documents |
+ Status |
+ Action |
+
+
+
+
+
+ | = e((string)$owner['id']) ?> |
+
+ = e($owner['full_name']) ?>
+
+ = e((string)$owner['phone']) ?>
+ |
+
+ Type: = e((string)$owner['truck_type']) ?>
+ Cap: = e((string)$owner['load_capacity']) ?>t
+ Plate: = e((string)$owner['plate_no']) ?>
+ |
+
+
+ |
+
+
+ Active
+
+ Pending
+
+ = e(ucfirst($owner['status'] ?? 'unknown')) ?>
+
+ |
+
+
+ |
+
+
+
+
+
+
+ 1): ?>
+
+ Showing = count($owners) ?> of = $total ?> truck owners
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ID Card
+
+
+
Truck Registration
+
+
+
Truck Picture
+
+
+
+
No picture uploaded.
+
+
+
+
+
+
+
+
+
diff --git a/assets/css/custom.css b/assets/css/custom.css
index fe6168d..0fa6bd7 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -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;
}
-}
+}
\ No newline at end of file
diff --git a/assets/images/sample_id.jpg b/assets/images/sample_id.jpg
new file mode 100644
index 0000000..a0e1827
--- /dev/null
+++ b/assets/images/sample_id.jpg
@@ -0,0 +1 @@
+
diff --git a/assets/images/sample_reg.jpg b/assets/images/sample_reg.jpg
new file mode 100644
index 0000000..a0e1827
--- /dev/null
+++ b/assets/images/sample_reg.jpg
@@ -0,0 +1 @@
+
diff --git a/assets/images/sample_truck.jpg b/assets/images/sample_truck.jpg
new file mode 100644
index 0000000..a0e1827
--- /dev/null
+++ b/assets/images/sample_truck.jpg
@@ -0,0 +1 @@
+
diff --git a/includes/app.php b/includes/app.php
index fdb703c..21f7776 100644
--- a/includes/app.php
+++ b/includes/app.php
@@ -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
diff --git a/includes/layout.php b/includes/layout.php
index 433c551..a83b3a9 100644
--- a/includes/layout.php
+++ b/includes/layout.php
@@ -28,55 +28,114 @@ function render_header(string $title, string $active = ''): void
-