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.

+
+ + +
+ + +
+ + +
+

Add city

+
+
+ + +
+
+ + +
+
+ + +
+
+ + Go to countries +
+
+
+ +
+

Cities list

+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + Cancel +
+
+ + + +

No cities added yet.

+ +
+ + + + + + + + + + + + + + + + + + + + + +
IDCountryCity (EN)City (AR)Action
+ 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.

+
+ + +
+ + +
+ + +
+

Add country

+
+
+ + +
+
+ + +
+
+ + Go to cities +
+
+
+ +
+

Countries list

+ + +
+ +
+ + +
+
+ + +
+
+ + Cancel +
+
+ + + +

No countries added yet.

+ +
+ + + + + + + + + + + + + + + + + + + +
IDCountry (EN)Country (AR)Action
+ 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');

Quick actions

- Manage Locations + Manage Countries + Manage Cities Register New User
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.

-
- - -
- - -
- - -
-
-
-

Add country

-
-
- - -
-
- - -
- -
-
-
-
-
-

Add city

-
-
- - -
-
- - -
-
- - -
- -
-
-
-
- -
-
-

Recently added cities

- Back to admin -
- -

No cities added yet.

- -
- - - - - - - - - - - - - - - -
CountryCity
-
- -
-
-
- - +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 .

+
+
+ + +
+ + +
', $errors)) ?>
+ + +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + Cancel +
+
+
+
+
+
+ + + + \ 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.

+
+ +
+ + + + Clear + +
+
+ + +
+ + +
+ +

No shippers found matching your search.

+ +

No shippers registered yet.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
IDName / CompanyContactLocationStatusAction
+
+
+
+
+
+
+ , + + + Active + + Pending + + + + +
+ + + + + +
+ + + + + + + + +
+
+
+
+ + 1): ?> +
+ Showing of 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'); +?> + +
+
+ +
+
+
+
+ ← Back to Truck Owners +

Edit Truck Owner

+

Update profile information for .

+
+
+ + +
+ + +
', $errors)) ?>
+ + +
+
+
Personal Details
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
Location
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
Truck Details
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + Cancel +
+
+
+
+
+ + + + \ 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.

+
+ +
+ + + + Clear + +
+
+ + +
+ + +
+ +

No truck owners found matching your search.

+ +

No truck owners registered yet.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
IDName / EmailTruck InfoDocumentsStatusAction
+
+
+
+
+
Type:
+
Cap: t
+
Plate:
+
+ + + + Active + + Pending + + + + +
+ + + + + +
+ + + + + + + + +
+
+
+
+ + 1): ?> +
+ Showing of truck owners + +
+ + +
+
+
+ + + + + + + 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 @@ +Sample Document 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 @@ +Sample Document 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 @@ +Sample Document 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 -