Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c05193a14 | ||
|
|
4167b44e71 | ||
|
|
16cb6e8b9f | ||
|
|
bcfce1b10b | ||
|
|
4804c27495 | ||
|
|
c51c984fbc | ||
|
|
c1349af904 | ||
|
|
d1d4a0cc48 | ||
|
|
60195fb655 | ||
|
|
06bba2398c | ||
|
|
e163ce11cd | ||
|
|
e67be138ad | ||
|
|
b587241984 | ||
|
|
e222bcb8eb | ||
|
|
02c4e3f13f | ||
|
|
56f2aad3d0 | ||
|
|
414499c9c9 | ||
|
|
e2a20e34a0 | ||
|
|
b41902be07 | ||
|
|
31cd804b2c | ||
|
|
b21d7e9d88 | ||
|
|
a361ed7269 | ||
|
|
486d54348f | ||
|
|
aebf7d0667 | ||
|
|
f2a3c9ce49 | ||
|
|
7fefc460c2 | ||
|
|
3f3ac7d889 | ||
|
|
a3d27b0ee3 | ||
|
|
863bd1cca9 | ||
|
|
0ebb8e142e | ||
|
|
2ee38edf60 | ||
|
|
eeb8d0977c | ||
|
|
4b01726905 | ||
|
|
031d73aec7 | ||
|
|
f510b10571 | ||
|
|
acca4d8e6f | ||
|
|
0d73d6ab31 | ||
|
|
d9e4e98300 | ||
|
|
3e575f9e76 | ||
|
|
3ef2b853c6 | ||
|
|
d424fc2360 | ||
|
|
022932f8fe | ||
|
|
2d3486ba46 | ||
|
|
29654f3470 | ||
|
|
c17c11d23a | ||
|
|
6485678cb7 | ||
|
|
1173fcbe16 | ||
|
|
890b723e72 | ||
|
|
9e5fb665e5 | ||
|
|
890ce0acd2 | ||
|
|
8bc413c26a | ||
|
|
41b182bfba |
1
.session_id
Normal file
@ -0,0 +1 @@
|
|||||||
|
jbuvnmdmjhe702uecv5o643pb7
|
||||||
256
admin_cities.php
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$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') { validate_csrf_token();
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$total = (int)db()->query("SELECT COUNT(*) FROM cities")->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
$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 $limit OFFSET $offset"
|
||||||
|
)->fetchAll();
|
||||||
|
|
||||||
|
$editingCity = null;
|
||||||
|
if ($editCityId > 0) {
|
||||||
|
// Fetch explicitly if editing, as it might not be on the current page
|
||||||
|
$stmt = db()->prepare("SELECT * FROM cities WHERE id = ?");
|
||||||
|
$stmt->execute([$editCityId]);
|
||||||
|
$editingCity = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Manage Cities', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('cities'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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"> <?= csrf_field() ?>
|
||||||
|
<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"> <?= csrf_field() ?>
|
||||||
|
<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 p-1 border-0 bg-transparent text-primary" href="<?= e(url_with_lang('admin_cities.php', ['edit_city' => (int)$city['id']])) ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline m-0 p-0" onsubmit="return confirm('Delete this city?');"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="city_id" value="<?= e((string)$city['id']) ?>">
|
||||||
|
<button type="submit" name="delete_city" class="btn btn-sm p-1 border-0 bg-transparent text-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalPages > 1): ?>
|
||||||
|
<div class="pt-3 border-top d-flex justify-content-between align-items-center">
|
||||||
|
<span class="text-muted small">Showing <?= count($cities) ?> of <?= $total ?> cities</span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?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="?page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
274
admin_company_profile.php
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$companyName = trim($_POST['company_name'] ?? '');
|
||||||
|
$companyEmail = trim($_POST['company_email'] ?? '');
|
||||||
|
$companyPhone = trim($_POST['company_phone'] ?? '');
|
||||||
|
$companyAddress = trim($_POST['company_address'] ?? '');
|
||||||
|
$platformCharge = trim($_POST['platform_charge_percentage'] ?? '0');
|
||||||
|
$pricingModel = trim($_POST['pricing_model'] ?? 'percentage');
|
||||||
|
$timezone = trim($_POST['timezone'] ?? 'UTC');
|
||||||
|
|
||||||
|
$updates = [
|
||||||
|
'company_name' => $companyName,
|
||||||
|
'company_email' => $companyEmail,
|
||||||
|
'company_phone' => $companyPhone,
|
||||||
|
'company_address' => $companyAddress,
|
||||||
|
'platform_charge_percentage' => $platformCharge,
|
||||||
|
'pricing_model' => $pricingModel,
|
||||||
|
'timezone' => $timezone,
|
||||||
|
'terms_en' => trim($_POST['terms_en'] ?? ''),
|
||||||
|
'terms_ar' => trim($_POST['terms_ar'] ?? ''),
|
||||||
|
'privacy_en' => trim($_POST['privacy_en'] ?? ''),
|
||||||
|
'privacy_ar' => trim($_POST['privacy_ar'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle file uploads
|
||||||
|
$uploadDir = __DIR__ . '/uploads/logos/';
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_FILES['logo_file']) && $_FILES['logo_file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$tmpName = $_FILES['logo_file']['tmp_name'];
|
||||||
|
$ext = strtolower(pathinfo($_FILES['logo_file']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
||||||
|
if (in_array($ext, $allowedExt, true)) {
|
||||||
|
$logoName = 'logo_' . time() . '.' . $ext;
|
||||||
|
$dest = $uploadDir . $logoName;
|
||||||
|
if (move_uploaded_file($tmpName, $dest)) {
|
||||||
|
$updates['logo_path'] = '/uploads/logos/' . $logoName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = t('invalid_logo_format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_FILES['favicon_file']) && $_FILES['favicon_file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$tmpName = $_FILES['favicon_file']['tmp_name'];
|
||||||
|
$ext = strtolower(pathinfo($_FILES['favicon_file']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowedExt = ['ico', 'png', 'svg', 'gif'];
|
||||||
|
if (in_array($ext, $allowedExt, true)) {
|
||||||
|
$faviconName = 'favicon_' . time() . '.' . $ext;
|
||||||
|
$dest = $uploadDir . $faviconName;
|
||||||
|
if (move_uploaded_file($tmpName, $dest)) {
|
||||||
|
$updates['favicon_path'] = '/uploads/logos/' . $faviconName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = t('invalid_favicon_format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$pdo = db();
|
||||||
|
foreach ($updates as $key => $val) {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:k, :v) ON DUPLICATE KEY UPDATE setting_value = :v2");
|
||||||
|
$stmt->execute([':k' => $key, ':v' => $val, ':v2' => $val]);
|
||||||
|
}
|
||||||
|
$success = t('company_profile_updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current settings
|
||||||
|
$settings = get_settings();
|
||||||
|
$currentName = $settings['company_name'] ?? t('app_name');
|
||||||
|
$currentEmail = $settings['company_email'] ?? '';
|
||||||
|
$currentPhone = $settings['company_phone'] ?? '';
|
||||||
|
$currentAddress = $settings['company_address'] ?? '';
|
||||||
|
$currentPlatformCharge = $settings['platform_charge_percentage'] ?? '0';
|
||||||
|
$currentPricingModel = $settings['pricing_model'] ?? 'percentage';
|
||||||
|
$currentTimezone = $settings['timezone'] ?? 'UTC';
|
||||||
|
$currentLogo = $settings['logo_path'] ?? '';
|
||||||
|
$currentFavicon = $settings['favicon_path'] ?? '';
|
||||||
|
$currentTermsEn = $settings['terms_en'] ?? '';
|
||||||
|
$currentTermsAr = $settings['terms_ar'] ?? '';
|
||||||
|
$currentPrivacyEn = $settings['privacy_en'] ?? '';
|
||||||
|
$currentPrivacyAr = $settings['privacy_ar'] ?? '';
|
||||||
|
|
||||||
|
render_header(t('company_profile'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('company_profile'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="section-title mb-1"><?= t('company_profile') ?></h1>
|
||||||
|
<p class="muted mb-0"><?= t('company_profile_intro') ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?= e($success) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel p-4">
|
||||||
|
<form method="post" enctype="multipart/form-data"> <?= csrf_field() ?>
|
||||||
|
<ul class="nav nav-tabs mb-4" id="companySettingsTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="company-tab" data-bs-toggle="tab" data-bs-target="#company" type="button" role="tab" aria-controls="company" aria-selected="true">
|
||||||
|
<i class="bi bi-building me-2"></i><?= t('tab_company_setting') ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="legal-tab" data-bs-toggle="tab" data-bs-target="#legal" type="button" role="tab" aria-controls="legal" aria-selected="false">
|
||||||
|
<i class="bi bi-file-earmark-text me-2"></i><?= t('tab_legal_policies') ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="privacy-tab" data-bs-toggle="tab" data-bs-target="#privacy" type="button" role="tab" aria-controls="privacy" aria-selected="false">
|
||||||
|
<i class="bi bi-shield-lock me-2"></i><?= t('tab_privacy_policy') ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="companySettingsTabContent">
|
||||||
|
<!-- Tab 1: Company Setting -->
|
||||||
|
<div class="tab-pane fade show active" id="company" role="tabpanel" aria-labelledby="company-tab">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('company_app_name') ?></label>
|
||||||
|
<input type="text" name="company_name" class="form-control" value="<?= e($currentName) ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('contact_email') ?></label>
|
||||||
|
<input type="email" name="company_email" class="form-control" value="<?= e($currentEmail) ?>">
|
||||||
|
<div class="form-text"><?= t('displayed_in_footer') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('contact_phone') ?></label>
|
||||||
|
<input type="text" name="company_phone" class="form-control" value="<?= e($currentPhone) ?>">
|
||||||
|
<div class="form-text"><?= t('displayed_in_footer') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('address') ?></label>
|
||||||
|
<input type="text" name="company_address" class="form-control" value="<?= e($currentAddress) ?>">
|
||||||
|
<div class="form-text"><?= t('displayed_in_footer') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-bold"><?= t('pricing_model') ?></label>
|
||||||
|
<div class="card p-3 bg-light border-0">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="radio" name="pricing_model" id="model_percentage" value="percentage" <?= $currentPricingModel === 'percentage' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="model_percentage">
|
||||||
|
<strong><?= t('pricing_percentage_model') ?></strong> <?= t('default_label') ?><br>
|
||||||
|
<span class="text-muted small"><?= t('pricing_percentage_desc') ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="pricing_model" id="model_fixed" value="fixed_price" <?= $currentPricingModel === 'fixed_price' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="model_fixed">
|
||||||
|
<strong><?= t('pricing_fixed_model') ?></strong><br>
|
||||||
|
<span class="text-muted small"><?= t('pricing_fixed_desc') ?></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('platform_charge_percent') ?></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" step="0.01" min="0" max="100" name="platform_charge_percentage" class="form-control" value="<?= e($currentPlatformCharge) ?>">
|
||||||
|
<span class="input-group-text">%</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text"><?= t('platform_charge_help') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('timezone')) ?></label>
|
||||||
|
<select name="timezone" class="form-select">
|
||||||
|
<?php foreach (DateTimeZone::listIdentifiers() as $tz): ?>
|
||||||
|
<option value="<?= e($tz) ?>" <?= $tz === $currentTimezone ? 'selected' : '' ?>>
|
||||||
|
<?= e($tz) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<div class="form-text"><?= t('system_timezone') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<hr class="my-2">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('company_logo') ?></label>
|
||||||
|
<?php if ($currentLogo): ?>
|
||||||
|
<div class="mb-2">
|
||||||
|
<img src="<?= e($currentLogo) ?>" alt="Logo" height="40" class="border rounded p-1">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<input type="file" name="logo_file" class="form-control" accept="image/*">
|
||||||
|
<div class="form-text"><?= t('logo_help') ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('favicon') ?></label>
|
||||||
|
<?php if ($currentFavicon): ?>
|
||||||
|
<div class="mb-2">
|
||||||
|
<img src="<?= e($currentFavicon) ?>" alt="Favicon" height="32" class="border rounded p-1">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<input type="file" name="favicon_file" class="form-control" accept="image/png, image/x-icon, image/svg+xml">
|
||||||
|
<div class="form-text"><?= t('favicon_help') ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 2: Legal & Policies -->
|
||||||
|
<div class="tab-pane fade" id="legal" role="tabpanel" aria-labelledby="legal-tab">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="fw-bold mb-3"><?= t('terms_of_service') ?></h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('english_label') ?></label>
|
||||||
|
<textarea name="terms_en" class="form-control" rows="10" placeholder="<?= t('terms_placeholder_en') ?>"><?= e($currentTermsEn) ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('arabic_label') ?></label>
|
||||||
|
<textarea name="terms_ar" class="form-control" rows="10" dir="rtl" placeholder="<?= t('terms_placeholder_ar') ?>"><?= e($currentTermsAr) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 3: Privacy Policy -->
|
||||||
|
<div class="tab-pane fade" id="privacy" role="tabpanel" aria-labelledby="privacy-tab">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5 class="fw-bold mb-3"><?= t('tab_privacy_policy') ?></h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('english_label') ?></label>
|
||||||
|
<textarea name="privacy_en" class="form-control" rows="10" placeholder="<?= t('privacy_placeholder_en') ?>"><?= e($currentPrivacyEn) ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= t('arabic_label') ?></label>
|
||||||
|
<textarea name="privacy_ar" class="form-control" rows="10" dir="rtl" placeholder="<?= t('privacy_placeholder_ar') ?>"><?= e($currentPrivacyAr) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
<button type="submit" class="btn btn-primary px-4"><?= t('save_changes') ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
213
admin_countries.php
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$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') { validate_csrf_token();
|
||||||
|
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)";
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$total = (int)db()->query("SELECT COUNT(*) FROM countries")->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
$countries = db()->query("SELECT id, name_en, name_ar, {$countryNameExprNoAlias} AS display_name FROM countries ORDER BY display_name ASC LIMIT $limit OFFSET $offset")->fetchAll();
|
||||||
|
|
||||||
|
$editingCountry = null;
|
||||||
|
if ($editCountryId > 0) {
|
||||||
|
// Fetch explicitly if editing
|
||||||
|
$stmt = db()->prepare("SELECT id, name_en, name_ar, {$countryNameExprNoAlias} AS display_name FROM countries WHERE id = ?");
|
||||||
|
$stmt->execute([$editCountryId]);
|
||||||
|
$editingCountry = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Manage Countries', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('countries'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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"> <?= csrf_field() ?>
|
||||||
|
<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"> <?= csrf_field() ?>
|
||||||
|
<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 p-1 border-0 bg-transparent text-primary" href="<?= e(url_with_lang('admin_countries.php', ['edit_country' => (int)$country['id']])) ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline m-0 p-0" onsubmit="return confirm('Delete this country and its cities?');"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="country_id" value="<?= e((string)$country['id']) ?>">
|
||||||
|
<button type="submit" name="delete_country" class="btn btn-sm p-1 border-0 bg-transparent text-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($totalPages > 1): ?>
|
||||||
|
<div class="pt-3 border-top d-flex justify-content-between align-items-center">
|
||||||
|
<span class="text-muted small">Showing <?= count($countries) ?> of <?= $total ?> countries</span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?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="?page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
327
admin_dashboard.php
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$shipments = [];
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 10");
|
||||||
|
$shipments = $stmt->fetchAll();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$shipments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total_shipments' => 0,
|
||||||
|
'active_shipments' => 0,
|
||||||
|
'total_shippers' => 0,
|
||||||
|
'total_truck_owners' => 0,
|
||||||
|
'total_revenue' => 0.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$chartData = [
|
||||||
|
'labels' => [],
|
||||||
|
'data' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stats['total_shipments'] = (int)$pdo->query("SELECT COUNT(*) FROM shipments")->fetchColumn();
|
||||||
|
$stats['active_shipments'] = (int)$pdo->query("SELECT COUNT(*) FROM shipments WHERE status != 'delivered'")->fetchColumn();
|
||||||
|
$stats['total_shippers'] = (int)$pdo->query("SELECT COUNT(*) FROM users WHERE role = 'shipper'")->fetchColumn();
|
||||||
|
$stats['total_truck_owners'] = (int)$pdo->query("SELECT COUNT(*) FROM users WHERE role = 'truck_owner'")->fetchColumn();
|
||||||
|
$stats['total_revenue'] = (float)$pdo->query("SELECT SUM(total_price) FROM shipments WHERE payment_status = 'paid'")->fetchColumn();
|
||||||
|
|
||||||
|
// Chart Data: Last 30 days
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT DATE(created_at) as date, COUNT(*) as count
|
||||||
|
FROM shipments
|
||||||
|
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||||
|
GROUP BY DATE(created_at)
|
||||||
|
ORDER BY date ASC
|
||||||
|
");
|
||||||
|
$dailyStats = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// Fill in missing days
|
||||||
|
$period = new DatePeriod(
|
||||||
|
new DateTime('-30 days'),
|
||||||
|
new DateInterval('P1D'),
|
||||||
|
new DateTime('+1 day')
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($period as $date) {
|
||||||
|
$d = $date->format('Y-m-d');
|
||||||
|
$chartData['labels'][] = $date->format('d M');
|
||||||
|
$chartData['data'][] = $dailyStats[$d] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Silent fail for stats, defaults are 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash = get_flash();
|
||||||
|
|
||||||
|
render_header(t('admin_dashboard'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('dashboard'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('admin_dashboard')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('overview_performance')) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-box-seam" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-primary mb-2 position-relative"><i class="bi bi-box-seam fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['total_shipments'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('total_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-truck" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-warning mb-2 position-relative"><i class="bi bi-truck fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['active_shipments'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('active_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-people" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-success mb-2 position-relative"><i class="bi bi-people fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['total_shippers'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('total_shippers')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-person-vcard" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-info mb-2 position-relative"><i class="bi bi-person-vcard fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['total_truck_owners'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('total_truck_owners')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-currency-dollar" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-success mb-2 position-relative"><i class="bi bi-currency-dollar fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= format_currency($stats['total_revenue']) ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('total_revenue')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Main Content: Shipments Chart -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Chart Section -->
|
||||||
|
<div class="panel shadow-sm border-0 rounded-4 mb-4">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2 class="h5 mb-0 fw-bold text-white"><i class="bi bi-graph-up text-white-50 me-2"></i><?= e(t('shipments_analytics')) ?></h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<canvas id="shipmentsChart" style="max-height: 300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel shadow-sm border-0 rounded-4 d-flex flex-column">
|
||||||
|
<div class="panel-heading d-flex justify-content-between align-items-center">
|
||||||
|
<h2 class="h5 mb-0 fw-bold text-white"><i class="bi bi-clock-history text-white-50 me-2"></i><?= e(t('recent_shipments')) ?></h2>
|
||||||
|
<a href="<?= e(url_with_lang('admin_shipments.php')) ?>" class="btn btn-sm btn-light text-primary"><?= e(t('view_all')) ?></a>
|
||||||
|
</div>
|
||||||
|
<div class="p-0 flex-grow-1 d-flex flex-column">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success m-3" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-warning m-3"><?= e(implode(' ', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!$shipments): ?>
|
||||||
|
<div class="text-center p-5 text-muted flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
<i class="bi bi-inbox fs-1 mb-3 d-block opacity-50"></i>
|
||||||
|
<p class="mb-0"><?= e(t('no_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive flex-grow-1">
|
||||||
|
<table class="table align-middle mb-0 table-hover">
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4 text-uppercase small text-muted border-top-0"><?= e(t('shipment')) ?></th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0"><?= e(t('route')) ?></th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0"><?= e(t('status')) ?></th>
|
||||||
|
<th class="pe-4 text-uppercase small text-muted border-top-0 text-end"><?= e(t('action')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($shipments as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4">
|
||||||
|
<div class="fw-bold text-dark"><?= e($row['shipper_company']) ?></div>
|
||||||
|
<small class="text-muted"><?= e($row['payment_method'] === 'bank_transfer' ? t('payment_bank') : t('payment_thawani')) ?></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="fw-medium text-dark"><?= e($row['origin_city']) ?></span>
|
||||||
|
<i class="bi bi-arrow-right text-muted small"></i>
|
||||||
|
<span class="fw-medium text-dark"><?= e($row['destination_city']) ?></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge <?= e($row['status']) ?> rounded-pill px-3 py-2"><?= e(status_label($row['status'])) ?></span></td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<a href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>" class="btn btn-sm btn-light text-primary" title="<?= e(t('view_details')) ?>">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar: Quick Links List -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="panel shadow-sm border-0 h-100 rounded-4" style="background-color: #fafbfc;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2 class="h5 mb-0 fw-bold text-white"><i class="bi bi-lightning-charge text-warning me-2"></i><?= e(t('quick_links')) ?></h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="list-group list-group-flush bg-transparent">
|
||||||
|
<a href="<?= e(url_with_lang('admin_countries.php')) ?>" class="list-group-item list-group-item-action bg-transparent border-bottom d-flex align-items-center py-3 px-0">
|
||||||
|
<div class="bg-white rounded p-3 shadow-sm me-3 text-primary d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-globe2 fs-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e(t('manage_countries')) ?></h6>
|
||||||
|
<small class="text-muted d-block line-height-sm"><?= e(t('add_remove_countries')) ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
|
||||||
|
</a>
|
||||||
|
<a href="<?= e(url_with_lang('admin_cities.php')) ?>" class="list-group-item list-group-item-action bg-transparent border-bottom d-flex align-items-center py-3 px-0">
|
||||||
|
<div class="bg-white rounded p-3 shadow-sm me-3 text-primary d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-pin-map fs-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e(t('manage_cities')) ?></h6>
|
||||||
|
<small class="text-muted d-block line-height-sm"><?= e(t('configure_cities')) ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
|
||||||
|
</a>
|
||||||
|
<a href="<?= e(url_with_lang('register.php')) ?>" class="list-group-item list-group-item-action bg-transparent border-bottom d-flex align-items-center py-3 px-0">
|
||||||
|
<div class="bg-white rounded p-3 shadow-sm me-3 text-success d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-person-plus fs-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e(t('register_user')) ?></h6>
|
||||||
|
<small class="text-muted d-block line-height-sm"><?= e(t('manually_onboard')) ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
|
||||||
|
</a>
|
||||||
|
<a href="<?= e(url_with_lang('admin_company_profile.php')) ?>" class="list-group-item list-group-item-action bg-transparent border-bottom d-flex align-items-center py-3 px-0">
|
||||||
|
<div class="bg-white rounded p-3 shadow-sm me-3 text-secondary d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-buildings fs-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e(t('company_profile')) ?></h6>
|
||||||
|
<small class="text-muted d-block line-height-sm"><?= e(t('update_branding')) ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
|
||||||
|
</a>
|
||||||
|
<a href="<?= e(url_with_lang('admin_reports_summary.php')) ?>" class="list-group-item list-group-item-action bg-transparent d-flex align-items-center py-3 px-0">
|
||||||
|
<div class="bg-white rounded p-3 shadow-sm me-3 text-warning d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-bar-chart fs-5"></i></div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e(t('summary_report')) ?></h6>
|
||||||
|
<small class="text-muted d-block line-height-sm"><?= e(t('view_analytics')) ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const ctx = document.getElementById('shipmentsChart').getContext('2d');
|
||||||
|
const chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: <?= json_encode($chartData['labels']) ?>,
|
||||||
|
datasets: [{
|
||||||
|
label: '<?= t('shipments') ?>',
|
||||||
|
data: <?= json_encode($chartData['data']) ?>,
|
||||||
|
borderColor: '#0d6efd',
|
||||||
|
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
pointBackgroundColor: '#ffffff',
|
||||||
|
pointBorderColor: '#0d6efd',
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
titleColor: '#000',
|
||||||
|
bodyColor: '#666',
|
||||||
|
borderColor: '#eee',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 10,
|
||||||
|
displayColors: false,
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.parsed.y + ' Shipments';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: '#f0f0f0',
|
||||||
|
drawBorder: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
font: {
|
||||||
|
size: 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
font: {
|
||||||
|
size: 11
|
||||||
|
},
|
||||||
|
maxTicksLimit: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
281
admin_faqs.php
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = null;
|
||||||
|
$editFaqId = isset($_GET['edit_faq']) ? (int)$_GET['edit_faq'] : 0;
|
||||||
|
|
||||||
|
db()->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS faqs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
question_en VARCHAR(255) NOT NULL,
|
||||||
|
answer_en TEXT NOT NULL,
|
||||||
|
question_ar VARCHAR(255) DEFAULT NULL,
|
||||||
|
answer_ar TEXT DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
if (isset($_POST['add_faq'])) {
|
||||||
|
$questionEn = trim($_POST['question_en'] ?? '');
|
||||||
|
$answerEn = trim($_POST['answer_en'] ?? '');
|
||||||
|
$questionAr = trim($_POST['question_ar'] ?? '');
|
||||||
|
$answerAr = trim($_POST['answer_ar'] ?? '');
|
||||||
|
$sortOrder = (int)($_POST['sort_order'] ?? 0);
|
||||||
|
|
||||||
|
if ($questionEn === '' || $answerEn === '') {
|
||||||
|
$errors[] = 'Question and Answer (English) are required.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("INSERT INTO faqs (question_en, answer_en, question_ar, answer_ar, sort_order) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$questionEn,
|
||||||
|
$answerEn,
|
||||||
|
$questionAr !== '' ? $questionAr : null,
|
||||||
|
$answerAr !== '' ? $answerAr : null,
|
||||||
|
$sortOrder
|
||||||
|
]);
|
||||||
|
$flash = 'FAQ added successfully.';
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errors[] = 'Failed to add FAQ: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['update_faq'])) {
|
||||||
|
$faqId = (int)($_POST['faq_id'] ?? 0);
|
||||||
|
$questionEn = trim($_POST['question_en'] ?? '');
|
||||||
|
$answerEn = trim($_POST['answer_en'] ?? '');
|
||||||
|
$questionAr = trim($_POST['question_ar'] ?? '');
|
||||||
|
$answerAr = trim($_POST['answer_ar'] ?? '');
|
||||||
|
$sortOrder = (int)($_POST['sort_order'] ?? 0);
|
||||||
|
|
||||||
|
if ($faqId <= 0 || $questionEn === '' || $answerEn === '') {
|
||||||
|
$errors[] = 'FAQ ID, Question, and Answer (English) are required.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("UPDATE faqs SET question_en = ?, answer_en = ?, question_ar = ?, answer_ar = ?, sort_order = ? WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$questionEn,
|
||||||
|
$answerEn,
|
||||||
|
$questionAr !== '' ? $questionAr : null,
|
||||||
|
$answerAr !== '' ? $answerAr : null,
|
||||||
|
$sortOrder,
|
||||||
|
$faqId
|
||||||
|
]);
|
||||||
|
$flash = 'FAQ updated successfully.';
|
||||||
|
$editFaqId = 0;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errors[] = 'Failed to update FAQ: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['delete_faq'])) {
|
||||||
|
$faqId = (int)($_POST['faq_id'] ?? 0);
|
||||||
|
if ($faqId <= 0) {
|
||||||
|
$errors[] = 'Invalid FAQ selected.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("DELETE FROM faqs WHERE id = ?");
|
||||||
|
$stmt->execute([$faqId]);
|
||||||
|
$flash = 'FAQ deleted successfully.';
|
||||||
|
$editFaqId = 0;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errors[] = 'Failed to delete FAQ: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$total = (int)db()->query("SELECT COUNT(*) FROM faqs")->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
$faqs = db()->query("SELECT * FROM faqs ORDER BY sort_order ASC, id DESC LIMIT $limit OFFSET $offset")->fetchAll();
|
||||||
|
|
||||||
|
$editingFaq = null;
|
||||||
|
if ($editFaqId > 0) {
|
||||||
|
// Fetch specifically if editing
|
||||||
|
$stmt = db()->prepare("SELECT * FROM faqs WHERE id = ?");
|
||||||
|
$stmt->execute([$editFaqId]);
|
||||||
|
$editingFaq = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Manage FAQs', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('faqs'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="h3 mb-1">Frequently Asked Questions</h1>
|
||||||
|
<p class="text-muted mb-0">Manage the Q&A list displayed on the public FAQ page.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<?= e($flash) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<?= e(implode(' ', $errors)) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($editingFaq): ?>
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Edit FAQ</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" class="row g-3"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="faq_id" value="<?= e((string)$editingFaq['id']) ?>">
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="edit_question_en">Question (EN) <span class="text-danger">*</span></label>
|
||||||
|
<input id="edit_question_en" type="text" name="question_en" class="form-control" value="<?= e($editingFaq['question_en']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="edit_question_ar">Question (AR)</label>
|
||||||
|
<input id="edit_question_ar" type="text" name="question_ar" class="form-control" value="<?= e((string)($editingFaq['question_ar'] ?? '')) ?>" dir="auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="edit_answer_en">Answer (EN) <span class="text-danger">*</span></label>
|
||||||
|
<textarea id="edit_answer_en" name="answer_en" class="form-control" rows="4" required><?= e($editingFaq['answer_en']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="edit_answer_ar">Answer (AR)</label>
|
||||||
|
<textarea id="edit_answer_ar" name="answer_ar" class="form-control" rows="4" dir="auto"><?= e((string)($editingFaq['answer_ar'] ?? '')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="edit_sort_order">Sort Order</label>
|
||||||
|
<input id="edit_sort_order" type="number" name="sort_order" class="form-control" value="<?= e((string)$editingFaq['sort_order']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<button type="submit" name="update_faq" class="btn btn-primary px-4 me-2">Save Changes</button>
|
||||||
|
<a class="btn btn-outline-secondary px-4" href="<?= e(url_with_lang('admin_faqs.php')) ?>">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Add New FAQ</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" class="row g-3"> <?= csrf_field() ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="question_en">Question (EN) <span class="text-danger">*</span></label>
|
||||||
|
<input id="question_en" type="text" name="question_en" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="question_ar">Question (AR)</label>
|
||||||
|
<input id="question_ar" type="text" name="question_ar" class="form-control" dir="auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="answer_en">Answer (EN) <span class="text-danger">*</span></label>
|
||||||
|
<textarea id="answer_en" name="answer_en" class="form-control" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="answer_ar">Answer (AR)</label>
|
||||||
|
<textarea id="answer_ar" name="answer_ar" class="form-control" rows="3" dir="auto"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="sort_order">Sort Order (Optional)</label>
|
||||||
|
<input id="sort_order" type="number" name="sort_order" class="form-control" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<button type="submit" name="add_faq" class="btn btn-primary px-4"><i class="bi bi-plus-circle me-2"></i>Add FAQ</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Current FAQs</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<?php if (!$faqs): ?>
|
||||||
|
<div class="text-center p-5 text-muted">
|
||||||
|
<i class="bi bi-question-circle display-4 mb-3 d-block text-secondary opacity-50"></i>
|
||||||
|
<p class="mb-0">No FAQs added yet.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4">Sort</th>
|
||||||
|
<th>Question (EN)</th>
|
||||||
|
<th>Answer Extract</th>
|
||||||
|
<th class="text-end pe-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($faqs as $faq): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 text-muted"><small><?= e((string)$faq['sort_order']) ?></small></td>
|
||||||
|
<td class="fw-medium"><?= e($faq['question_en']) ?></td>
|
||||||
|
<td class="text-muted"><small><?= e(mb_strimwidth($faq['answer_en'], 0, 50, '...')) ?></small></td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<a class="btn btn-sm btn-light border me-1" href="<?= e(url_with_lang('admin_faqs.php', ['edit_faq' => (int)$faq['id']])) ?>" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this FAQ?');"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="faq_id" value="<?= e((string)$faq['id']) ?>">
|
||||||
|
<button type="submit" name="delete_faq" class="btn btn-sm btn-light border text-danger" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</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($faqs) ?> of <?= $total ?> FAQs</span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?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="?page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
294
admin_integrations.php
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
require_once __DIR__ . '/includes/NotificationService.php';
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$success = '';
|
||||||
|
$testSuccess = '';
|
||||||
|
$testError = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
if (isset($_POST['action']) && $_POST['action'] === 'test_whatsapp') {
|
||||||
|
$testPhone = trim($_POST['test_phone'] ?? '');
|
||||||
|
$testMessage = trim($_POST['test_message'] ?? '');
|
||||||
|
|
||||||
|
if ($testPhone === '') {
|
||||||
|
$testError = 'Phone number is required for testing.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// We use the new public method in NotificationService
|
||||||
|
// This method handles the 968 prefix logic internally.
|
||||||
|
$res = NotificationService::sendWhatsApp($testPhone, $testMessage ?: 'Test message from CargoLink');
|
||||||
|
|
||||||
|
if ($res['success']) {
|
||||||
|
$testSuccess = 'Message sent! ' . $res['message'];
|
||||||
|
} else {
|
||||||
|
$testError = 'Failed: ' . $res['message'];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$testError = 'Error sending test: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Save Settings
|
||||||
|
$updates = [
|
||||||
|
'thawani_publishable_key' => trim($_POST['thawani_publishable_key'] ?? ''),
|
||||||
|
'thawani_secret_key' => trim($_POST['thawani_secret_key'] ?? ''),
|
||||||
|
'thawani_environment' => trim($_POST['thawani_environment'] ?? 'test'),
|
||||||
|
'wablas_domain' => trim($_POST['wablas_domain'] ?? ''),
|
||||||
|
'wablas_api_token' => trim($_POST['wablas_api_token'] ?? ''),
|
||||||
|
'wablas_secret_key' => trim($_POST['wablas_secret_key'] ?? ''),
|
||||||
|
'whatsapp_enabled' => isset($_POST['whatsapp_enabled']) ? '1' : '0',
|
||||||
|
'smtp_host' => trim($_POST['smtp_host'] ?? ''),
|
||||||
|
'smtp_port' => trim($_POST['smtp_port'] ?? ''),
|
||||||
|
'smtp_secure' => trim($_POST['smtp_secure'] ?? ''),
|
||||||
|
'smtp_user' => trim($_POST['smtp_user'] ?? ''),
|
||||||
|
'smtp_pass' => trim($_POST['smtp_pass'] ?? ''),
|
||||||
|
'mail_from' => trim($_POST['mail_from'] ?? ''),
|
||||||
|
'mail_from_name' => trim($_POST['mail_from_name'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$pdo = db();
|
||||||
|
foreach ($updates as $key => $val) {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (:k, :v) ON DUPLICATE KEY UPDATE setting_value = :v2");
|
||||||
|
$stmt->execute([':k' => $key, ':v' => $val, ':v2' => $val]);
|
||||||
|
}
|
||||||
|
$success = "Integrations settings updated successfully.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = get_settings();
|
||||||
|
$thawaniPub = $settings['thawani_publishable_key'] ?? '';
|
||||||
|
$thawaniSec = $settings['thawani_secret_key'] ?? '';
|
||||||
|
$thawaniEnv = $settings['thawani_environment'] ?? 'test';
|
||||||
|
$wablasDomain = $settings['wablas_domain'] ?? '';
|
||||||
|
$wablasToken = $settings['wablas_api_token'] ?? '';
|
||||||
|
$wablasSecret = $settings['wablas_secret_key'] ?? '';
|
||||||
|
$whatsappEnabled = $settings['whatsapp_enabled'] ?? '0';
|
||||||
|
$smtpHost = $settings['smtp_host'] ?? '';
|
||||||
|
$smtpPort = $settings['smtp_port'] ?? '587';
|
||||||
|
$smtpSecure = $settings['smtp_secure'] ?? 'tls';
|
||||||
|
$smtpUser = $settings['smtp_user'] ?? '';
|
||||||
|
$smtpPass = $settings['smtp_pass'] ?? '';
|
||||||
|
$mailFrom = $settings['mail_from'] ?? '';
|
||||||
|
$mailFromName = $settings['mail_from_name'] ?? '';
|
||||||
|
|
||||||
|
render_header('Integrations', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('integrations'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('integrations')) ?></h1>
|
||||||
|
<p class="muted mb-0">Manage your payment gateway and communication APIs.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?= e($success) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($testSuccess): ?>
|
||||||
|
<div class="alert alert-info"><?= e($testSuccess) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($testError): ?>
|
||||||
|
<div class="alert alert-danger"><?= e($testError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" id="settingsForm"> <?= csrf_field() ?>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mb-4" id="integrationsTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="thawani-tab" data-bs-toggle="tab" data-bs-target="#thawani" type="button" role="tab" aria-controls="thawani" aria-selected="true">
|
||||||
|
Thawani Gateway
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="whatsapp-tab" data-bs-toggle="tab" data-bs-target="#whatsapp" type="button" role="tab" aria-controls="whatsapp" aria-selected="false">
|
||||||
|
<?= e(t('whatsapp_settings')) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#smtp" type="button" role="tab" aria-controls="smtp" aria-selected="false">
|
||||||
|
SMTP Mail
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="integrationsTabContent">
|
||||||
|
|
||||||
|
<!-- Thawani Payments Panel -->
|
||||||
|
<div class="tab-pane fade show active" id="thawani" role="tabpanel" aria-labelledby="thawani-tab">
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<h3 class="h5 fw-bold mb-3 d-flex align-items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-credit-card me-2 text-primary" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v1h14V4a1 1 0 0 0-1-1zm13 4H1v5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||||
|
<path d="M2 10a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/>
|
||||||
|
</svg>
|
||||||
|
Thawani Payments Gateway
|
||||||
|
</h3>
|
||||||
|
<p class="text-muted small mb-4">Configure your Oman-based Thawani Pay integration to process shipment payments.</p>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Environment</label>
|
||||||
|
<select name="thawani_environment" class="form-select">
|
||||||
|
<option value="test" <?= $thawaniEnv === 'test' ? 'selected' : '' ?>>Test / Sandbox</option>
|
||||||
|
<option value="live" <?= $thawaniEnv === 'live' ? 'selected' : '' ?>>Live / Production</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6"></div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Publishable Key</label>
|
||||||
|
<input type="text" name="thawani_publishable_key" class="form-control" value="<?= e($thawaniPub) ?>" placeholder="pk_test_...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Secret Key</label>
|
||||||
|
<input type="password" name="thawani_secret_key" class="form-control" value="<?= e($thawaniSec) ?>" placeholder="sk_test_...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wablas WhatsApp Gateway Panel -->
|
||||||
|
<div class="tab-pane fade" id="whatsapp" role="tabpanel" aria-labelledby="whatsapp-tab">
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<h3 class="h5 fw-bold mb-3 d-flex align-items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-whatsapp me-2 text-success" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.601 2.326A7.85 7.85 0 0 0 7.994 0C3.627 0 .068 3.558.064 7.926c0 1.399.366 2.76 1.057 3.965L0 16l4.204-1.102a7.9 7.9 0 0 0 3.79.965h.004c4.368 0 7.926-3.558 7.93-7.93A7.9 7.9 0 0 0 13.6 2.326zM7.994 14.521a6.6 6.6 0 0 1-3.356-.92l-.24-.144-2.494.654.666-2.433-.156-.251a6.56 6.56 0 0 1-1.007-3.505c0-3.626 2.957-6.584 6.591-6.584a6.56 6.56 0 0 1 4.66 1.931 6.56 6.56 0 0 1 1.928 4.66c-.004 3.639-2.961 6.592-6.592 6.592m3.615-4.934c-.197-.099-1.17-.578-1.353-.646-.182-.065-.315-.099-.445.099-.133.197-.513.646-.627.775-.114.133-.232.148-.43.05-.197-.1-.836-.308-1.592-.985-.59-.525-.985-1.175-1.103-1.372-.114-.198-.011-.304.088-.403.087-.088.197-.232.296-.346.1-.114.133-.198.198-.33.065-.134.034-.248-.015-.347-.05-.099-.445-1.076-.612-1.47-.16-.389-.323-.335-.445-.34-.114-.007-.247-.007-.38-.007a.73.73 0 0 0-.529.247c-.182.198-.691.677-.691 1.654s.71 1.916.81 2.049c.098.133 1.394 2.132 3.383 2.992.47.205.84.326 1.129.418.475.152.904.129 1.246.08.38-.058 1.171-.48 1.338-.943.164-.464.164-.86.114-.943-.049-.084-.182-.133-.38-.232"/>
|
||||||
|
</svg>
|
||||||
|
<?= e(t('whatsapp_settings')) ?>
|
||||||
|
</h3>
|
||||||
|
<p class="text-muted small mb-4">Connect Wablas to automatically send WhatsApp notifications to Shippers and Truck Owners.</p>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="whatsapp_enabled" id="whatsapp_enabled" value="1" <?= $whatsappEnabled === '1' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label fw-bold" for="whatsapp_enabled"><?= e(t('enable_whatsapp')) ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('wablas_domain')) ?></label>
|
||||||
|
<input type="text" name="wablas_domain" class="form-control" value="<?= e($wablasDomain) ?>" placeholder="e.g. https://solo.wablas.com">
|
||||||
|
<div class="form-text">Your assigned server node from the Wablas dashboard.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('wablas_api_token')) ?></label>
|
||||||
|
<input type="password" name="wablas_api_token" class="form-control" value="<?= e($wablasToken) ?>" placeholder="API Token">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Secret Key</label>
|
||||||
|
<input type="password" name="wablas_secret_key" class="form-control" value="<?= e($wablasSecret) ?>" placeholder="Secret Key">
|
||||||
|
<div class="form-text">Optional. Provide if your Wablas webhooks require signature verification.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Section -->
|
||||||
|
<div class="panel p-4 border-top">
|
||||||
|
<h4 class="h6 fw-bold mb-3">Test Connection</h4>
|
||||||
|
<div class="row g-2 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-muted">Test Phone Number</label>
|
||||||
|
<input type="text" form="testForm" name="test_phone" class="form-control" placeholder="e.g. 12345678">
|
||||||
|
<div class="form-text x-small">Enter 8 digits (system adds 968) or full number.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label small text-muted">Test Message</label>
|
||||||
|
<input type="text" form="testForm" name="test_message" class="form-control" placeholder="Test message...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="submit" form="testForm" class="btn btn-outline-success w-100">
|
||||||
|
<i class="bi bi-send me-1"></i> Send Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SMTP Mail Gateway Panel -->
|
||||||
|
<div class="tab-pane fade" id="smtp" role="tabpanel" aria-labelledby="smtp-tab">
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<h3 class="h5 fw-bold mb-3 d-flex align-items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-envelope me-2 text-danger" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||||
|
</svg>
|
||||||
|
SMTP Mail Settings
|
||||||
|
</h3>
|
||||||
|
<p class="text-muted small mb-4">Configure your SMTP server to send emails and system notifications.</p>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">SMTP Host</label>
|
||||||
|
<input type="text" name="smtp_host" class="form-control" value="<?= e($smtpHost) ?>" placeholder="smtp.example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-bold">SMTP Port</label>
|
||||||
|
<input type="number" name="smtp_port" class="form-control" value="<?= e($smtpPort) ?>" placeholder="587">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label fw-bold">Security</label>
|
||||||
|
<select name="smtp_secure" class="form-select">
|
||||||
|
<option value="tls" <?= $smtpSecure === 'tls' ? 'selected' : '' ?>>TLS</option>
|
||||||
|
<option value="ssl" <?= $smtpSecure === 'ssl' ? 'selected' : '' ?>>SSL</option>
|
||||||
|
<option value="none" <?= $smtpSecure === 'none' || $smtpSecure === '' ? 'selected' : '' ?>>None</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">SMTP Username</label>
|
||||||
|
<input type="text" name="smtp_user" class="form-control" value="<?= e($smtpUser) ?>" placeholder="user@example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">SMTP Password</label>
|
||||||
|
<input type="password" name="smtp_pass" class="form-control" value="<?= e($smtpPass) ?>" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Mail From Address</label>
|
||||||
|
<input type="email" name="mail_from" class="form-control" value="<?= e($mailFrom) ?>" placeholder="noreply@example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Mail From Name</label>
|
||||||
|
<input type="text" name="mail_from_name" class="form-control" value="<?= e($mailFromName) ?>" placeholder="CargoLink System">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary px-5">Save Integrations</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Hidden Test Form -->
|
||||||
|
<form method="post" id="testForm" style="display:none;"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="test_whatsapp">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
250
admin_landing_pages.php
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/app.php';
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
if (empty($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
|
||||||
|
header('Location: ' . url_with_lang('login.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'create' || $action === 'edit') {
|
||||||
|
$id = $_POST['id'] ?? null;
|
||||||
|
$title = $_POST['title'] ?? '';
|
||||||
|
$title_ar = $_POST['title_ar'] ?? '';
|
||||||
|
$subtitle = $_POST['subtitle'] ?? '';
|
||||||
|
$subtitle_ar = $_POST['subtitle_ar'] ?? '';
|
||||||
|
$content = $_POST['content'] ?? '';
|
||||||
|
$content_ar = $_POST['content_ar'] ?? '';
|
||||||
|
$layout = $_POST['layout'] ?? 'text_left';
|
||||||
|
$button_text = $_POST['button_text'] ?? '';
|
||||||
|
$button_text_ar = $_POST['button_text_ar'] ?? '';
|
||||||
|
$button_link = $_POST['button_link'] ?? '';
|
||||||
|
$section_order = (int)($_POST['section_order'] ?? 0);
|
||||||
|
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||||
|
|
||||||
|
$image_path = $_POST['current_image'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($_FILES['image']['name'])) {
|
||||||
|
$uploadDir = __DIR__ . '/uploads/pages/';
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0777, true);
|
||||||
|
}
|
||||||
|
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
||||||
|
$fileName = uniqid('img_') . '.' . $ext;
|
||||||
|
$dest = $uploadDir . $fileName;
|
||||||
|
if (move_uploaded_file($_FILES['image']['tmp_name'], $dest)) {
|
||||||
|
$image_path = 'uploads/pages/' . $fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'create') {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO landing_sections (title, title_ar, subtitle, subtitle_ar, content, content_ar, image_path, layout, button_text, button_text_ar, button_link, section_order, is_active, section_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'custom')");
|
||||||
|
$stmt->execute([$title, $title_ar, $subtitle, $subtitle_ar, $content, $content_ar, $image_path, $layout, $button_text, $button_text_ar, $button_link, $section_order, $is_active]);
|
||||||
|
set_flash('success', 'Section created successfully.');
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("UPDATE landing_sections SET title=?, title_ar=?, subtitle=?, subtitle_ar=?, content=?, content_ar=?, image_path=?, layout=?, button_text=?, button_text_ar=?, button_link=?, section_order=?, is_active=? WHERE id=?");
|
||||||
|
$stmt->execute([$title, $title_ar, $subtitle, $subtitle_ar, $content, $content_ar, $image_path, $layout, $button_text, $button_text_ar, $button_link, $section_order, $is_active, $id]);
|
||||||
|
set_flash('success', 'Section updated successfully.');
|
||||||
|
}
|
||||||
|
header('Location: ' . url_with_lang('admin_landing_pages.php'));
|
||||||
|
exit;
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
$id = $_POST['id'] ?? null;
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $pdo->prepare("SELECT section_type FROM landing_sections WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$sec = $stmt->fetch();
|
||||||
|
if ($sec && $sec['section_type'] !== 'custom') {
|
||||||
|
set_flash('danger', 'Built-in sections cannot be deleted, but you can hide them by unchecking Active.');
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM landing_sections WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
set_flash('success', 'Section deleted successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Location: ' . url_with_lang('admin_landing_pages.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->query("SELECT * FROM landing_sections ORDER BY section_order ASC, id ASC");
|
||||||
|
$sections = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$editId = $_GET['edit'] ?? null;
|
||||||
|
$editSection = null;
|
||||||
|
if ($editId) {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM landing_sections WHERE id = ?");
|
||||||
|
$stmt->execute([$editId]);
|
||||||
|
$editSection = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('app_name') . ' - Landing Pages', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('landing_pages'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="fw-bold mb-0">Landing Page Customization</h2>
|
||||||
|
<a href="<?= e(url_with_lang('admin_dashboard.php')) ?>" class="btn btn-outline-secondary">Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash = get_flash()): ?>
|
||||||
|
<div class="alert alert-<?= e($flash['type'] === 'success' ? 'success' : 'danger') ?> alert-dismissible fade show">
|
||||||
|
<?= e($flash['message']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="panel p-4 shadow-sm border-0 rounded-4 bg-white">
|
||||||
|
<h4 class="mb-4"><?= $editSection ? 'Edit Section' : 'Add New Section' ?></h4>
|
||||||
|
<form action="<?= e(url_with_lang('admin_landing_pages.php')) ?>" method="POST" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="action" value="<?= $editSection ? 'edit' : 'create' ?>">
|
||||||
|
<?php if ($editSection): ?>
|
||||||
|
<input type="hidden" name="id" value="<?= e($editSection['id']) ?>">
|
||||||
|
<input type="hidden" name="current_image" value="<?= e($editSection['image_path']) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Title (English) <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control" value="<?= e($editSection['title'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Title (Arabic)</label>
|
||||||
|
<input type="text" name="title_ar" class="form-control" dir="rtl" value="<?= e($editSection['title_ar'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Subtitle (English) (Optional)</label>
|
||||||
|
<textarea name="subtitle" class="form-control" rows="2"><?= e($editSection['subtitle'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Subtitle (Arabic) (Optional)</label>
|
||||||
|
<textarea name="subtitle_ar" class="form-control" rows="2" dir="rtl"><?= e($editSection['subtitle_ar'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Content (English) (HTML allowed)</label>
|
||||||
|
<textarea name="content" class="form-control" rows="5"><?= e($editSection['content'] ?? '') ?></textarea>
|
||||||
|
<small class="text-muted">Not applicable for most built-in sections.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Content (Arabic) (HTML allowed)</label>
|
||||||
|
<textarea name="content_ar" class="form-control" rows="5" dir="rtl"><?= e($editSection['content_ar'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Layout Type</label>
|
||||||
|
<select name="layout" class="form-select" <?= ($editSection['section_type'] ?? 'custom') !== 'custom' ? 'disabled' : '' ?>>
|
||||||
|
<option value="text_left" <?= ($editSection['layout'] ?? '') === 'text_left' ? 'selected' : '' ?>>Text Left, Image Right</option>
|
||||||
|
<option value="text_right" <?= ($editSection['layout'] ?? '') === 'text_right' ? 'selected' : '' ?>>Image Left, Text Right</option>
|
||||||
|
<option value="center" <?= ($editSection['layout'] ?? '') === 'center' ? 'selected' : '' ?>>Center (No Image)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Order</label>
|
||||||
|
<input type="number" name="section_order" class="form-control" value="<?= e($editSection['section_order'] ?? 0) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Button Text (English)</label>
|
||||||
|
<input type="text" name="button_text" class="form-control" value="<?= e($editSection['button_text'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Button Text (Arabic)</label>
|
||||||
|
<input type="text" name="button_text_ar" class="form-control" dir="rtl" value="<?= e($editSection['button_text_ar'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Button Link (e.g. login.php)</label>
|
||||||
|
<input type="text" name="button_link" class="form-control" value="<?= e($editSection['button_link'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Upload Picture</label>
|
||||||
|
<input type="file" name="image" class="form-control" accept="image/*">
|
||||||
|
<?php if (!empty($editSection['image_path'])): ?>
|
||||||
|
<div class="mt-2">
|
||||||
|
<img src="<?= e($editSection['image_path']) ?>" alt="Current Image" style="max-height: 80px; border-radius: 4px;">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_active" id="isActive" <?= (!isset($editSection) || $editSection['is_active']) ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="isActive">Active</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary"><?= $editSection ? 'Update Section' : 'Create Section' ?></button>
|
||||||
|
<?php if ($editSection): ?>
|
||||||
|
<a href="<?= e(url_with_lang('admin_landing_pages.php')) ?>" class="btn btn-outline-secondary">Cancel</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="panel p-4 shadow-sm border-0 rounded-4 bg-white">
|
||||||
|
<h4 class="mb-4">Current Sections</h4>
|
||||||
|
<?php if (!$sections): ?>
|
||||||
|
<p class="text-muted">No custom sections added yet.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($sections as $sec): ?>
|
||||||
|
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-center p-3">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 fw-bold"><?= e($sec['title']) ?> <span class="badge bg-<?= $sec['is_active'] ? 'success' : 'secondary' ?> ms-2"><?= $sec['is_active'] ? 'Active' : 'Draft' ?></span></h6>
|
||||||
|
<small class="text-muted">Order: <?= e($sec['section_order']) ?> | Type: <?= e(ucfirst($sec['section_type'])) ?> <?= $sec['section_type']==='custom' ? '| Layout: '.e($sec['layout']) : '' ?></small>
|
||||||
|
<?php if (!empty($sec['title_ar'])): ?>
|
||||||
|
<div class="mt-1 small text-muted">AR: <?= e($sec['title_ar']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="<?= e(url_with_lang('admin_landing_pages.php', ['edit' => $sec['id']])) ?>" class="btn btn-sm p-1 border-0 bg-transparent text-primary" title="Edit">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<?php if ($sec['section_type'] === 'custom'): ?>
|
||||||
|
<form action="<?= e(url_with_lang('admin_landing_pages.php')) ?>" method="POST" onsubmit="return confirm('Are you sure you want to delete this section?');" style="display:inline;">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="id" value="<?= e($sec['id']) ?>">
|
||||||
|
<button type="submit" class="btn btn-sm p-1 border-0 bg-transparent text-danger" title="Delete">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
7
admin_manage_locations.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/app.php'; require_role('admin');
|
||||||
|
|
||||||
|
header('Location: ' . url_with_lang('admin_countries.php'), true, 302);
|
||||||
|
exit;
|
||||||
228
admin_notification_templates.php
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
// Access Control
|
||||||
|
if (($_SESSION['user_role'] ?? '') !== 'admin') {
|
||||||
|
header('Location: ' . url_with_lang('login.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure table exists (idempotent)
|
||||||
|
try {
|
||||||
|
db()->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
event_name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email_subject_en VARCHAR(255),
|
||||||
|
email_body_en TEXT,
|
||||||
|
email_subject_ar VARCHAR(255),
|
||||||
|
email_body_ar TEXT,
|
||||||
|
whatsapp_body_en TEXT,
|
||||||
|
whatsapp_body_ar TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
");
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Ignore if table exists or permission issue, subsequent queries will fail if critical
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? 'list';
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
$errors = [];
|
||||||
|
$flash = get_flash();
|
||||||
|
|
||||||
|
if ($action === 'edit' && $id > 0) {
|
||||||
|
// Handle Edit
|
||||||
|
$stmt = db()->prepare("SELECT * FROM notification_templates WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$template = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
set_flash('error', t('template_not_found'));
|
||||||
|
header('Location: ' . url_with_lang('admin_notification_templates.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$email_subject_en = trim($_POST['email_subject_en'] ?? '');
|
||||||
|
$email_body_en = trim($_POST['email_body_en'] ?? '');
|
||||||
|
$email_subject_ar = trim($_POST['email_subject_ar'] ?? '');
|
||||||
|
$email_body_ar = trim($_POST['email_body_ar'] ?? '');
|
||||||
|
$whatsapp_body_en = trim($_POST['whatsapp_body_en'] ?? '');
|
||||||
|
$whatsapp_body_ar = trim($_POST['whatsapp_body_ar'] ?? '');
|
||||||
|
|
||||||
|
if ($email_subject_en === '' || $email_body_en === '') {
|
||||||
|
$errors[] = t('english_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
UPDATE notification_templates SET
|
||||||
|
email_subject_en = ?, email_body_en = ?,
|
||||||
|
email_subject_ar = ?, email_body_ar = ?,
|
||||||
|
whatsapp_body_en = ?, whatsapp_body_ar = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$email_subject_en, $email_body_en,
|
||||||
|
$email_subject_ar, $email_body_ar,
|
||||||
|
$whatsapp_body_en, $whatsapp_body_ar,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
set_flash('success', t('template_updated'));
|
||||||
|
header('Location: ' . url_with_lang('admin_notification_templates.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('edit_template'), 'admin', true);
|
||||||
|
?>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?= render_admin_sidebar('notification_templates') ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h4 mb-0"><?= e(t('edit_template')) ?>: <?= e($template['event_name']) ?></h2>
|
||||||
|
<a href="<?= e(url_with_lang('admin_notification_templates.php')) ?>" class="btn btn-outline-secondary"><?= e(t('back_to_list')) ?></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post"> <?= csrf_field() ?>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="mb-3 border-bottom pb-2"><?= e(t('english_version')) ?></h5>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('email_subject')) ?></label>
|
||||||
|
<input type="text" class="form-control" name="email_subject_en" value="<?= e($template['email_subject_en']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('email_body')) ?></label>
|
||||||
|
<textarea class="form-control" name="email_body_en" rows="6" required><?= e($template['email_body_en']) ?></textarea>
|
||||||
|
<div class="form-text text-muted"><?= e(t('email_body_help')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('whatsapp_body')) ?></label>
|
||||||
|
<textarea class="form-control" name="whatsapp_body_en" rows="4"><?= e($template['whatsapp_body_en']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5 class="mb-3 border-bottom pb-2"><?= e(t('arabic_version')) ?></h5>
|
||||||
|
<div class="mb-3" dir="rtl">
|
||||||
|
<label class="form-label"><?= e(t('email_subject')) ?></label>
|
||||||
|
<input type="text" class="form-control" name="email_subject_ar" value="<?= e($template['email_subject_ar']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" dir="rtl">
|
||||||
|
<label class="form-label"><?= e(t('email_body')) ?></label>
|
||||||
|
<textarea class="form-control" name="email_body_ar" rows="6"><?= e($template['email_body_ar']) ?></textarea>
|
||||||
|
<div class="form-text text-muted"><?= e(t('email_body_help')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" dir="rtl">
|
||||||
|
<label class="form-label"><?= e(t('whatsapp_body')) ?></label>
|
||||||
|
<textarea class="form-control" name="whatsapp_body_ar" rows="4"><?= e($template['whatsapp_body_ar']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-primary px-4"><?= e(t('save_changes')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List View
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$total = (int)db()->query("SELECT COUNT(*) FROM notification_templates")->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
$stmt = db()->query("SELECT * FROM notification_templates ORDER BY event_name ASC LIMIT $limit OFFSET $offset");
|
||||||
|
$templates = $stmt->fetchAll();
|
||||||
|
|
||||||
|
render_header(t('notification_templates'), 'admin', true);
|
||||||
|
?>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?= render_admin_sidebar('notification_templates') ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h4 mb-0"><?= e(t('notification_templates')) ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= $flash['type'] === 'error' ? 'danger' : 'success' ?>"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4"><?= e(t('event_name')) ?></th>
|
||||||
|
<th><?= e(t('subject_en')) ?></th>
|
||||||
|
<th><?= e(t('subject_ar')) ?></th>
|
||||||
|
<th class="text-end pe-4"><?= e(t('actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($templates as $t): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-medium"><?= e($t['event_name']) ?></td>
|
||||||
|
<td><?= e($t['email_subject_en']) ?></td>
|
||||||
|
<td><?= e($t['email_subject_ar']) ?></td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<a href="<?= e(url_with_lang('admin_notification_templates.php', ['action' => 'edit', 'id' => $t['id']])) ?>" class="btn btn-sm p-1 border-0 bg-transparent text-primary" title="<?= e(t('edit')) ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</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"><?= e(t('shown')) ?> <?= count($templates) ?> of <?= $total ?> templates</span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?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="?page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php render_footer(); ?>
|
||||||
321
admin_platform_users.php
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
// Ensure user is logged in and is an admin
|
||||||
|
if (!isset($_SESSION['user_id']) || ($_SESSION['user_role'] ?? '') !== 'admin') {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('manage_platform_users')) {
|
||||||
|
render_header(t('nav_platform_users'), 'platform_users');
|
||||||
|
$uid = $_SESSION['user_id'] ?? 'unknown';
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied. You do not have permission to manage platform users. (Error Code: 403-U' . $uid . ')</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$message = '';
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
// Handle Actions
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'create' || $action === 'edit') {
|
||||||
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$fullName = trim($_POST['full_name'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$selectedPermissions = $_POST['permissions'] ?? [];
|
||||||
|
|
||||||
|
if (empty($email) || empty($fullName)) {
|
||||||
|
$error = t('error_required');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($action === 'create') {
|
||||||
|
// Check if email exists
|
||||||
|
$stmtCheck = $pdo->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmtCheck->execute([$email]);
|
||||||
|
if ($stmtCheck->fetch()) {
|
||||||
|
$error = t('error_email_exists');
|
||||||
|
} else {
|
||||||
|
if (empty($password)) {
|
||||||
|
$error = t('error_required');
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO users (email, password, full_name, role, status) VALUES (?, ?, ?, 'admin', 'active')");
|
||||||
|
$stmt->execute([$email, password_hash($password, PASSWORD_DEFAULT), $fullName]);
|
||||||
|
$id = (int)$pdo->lastInsertId();
|
||||||
|
$message = t('user_created');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Edit
|
||||||
|
// Check if email exists for other user
|
||||||
|
$stmtCheck = $pdo->prepare("SELECT id FROM users WHERE email = ? AND id != ?");
|
||||||
|
$stmtCheck->execute([$email, $id]);
|
||||||
|
if ($stmtCheck->fetch()) {
|
||||||
|
$error = t('error_email_exists');
|
||||||
|
} else {
|
||||||
|
$sql = "UPDATE users SET email = ?, full_name = ? WHERE id = ?";
|
||||||
|
$params = [$email, $fullName, $id];
|
||||||
|
|
||||||
|
if (!empty($password)) {
|
||||||
|
$sql = "UPDATE users SET email = ?, full_name = ?, password = ? WHERE id = ?";
|
||||||
|
$params = [$email, $fullName, password_hash($password, PASSWORD_DEFAULT), $id];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$message = t('user_updated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error && $id) {
|
||||||
|
// Update Permissions
|
||||||
|
$pdo->prepare("DELETE FROM user_permissions WHERE user_id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
if (!empty($selectedPermissions)) {
|
||||||
|
$stmtPerm = $pdo->prepare("INSERT INTO user_permissions (user_id, permission_id) VALUES (?, ?)");
|
||||||
|
foreach ($selectedPermissions as $permId) {
|
||||||
|
$stmtPerm->execute([$id, $permId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$error) {
|
||||||
|
$pdo->commit();
|
||||||
|
} else {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id === $_SESSION['user_id']) {
|
||||||
|
$error = "You cannot delete your own account.";
|
||||||
|
} else {
|
||||||
|
$pdo->prepare("DELETE FROM users WHERE id = ? AND role = 'admin'")->execute([$id]);
|
||||||
|
$message = t('user_deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$total = (int)$pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'")->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
// Fetch Users
|
||||||
|
$stmtUsers = $pdo->query("SELECT id, email, full_name, created_at FROM users WHERE role = 'admin' ORDER BY created_at DESC LIMIT $limit OFFSET $offset");
|
||||||
|
$users = $stmtUsers->fetchAll();
|
||||||
|
|
||||||
|
// Fetch Permissions
|
||||||
|
$stmtPerms = $pdo->query("SELECT id, slug, name, description FROM permissions ORDER BY name ASC");
|
||||||
|
$allPermissions = $stmtPerms->fetchAll();
|
||||||
|
|
||||||
|
render_header(t('nav_platform_users'), 'platform_users', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('platform_users'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 fw-bold mb-0 text-dark"><?= e(t('nav_platform_users')) ?></h1>
|
||||||
|
<button class="btn btn-primary rounded-pill fw-bold px-4 shadow-sm" data-bs-toggle="modal" data-bs-target="#userModal" onclick="resetForm()">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i><?= e(t('create_user')) ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="alert alert-success shadow-sm border-0 rounded-3 mb-4"><?= e($message) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger shadow-sm border-0 rounded-3 mb-4"><?= e($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 rounded-4">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4 py-3 text-secondary text-uppercase small fw-bold">ID</th>
|
||||||
|
<th class="py-3 text-secondary text-uppercase small fw-bold"><?= e(t('full_name')) ?></th>
|
||||||
|
<th class="py-3 text-secondary text-uppercase small fw-bold"><?= e(t('email_address')) ?></th>
|
||||||
|
<th class="py-3 text-secondary text-uppercase small fw-bold"><?= e(t('created_at')) ?></th>
|
||||||
|
<th class="pe-4 py-3 text-end text-secondary text-uppercase small fw-bold"><?= e(t('actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($users)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted"><?= e(t('no_users')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($users as $user): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold">#<?= e($user['id']) ?></td>
|
||||||
|
<td><?= e($user['full_name']) ?></td>
|
||||||
|
<td><?= e($user['email']) ?></td>
|
||||||
|
<td class="text-muted small"><?= e(date('M j, Y', strtotime($user['created_at']))) ?></td>
|
||||||
|
<td class="pe-4 text-end">
|
||||||
|
<button class="btn btn-sm p-1 border-0 bg-transparent text-primary" onclick="editUser(<?= e(json_encode($user)) ?>)">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<?php if ($user['id'] !== $_SESSION['user_id']): ?>
|
||||||
|
<button class="btn btn-sm p-1 border-0 bg-transparent text-danger" onclick="confirmDelete(<?= $user['id'] ?>)">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</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"><?= e(t('shown')) ?> <?= count($users) ?> of <?= $total ?> users</span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?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="?page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Modal -->
|
||||||
|
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4">
|
||||||
|
<div class="modal-header border-0 pb-0">
|
||||||
|
<h5 class="modal-title fw-bold" id="modalTitle"><?= e(t('create_user')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" id="userForm"> <?= csrf_field() ?>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<input type="hidden" name="action" id="formAction" value="create">
|
||||||
|
<input type="hidden" name="id" id="userId">
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('full_name')) ?></label>
|
||||||
|
<input type="text" name="full_name" id="fullName" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('email_address')) ?></label>
|
||||||
|
<input type="email" name="email" id="email" class="form-control rounded-3" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('password')) ?> <span class="text-muted fw-normal small" id="passwordHint">(Leave empty to keep current)</span></label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control rounded-3" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="fw-bold mb-3 border-bottom pb-2"><?= e(t('manage_permissions')) ?></h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php foreach ($allPermissions as $perm): ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check p-3 border rounded-3 bg-light h-100">
|
||||||
|
<input class="form-check-input" type="checkbox" name="permissions[]" value="<?= $perm['id'] ?>" id="perm_<?= $perm['id'] ?>">
|
||||||
|
<label class="form-check-label w-100" for="perm_<?= $perm['id'] ?>">
|
||||||
|
<div class="fw-bold text-dark"><?= e($perm['name']) ?></div>
|
||||||
|
<div class="small text-muted"><?= e($perm['description']) ?></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0 pt-0 pb-4 pe-4">
|
||||||
|
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Form -->
|
||||||
|
<form method="post" id="deleteForm"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="id" id="deleteId">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function resetForm() {
|
||||||
|
document.getElementById('userForm').reset();
|
||||||
|
document.getElementById('formAction').value = 'create';
|
||||||
|
document.getElementById('userId').value = '';
|
||||||
|
document.getElementById('modalTitle').innerText = '<?= e(t('create_user')) ?>';
|
||||||
|
document.getElementById('passwordHint').innerText = '(Required)';
|
||||||
|
document.getElementById('password').required = true;
|
||||||
|
|
||||||
|
// Uncheck all permissions
|
||||||
|
document.querySelectorAll('input[name="permissions[]"]').forEach(el => el.checked = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editUser(user) {
|
||||||
|
resetForm();
|
||||||
|
document.getElementById('formAction').value = 'edit';
|
||||||
|
document.getElementById('userId').value = user.id;
|
||||||
|
document.getElementById('fullName').value = user.full_name;
|
||||||
|
document.getElementById('email').value = user.email;
|
||||||
|
document.getElementById('modalTitle').innerText = '<?= e(t('edit_user')) ?>';
|
||||||
|
document.getElementById('passwordHint').innerText = '(Leave empty to keep current)';
|
||||||
|
document.getElementById('password').required = false;
|
||||||
|
|
||||||
|
// Fetch user permissions via AJAX (or simpler, just reload the page - but for UX let's fetch)
|
||||||
|
// For simplicity in this demo, we'll fetch them via a separate hidden endpoint or just pre-load all permissions for all users in PHP?
|
||||||
|
// Let's pre-load permissions for all users to avoid AJAX complexity here.
|
||||||
|
|
||||||
|
const userPermissions = <?= json_encode($pdo->query("SELECT user_id, permission_id FROM user_permissions")->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN)) ?>;
|
||||||
|
|
||||||
|
const perms = userPermissions[user.id] || [];
|
||||||
|
perms.forEach(permId => {
|
||||||
|
const el = document.getElementById('perm_' + permId);
|
||||||
|
if (el) el.checked = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('userModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(id) {
|
||||||
|
if (confirm('<?= e(t('confirm_delete')) ?>')) {
|
||||||
|
document.getElementById('deleteId').value = id;
|
||||||
|
document.getElementById('deleteForm').submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
292
admin_reports_shippers.php
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('view_reports') && !has_permission('manage_shippers')) {
|
||||||
|
render_header(t('shippers_statements'), 'reports_shippers');
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied. You do not have permission to view reports.</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipperId = (int)($_GET['shipper_id'] ?? 0);
|
||||||
|
$startDate = $_GET['start_date'] ?? '';
|
||||||
|
$endDate = $_GET['end_date'] ?? '';
|
||||||
|
|
||||||
|
$shipper = null;
|
||||||
|
$shipments = [];
|
||||||
|
$totalAmount = 0.00;
|
||||||
|
|
||||||
|
// Fetch all shippers for the dropdown
|
||||||
|
$allShippers = db()->query("
|
||||||
|
SELECT u.id, u.full_name, p.company_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN shipper_profiles p ON u.id = p.user_id
|
||||||
|
WHERE u.role = 'shipper'
|
||||||
|
ORDER BY u.full_name ASC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
if ($shipperId) {
|
||||||
|
// Fetch selected shipper details
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT u.id, u.full_name, u.email, p.company_name, p.phone, p.address_line,
|
||||||
|
c.name_en as country, ci.name_en as city
|
||||||
|
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 u.id = ? AND u.role = 'shipper'
|
||||||
|
");
|
||||||
|
$stmt->execute([$shipperId]);
|
||||||
|
$shipper = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($shipper) {
|
||||||
|
// Build Query with Date Filter
|
||||||
|
$sql = "SELECT *
|
||||||
|
FROM shipments
|
||||||
|
WHERE shipper_id = ?
|
||||||
|
AND status IN ('confirmed', 'in_transit', 'delivered')";
|
||||||
|
$params = [$shipperId];
|
||||||
|
|
||||||
|
if (!empty($startDate)) {
|
||||||
|
$sql .= " AND created_at >= ?";
|
||||||
|
$params[] = $startDate . ' 00:00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($endDate)) {
|
||||||
|
$sql .= " AND created_at <= ?";
|
||||||
|
$params[] = $endDate . ' 23:59:59';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$shipments = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($shipments as $s) {
|
||||||
|
$totalAmount += (float)($s['total_price'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('shippers_statements'), 'reports_shippers', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Select2 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Select2 Bootstrap 5 Fixes */
|
||||||
|
.select2-container .select2-selection--single { height: 38px !important; border: 1px solid #dee2e6 !important; padding: 5px 0; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px !important; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 26px !important; color: #212529; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
@page { size: A4; margin: 1cm; }
|
||||||
|
body { background: white !important; font-family: 'Times New Roman', serif; -webkit-print-color-adjust: exact; }
|
||||||
|
.no-print, .admin-sidebar, nav, footer, .btn, form.filter-form { display: none !important; }
|
||||||
|
.col-md-2 { display: none !important; }
|
||||||
|
.col-md-10 { width: 100% !important; flex: 0 0 100% !important; max-width: 100% !important; padding: 0 !important; margin: 0 !important; }
|
||||||
|
.row { display: flex !important; flex-wrap: wrap !important; margin: 0 !important; }
|
||||||
|
.col-6 { width: 50% !important; flex: 0 0 50% !important; max-width: 50% !important; }
|
||||||
|
.panel { border: none !important; box-shadow: none !important; padding: 0 !important; }
|
||||||
|
|
||||||
|
/* Compact Header */
|
||||||
|
.print-header { display: flex !important; justify-content: space-between !important; align-items: flex-start !important; margin-bottom: 10px !important; border-bottom: 2px solid #000 !important; padding-bottom: 5px !important; }
|
||||||
|
.print-header-left { text-align: left; }
|
||||||
|
.print-header-right { text-align: right; }
|
||||||
|
.print-header h1 { font-size: 14pt; font-weight: bold; margin: 0; text-transform: uppercase; }
|
||||||
|
.print-header h2 { font-size: 12pt; margin: 0; font-weight: normal; }
|
||||||
|
.print-header p { margin: 0; font-size: 9pt; color: #555; }
|
||||||
|
|
||||||
|
/* Compact Info Section */
|
||||||
|
.statement-info { margin-bottom: 10px !important; display: flex !important; justify-content: space-between !important; border-bottom: 1px solid #ddd !important; padding-bottom: 5px !important; }
|
||||||
|
.statement-info h5 { margin: 0 0 2px 0; font-size: 9pt; color: #777; text-transform: uppercase; font-weight: bold; }
|
||||||
|
.statement-info h4 { margin: 0; font-size: 11pt; font-weight: bold; }
|
||||||
|
.statement-info .small { font-size: 9pt; margin-top: 2px; }
|
||||||
|
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
|
table { width: 100% !important; border-collapse: collapse !important; font-size: 9pt; }
|
||||||
|
th, td { border: 1px solid #ddd !important; padding: 4px 6px !important; text-align: left; }
|
||||||
|
th { background-color: #f8f9fa !important; font-weight: bold; text-transform: uppercase; color: #000 !important; font-size: 8pt; }
|
||||||
|
.text-end { text-align: right !important; }
|
||||||
|
.total-row td { background-color: #eee !important; font-weight: bold; font-size: 10pt; border-top: 2px solid #000 !important; }
|
||||||
|
.print-footer { display: block !important; margin-top: 20px; text-align: center; font-size: 8pt; color: #777; border-top: 1px solid #ddd; padding-top: 5px; }
|
||||||
|
|
||||||
|
/* Arabic Support */
|
||||||
|
[dir="rtl"] body { font-family: 'Traditional Arabic', serif; }
|
||||||
|
[dir="rtl"] th, [dir="rtl"] td { text-align: right; }
|
||||||
|
[dir="rtl"] .text-end { text-align: left !important; }
|
||||||
|
}
|
||||||
|
.print-header, .print-footer { display: none; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100 no-print">
|
||||||
|
<?php render_admin_sidebar('reports_shippers'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<!-- Screen Only Header -->
|
||||||
|
<div class="page-intro d-flex justify-content-between align-items-center mb-4 no-print">
|
||||||
|
<div>
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('shippers_statements')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('view_print_statements')) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php if ($shipper): ?>
|
||||||
|
<button onclick="window.print()" class="btn btn-dark">
|
||||||
|
<i class="bi bi-printer me-2"></i><?= e(t('print_formal_statement')) ?>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="panel p-4 mb-4 no-print filter-form">
|
||||||
|
<form method="get" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('select_shipper')) ?></label>
|
||||||
|
<select name="shipper_id" id="shipperSelect" class="form-select" onchange="this.form.submit()">
|
||||||
|
<option value=""><?= e(t('choose_shipper')) ?></option>
|
||||||
|
<?php foreach ($allShippers as $s): ?>
|
||||||
|
<option value="<?= $s['id'] ?>" <?= $shipperId == $s['id'] ? 'selected' : '' ?>>
|
||||||
|
<?= e($s['full_name']) ?> (<?= e($s['company_name'] ?: 'No Company') ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('start_date')) ?></label>
|
||||||
|
<input type="date" name="start_date" class="form-control" value="<?= e($startDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('end_date')) ?></label>
|
||||||
|
<input type="date" name="end_date" class="form-control" value="<?= e($endDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= e(t('filter')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($shipperId && !$shipper): ?>
|
||||||
|
<div class="alert alert-warning"><?= e(t('shipper_not_found')) ?></div>
|
||||||
|
<?php elseif ($shipper): ?>
|
||||||
|
<div class="panel p-5 bg-white">
|
||||||
|
<!-- Print Header (Compact) -->
|
||||||
|
<div class="print-header">
|
||||||
|
<div class="print-header-left">
|
||||||
|
<h2 style="font-size: 14pt; margin: 0; font-weight: bold;"><?= e(get_setting('company_name', 'Logistics Platform')) ?></h2>
|
||||||
|
<p class="mt-1"><?= date('F j, Y') ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="print-header-right">
|
||||||
|
<h1><?= e(t('statement_of_account')) ?></h1>
|
||||||
|
<h2 style="font-size: 12pt; margin: 0; font-weight: normal;">كشف حساب</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Section (Compact) -->
|
||||||
|
<div class="statement-info row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5><?= e(t('billed_to')) ?> / إلى السيد</h5>
|
||||||
|
<h4><?= e($shipper['full_name']) ?></h4>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<?= e($shipper['company_name'] ?: '-') ?><br>
|
||||||
|
<?= e($shipper['address_line'] ?: '') ?><br>
|
||||||
|
<?= e($shipper['city'] ?: '') ?>, <?= e($shipper['country'] ?: '') ?><br>
|
||||||
|
<?= e($shipper['phone'] ?: '') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h5><?= e(t('summary')) ?> / ملخص</h5>
|
||||||
|
<div class="small">
|
||||||
|
<div><strong>Date:</strong> <?= date('Y-m-d') ?></div>
|
||||||
|
<?php if ($startDate || $endDate): ?>
|
||||||
|
<div><strong><?= e(t('period')) ?>:</strong> <?= e($startDate ?: 'Start') ?> to <?= e($endDate ?: 'Now') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div><strong><?= e(t('period')) ?>:</strong> <?= e(t('all_time')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div><strong><?= e(t('total_count')) ?>:</strong> <?= count($shipments) ?></div>
|
||||||
|
<div class="fs-5 fw-bold text-primary mt-1">
|
||||||
|
<?= format_currency($totalAmount) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date <br> <span class="fw-normal text-muted">التاريخ</span></th>
|
||||||
|
<th>Ref # <br> <span class="fw-normal text-muted">رقم الشحنة</span></th>
|
||||||
|
<th><?= e(t('route')) ?> <br> <span class="fw-normal text-muted">المسار</span></th>
|
||||||
|
<th><?= e(t('status')) ?> <br> <span class="fw-normal text-muted">الحالة</span></th>
|
||||||
|
<th class="text-end"><?= e(t('amount')) ?> <br> <span class="fw-normal text-muted">المبلغ</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($shipments)): ?>
|
||||||
|
<tr><td colspan="5" class="text-center py-4 text-muted"><?= e(t('no_completed_shipments')) ?></td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($shipments as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= date('Y-m-d', strtotime($row['created_at'])) ?></td>
|
||||||
|
<td><span class="font-monospace">#<?= $row['id'] ?></span></td>
|
||||||
|
<td>
|
||||||
|
<?= e($row['origin_city']) ?> <i class="bi bi-arrow-right small text-muted mx-1"></i> <?= e($row['destination_city']) ?>
|
||||||
|
<div class="small text-muted"><?= e($row['cargo_description']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= e(status_label($row['status'])) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end fw-bold">
|
||||||
|
<?= format_currency((float)$row['total_price']) ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="total-row table-active">
|
||||||
|
<td colspan="4" class="text-end text-uppercase"><?= e(t('total_balance')) ?> / الإجمالي</td>
|
||||||
|
<td class="text-end text-dark"><?= format_currency($totalAmount) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Footer -->
|
||||||
|
<div class="print-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 text-start">
|
||||||
|
<?= e(t('authorized_signature')) ?><br>
|
||||||
|
_______________________
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<?= e(t('printed_on')) ?> <?= date('Y-m-d H:i') ?><br>
|
||||||
|
<?= e(t('generated_doc')) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery and Select2 JS -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#shipperSelect').select2({
|
||||||
|
width: '100%',
|
||||||
|
placeholder: '<?= e(t('choose_shipper')) ?>',
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
287
admin_reports_summary.php
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
// Helper to validate date
|
||||||
|
function validate_date($date, $format = 'Y-m-d') {
|
||||||
|
$d = DateTime::createFromFormat($format, $date);
|
||||||
|
return $d && $d->format($format) === $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params
|
||||||
|
$reportType = $_GET['type'] ?? 'countries_origin'; // countries_origin, countries_dest, cities_origin, cities_dest, shippers
|
||||||
|
$startDate = $_GET['start_date'] ?? date('Y-m-01'); // Default to first day of current month
|
||||||
|
$endDate = $_GET['end_date'] ?? date('Y-m-t'); // Default to last day of current month
|
||||||
|
$export = $_GET['export'] ?? null;
|
||||||
|
|
||||||
|
if (!validate_date($startDate)) $startDate = date('Y-m-01');
|
||||||
|
if (!validate_date($endDate)) $endDate = date('Y-m-t');
|
||||||
|
|
||||||
|
// Build Query
|
||||||
|
$where = "s.payment_status = 'paid' AND DATE(s.created_at) BETWEEN ? AND ?";
|
||||||
|
$params = [$startDate, $endDate];
|
||||||
|
|
||||||
|
$groupBy = '';
|
||||||
|
$selectName = '';
|
||||||
|
$join = '';
|
||||||
|
$orderBy = 'total_amount DESC';
|
||||||
|
|
||||||
|
$pageTitle = t('shipments_by_origin_country');
|
||||||
|
|
||||||
|
switch ($reportType) {
|
||||||
|
case 'countries_origin':
|
||||||
|
$selectName = "COALESCE(co.name_en, 'Unknown') as name";
|
||||||
|
$join = "LEFT JOIN cities c ON s.origin_city = c.name_en
|
||||||
|
LEFT JOIN countries co ON c.country_id = co.id";
|
||||||
|
$groupBy = "co.name_en";
|
||||||
|
$pageTitle = t('shipments_by_origin_country');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'countries_dest':
|
||||||
|
$selectName = "COALESCE(co.name_en, 'Unknown') as name";
|
||||||
|
$join = "LEFT JOIN cities c ON s.destination_city = c.name_en
|
||||||
|
LEFT JOIN countries co ON c.country_id = co.id";
|
||||||
|
$groupBy = "co.name_en";
|
||||||
|
$pageTitle = t('shipments_by_dest_country');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cities_origin':
|
||||||
|
$selectName = "s.origin_city as name";
|
||||||
|
$groupBy = "s.origin_city";
|
||||||
|
$pageTitle = t('shipments_by_origin_city');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cities_dest':
|
||||||
|
$selectName = "s.destination_city as name";
|
||||||
|
$groupBy = "s.destination_city";
|
||||||
|
$pageTitle = t('shipments_by_dest_city');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'shippers':
|
||||||
|
$selectName = "s.shipper_name as name"; // simplified, could join users table
|
||||||
|
$groupBy = "s.shipper_name";
|
||||||
|
$pageTitle = t('shipments_by_shipper');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$reportType = 'countries_origin';
|
||||||
|
$selectName = "COALESCE(co.name_en, 'Unknown') as name";
|
||||||
|
$join = "LEFT JOIN cities c ON s.origin_city = c.name_en
|
||||||
|
LEFT JOIN countries co ON c.country_id = co.id";
|
||||||
|
$groupBy = "co.name_en";
|
||||||
|
$pageTitle = t('shipments_by_origin_country');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
$selectName,
|
||||||
|
COUNT(s.id) as shipment_count,
|
||||||
|
SUM(s.total_price) as total_amount,
|
||||||
|
SUM(s.platform_fee) as total_profit
|
||||||
|
FROM shipments s
|
||||||
|
$join
|
||||||
|
WHERE $where
|
||||||
|
GROUP BY $groupBy
|
||||||
|
ORDER BY $orderBy
|
||||||
|
";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$results = $stmt->fetchAll();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$results = [];
|
||||||
|
$error = "Database error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
$totalShipments = 0;
|
||||||
|
$grandTotalAmount = 0.0;
|
||||||
|
$grandTotalProfit = 0.0;
|
||||||
|
|
||||||
|
foreach ($results as $row) {
|
||||||
|
$totalShipments += $row['shipment_count'];
|
||||||
|
$grandTotalAmount += $row['total_amount'];
|
||||||
|
$grandTotalProfit += $row['total_profit'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CSV Export
|
||||||
|
if ($export === 'csv') {
|
||||||
|
header('Content-Type: text/csv');
|
||||||
|
header('Content-Disposition: attachment; filename="report_' . $reportType . '_' . date('Ymd') . '.csv"');
|
||||||
|
|
||||||
|
$out = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
fputcsv($out, ['Name', 'Shipments', 'Total Amount', 'Profit']);
|
||||||
|
|
||||||
|
// Rows
|
||||||
|
foreach ($results as $row) {
|
||||||
|
fputcsv($out, [
|
||||||
|
$row['name'],
|
||||||
|
$row['shipment_count'],
|
||||||
|
number_format((float)$row['total_amount'], 2, '.', ''),
|
||||||
|
number_format((float)$row['total_profit'], 2, '.', '')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
fputcsv($out, ['Total', $totalShipments, number_format($grandTotalAmount, 2, '.', ''), number_format($grandTotalProfit, 2, '.', '')]);
|
||||||
|
|
||||||
|
fclose($out);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('summary_report'), 'reports_summary', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100 d-print-none">
|
||||||
|
<?php render_admin_sidebar('reports_summary'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
|
||||||
|
<!-- Print Header -->
|
||||||
|
<div class="d-none d-print-block mb-4">
|
||||||
|
<div class="row align-items-center mb-4">
|
||||||
|
<div class="col-6">
|
||||||
|
<?php
|
||||||
|
$logoPath = get_setting('logo_path');
|
||||||
|
if ($logoPath): ?>
|
||||||
|
<img src="<?= e($logoPath) ?>" alt="Company Logo" style="max-height: 80px; width: auto;">
|
||||||
|
<?php else: ?>
|
||||||
|
<h2 class="fw-bold mb-0 text-dark"><?= e(get_setting('company_name', 'CargoLink')) ?></h2>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h3 class="mb-1 text-dark fw-bold"><?= e(t('summary_report')) ?></h3>
|
||||||
|
<p class="mb-0 text-muted small">
|
||||||
|
<?= e(get_setting('company_address')) ?><br>
|
||||||
|
<?= e(get_setting('company_email')) ?> | <?= e(get_setting('company_phone')) ?>
|
||||||
|
</p>
|
||||||
|
<p class="mb-0 text-muted small mt-2">
|
||||||
|
<?= e(t('generated')) ?>: <?= date('d M Y, H:i') ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="border p-3 rounded bg-light">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<strong class="text-muted d-block small text-uppercase"><?= e(t('report_type')) ?></strong>
|
||||||
|
<span class="fs-5 fw-bold text-dark"><?= e($pageTitle) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<strong class="text-muted d-block small text-uppercase"><?= e(t('period')) ?></strong>
|
||||||
|
<span class="fs-5 fw-bold text-dark"><?= e($startDate) ?> — <?= e($endDate) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
||||||
|
<div>
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('summary_report')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('analyze_performance')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="?type=<?= e($reportType) ?>&start_date=<?= e($startDate) ?>&end_date=<?= e($endDate) ?>&export=csv" class="btn btn-outline-primary btn-sm me-2">
|
||||||
|
<i class="bi bi-download me-2"></i><?= e(t('export_csv')) ?>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
||||||
|
<i class="bi bi-printer me-2"></i><?= e(t('print')) ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-4 mb-4 d-print-none">
|
||||||
|
<form method="get" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('report_type')) ?></label>
|
||||||
|
<select name="type" class="form-select" onchange="this.form.submit()">
|
||||||
|
<option value="countries_origin" <?= $reportType === 'countries_origin' ? 'selected' : '' ?>><?= e(t('shipments_by_origin_country')) ?></option>
|
||||||
|
<option value="countries_dest" <?= $reportType === 'countries_dest' ? 'selected' : '' ?>><?= e(t('shipments_by_dest_country')) ?></option>
|
||||||
|
<option value="cities_origin" <?= $reportType === 'cities_origin' ? 'selected' : '' ?>><?= e(t('shipments_by_origin_city')) ?></option>
|
||||||
|
<option value="cities_dest" <?= $reportType === 'cities_dest' ? 'selected' : '' ?>><?= e(t('shipments_by_dest_city')) ?></option>
|
||||||
|
<option value="shippers" <?= $reportType === 'shippers' ? 'selected' : '' ?>><?= e(t('shipments_by_shipper')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('start_date')) ?></label>
|
||||||
|
<input type="date" name="start_date" class="form-control" value="<?= e($startDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('end_date')) ?></label>
|
||||||
|
<input type="date" name="end_date" class="form-control" value="<?= e($endDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= e(t('apply_filter')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-0 print-no-shadow">
|
||||||
|
<div class="p-3 border-bottom bg-light d-flex justify-content-between align-items-center d-print-none">
|
||||||
|
<h5 class="mb-0 fw-bold"><?= e($pageTitle) ?></h5>
|
||||||
|
<span class="badge bg-secondary"><?= e($startDate) ?> to <?= e($endDate) ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($results)): ?>
|
||||||
|
<div class="p-5 text-center text-muted">
|
||||||
|
<i class="bi bi-bar-chart fs-1 mb-3 d-block"></i>
|
||||||
|
<p class="mb-0"><?= e(t('no_paid_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4"><?= e(t('name')) ?></th>
|
||||||
|
<th class="text-center"><?= e(t('stats_shipments')) ?></th>
|
||||||
|
<th class="text-end"><?= e(t('total_amount')) ?></th>
|
||||||
|
<th class="text-end pe-4"><?= e(t('profit')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($results as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-medium"><?= e($row['name']) ?></td>
|
||||||
|
<td class="text-center"><?= number_format((int)$row['shipment_count']) ?></td>
|
||||||
|
<td class="text-end text-dark"><?= format_currency((float)$row['total_amount']) ?></td>
|
||||||
|
<td class="text-end pe-4 text-success fw-bold"><?= format_currency((float)$row['total_profit']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="table-light fw-bold border-top">
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4"><?= e(t('total_label')) ?></td>
|
||||||
|
<td class="text-center"><?= number_format($totalShipments) ?></td>
|
||||||
|
<td class="text-end"><?= format_currency($grandTotalAmount) ?></td>
|
||||||
|
<td class="text-end pe-4 text-success"><?= format_currency($grandTotalProfit) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Print Footer -->
|
||||||
|
<div class="d-none d-print-block print-footer border-top pt-2">
|
||||||
|
<div class="row text-muted small">
|
||||||
|
<div class="col-6">
|
||||||
|
<?= e(t('printed_by')) ?>: <?= e($_SESSION['user_name'] ?? 'Admin') ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<?= e(t('generated')) ?>: <?= date('d M Y H:i') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
292
admin_reports_truck_owners.php
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('view_reports') && !has_permission('manage_truck_owners')) {
|
||||||
|
render_header(t('truck_owners_statements'), 'reports_truck_owners');
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied. You do not have permission to view reports.</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (int)($_GET['owner_id'] ?? 0);
|
||||||
|
$startDate = $_GET['start_date'] ?? '';
|
||||||
|
$endDate = $_GET['end_date'] ?? '';
|
||||||
|
|
||||||
|
$owner = null;
|
||||||
|
$shipments = [];
|
||||||
|
$totalEarnings = 0.00;
|
||||||
|
|
||||||
|
// Fetch all truck owners for the dropdown
|
||||||
|
$allOwners = db()->query("
|
||||||
|
SELECT u.id, u.full_name, p.plate_no
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN truck_owner_profiles p ON u.id = p.user_id
|
||||||
|
WHERE u.role = 'truck_owner'
|
||||||
|
ORDER BY u.full_name ASC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
if ($ownerId) {
|
||||||
|
// Fetch selected owner details
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT u.id, u.full_name, u.email, p.phone, p.plate_no, p.truck_type,
|
||||||
|
c.name_en as country, ci.name_en as city
|
||||||
|
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 u.id = ? AND u.role = 'truck_owner'
|
||||||
|
");
|
||||||
|
$stmt->execute([$ownerId]);
|
||||||
|
$owner = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($owner) {
|
||||||
|
// Build Query with Date Filter
|
||||||
|
$sql = "SELECT *
|
||||||
|
FROM shipments
|
||||||
|
WHERE truck_owner_id = ?
|
||||||
|
AND status IN ('confirmed', 'in_transit', 'delivered')";
|
||||||
|
$params = [$ownerId];
|
||||||
|
|
||||||
|
if (!empty($startDate)) {
|
||||||
|
$sql .= " AND created_at >= ?";
|
||||||
|
$params[] = $startDate . ' 00:00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($endDate)) {
|
||||||
|
$sql .= " AND created_at <= ?";
|
||||||
|
$params[] = $endDate . ' 23:59:59';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$shipments = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($shipments as $s) {
|
||||||
|
$totalEarnings += (float)($s['offer_price'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('truck_owners_statements'), 'reports_truck_owners', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Select2 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Select2 Bootstrap 5 Fixes */
|
||||||
|
.select2-container .select2-selection--single { height: 38px !important; border: 1px solid #dee2e6 !important; padding: 5px 0; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px !important; }
|
||||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 26px !important; color: #212529; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
@page { size: A4; margin: 1cm; }
|
||||||
|
body { background: white !important; font-family: 'Times New Roman', serif; -webkit-print-color-adjust: exact; }
|
||||||
|
.no-print, .admin-sidebar, nav, footer, .btn, form.filter-form { display: none !important; }
|
||||||
|
.col-md-2 { display: none !important; }
|
||||||
|
.col-md-10 { width: 100% !important; flex: 0 0 100% !important; max-width: 100% !important; padding: 0 !important; margin: 0 !important; }
|
||||||
|
.row { display: flex !important; flex-wrap: wrap !important; margin: 0 !important; }
|
||||||
|
.col-6 { width: 50% !important; flex: 0 0 50% !important; max-width: 50% !important; }
|
||||||
|
.panel { border: none !important; box-shadow: none !important; padding: 0 !important; }
|
||||||
|
|
||||||
|
/* Compact Header */
|
||||||
|
.print-header { display: flex !important; justify-content: space-between !important; align-items: flex-start !important; margin-bottom: 10px !important; border-bottom: 2px solid #000 !important; padding-bottom: 5px !important; }
|
||||||
|
.print-header-left { text-align: left; }
|
||||||
|
.print-header-right { text-align: right; }
|
||||||
|
.print-header h1 { font-size: 14pt; font-weight: bold; margin: 0; text-transform: uppercase; }
|
||||||
|
.print-header h2 { font-size: 12pt; margin: 0; font-weight: normal; }
|
||||||
|
.print-header p { margin: 0; font-size: 9pt; color: #555; }
|
||||||
|
|
||||||
|
/* Compact Info Section */
|
||||||
|
.statement-info { margin-bottom: 10px !important; display: flex !important; justify-content: space-between !important; border-bottom: 1px solid #ddd !important; padding-bottom: 5px !important; }
|
||||||
|
.statement-info h5 { margin: 0 0 2px 0; font-size: 9pt; color: #777; text-transform: uppercase; font-weight: bold; }
|
||||||
|
.statement-info h4 { margin: 0; font-size: 11pt; font-weight: bold; }
|
||||||
|
.statement-info .small { font-size: 9pt; margin-top: 2px; }
|
||||||
|
|
||||||
|
.table-responsive { overflow: visible !important; }
|
||||||
|
table { width: 100% !important; border-collapse: collapse !important; font-size: 9pt; }
|
||||||
|
th, td { border: 1px solid #ddd !important; padding: 4px 6px !important; text-align: left; }
|
||||||
|
th { background-color: #f8f9fa !important; font-weight: bold; text-transform: uppercase; color: #000 !important; font-size: 8pt; }
|
||||||
|
.text-end { text-align: right !important; }
|
||||||
|
.total-row td { background-color: #eee !important; font-weight: bold; font-size: 10pt; border-top: 2px solid #000 !important; }
|
||||||
|
.print-footer { display: block !important; margin-top: 20px; text-align: center; font-size: 8pt; color: #777; border-top: 1px solid #ddd; padding-top: 5px; }
|
||||||
|
|
||||||
|
/* Arabic Support */
|
||||||
|
[dir="rtl"] body { font-family: 'Traditional Arabic', serif; }
|
||||||
|
[dir="rtl"] th, [dir="rtl"] td { text-align: right; }
|
||||||
|
[dir="rtl"] .text-end { text-align: left !important; }
|
||||||
|
}
|
||||||
|
.print-header, .print-footer { display: none; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100 no-print">
|
||||||
|
<?php render_admin_sidebar('reports_truck_owners'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<!-- Screen Only Header -->
|
||||||
|
<div class="page-intro d-flex justify-content-between align-items-center mb-4 no-print">
|
||||||
|
<div>
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('truck_owners_statements')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('view_print_owner_statements')) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php if ($owner): ?>
|
||||||
|
<button onclick="window.print()" class="btn btn-dark">
|
||||||
|
<i class="bi bi-printer me-2"></i><?= e(t('print_formal_statement')) ?>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="panel p-4 mb-4 no-print filter-form">
|
||||||
|
<form method="get" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('select_truck_owner')) ?></label>
|
||||||
|
<select name="owner_id" id="ownerSelect" class="form-select" onchange="this.form.submit()">
|
||||||
|
<option value=""><?= e(t('choose_truck_owner')) ?></option>
|
||||||
|
<?php foreach ($allOwners as $o): ?>
|
||||||
|
<option value="<?= $o['id'] ?>" <?= $ownerId == $o['id'] ? 'selected' : '' ?>>
|
||||||
|
<?= e($o['full_name']) ?> (Plate: <?= e($o['plate_no'] ?: '-') ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('start_date')) ?></label>
|
||||||
|
<input type="date" name="start_date" class="form-control" value="<?= e($startDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('end_date')) ?></label>
|
||||||
|
<input type="date" name="end_date" class="form-control" value="<?= e($endDate) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= e(t('filter')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($ownerId && !$owner): ?>
|
||||||
|
<div class="alert alert-warning"><?= e(t('owner_not_found')) ?></div>
|
||||||
|
<?php elseif ($owner): ?>
|
||||||
|
<div class="panel p-5 bg-white">
|
||||||
|
<!-- Print Header (Compact) -->
|
||||||
|
<div class="print-header">
|
||||||
|
<div class="print-header-left">
|
||||||
|
<h2 style="font-size: 14pt; margin: 0; font-weight: bold;"><?= e(get_setting('company_name', 'Logistics Platform')) ?></h2>
|
||||||
|
<p class="mt-1"><?= date('F j, Y') ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="print-header-right">
|
||||||
|
<h1><?= e(t('payment_statement')) ?></h1>
|
||||||
|
<h2 style="font-size: 12pt; margin: 0; font-weight: normal;">كشف مدفوعات</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Section (Compact) -->
|
||||||
|
<div class="statement-info row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5><?= e(t('pay_to')) ?> / إلى السيد</h5>
|
||||||
|
<h4><?= e($owner['full_name']) ?></h4>
|
||||||
|
<div class="text-muted small"><?= e(t('truck_plate')) ?>: <?= e($owner['plate_no'] ?: '-') ?></div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<?= e($owner['truck_type'] ?: '') ?><br>
|
||||||
|
<?= e($owner['city'] ?: '') ?>, <?= e($owner['country'] ?: '') ?><br>
|
||||||
|
<?= e($owner['phone'] ?: '') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<h5><?= e(t('summary')) ?> / ملخص</h5>
|
||||||
|
<div class="small">
|
||||||
|
<div><strong>Date:</strong> <?= date('Y-m-d') ?></div>
|
||||||
|
<?php if ($startDate || $endDate): ?>
|
||||||
|
<div><strong><?= e(t('period')) ?>:</strong> <?= e($startDate ?: 'Start') ?> to <?= e($endDate ?: 'Now') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div><strong><?= e(t('period')) ?>:</strong> <?= e(t('all_time')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div><strong><?= e(t('total_trips')) ?>:</strong> <?= count($shipments) ?></div>
|
||||||
|
<div class="fs-5 fw-bold text-primary mt-1">
|
||||||
|
<?= format_currency($totalEarnings) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date <br> <span class="fw-normal text-muted">التاريخ</span></th>
|
||||||
|
<th>Ref # <br> <span class="fw-normal text-muted">رقم الشحنة</span></th>
|
||||||
|
<th><?= e(t('route')) ?> <br> <span class="fw-normal text-muted">المسار</span></th>
|
||||||
|
<th><?= e(t('status')) ?> <br> <span class="fw-normal text-muted">الحالة</span></th>
|
||||||
|
<th class="text-end"><?= e(t('earnings')) ?> <br> <span class="fw-normal text-muted">المبلغ</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($shipments)): ?>
|
||||||
|
<tr><td colspan="5" class="text-center py-4 text-muted"><?= e(t('no_completed_shipments')) ?></td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($shipments as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= date('Y-m-d', strtotime($row['created_at'])) ?></td>
|
||||||
|
<td><span class="font-monospace">#<?= $row['id'] ?></span></td>
|
||||||
|
<td>
|
||||||
|
<?= e($row['origin_city']) ?> <i class="bi bi-arrow-right small text-muted mx-1"></i> <?= e($row['destination_city']) ?>
|
||||||
|
<div class="small text-muted"><?= e($row['cargo_description']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= e(status_label($row['status'])) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end fw-bold">
|
||||||
|
<?= format_currency((float)$row['offer_price']) ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="total-row table-active">
|
||||||
|
<td colspan="4" class="text-end text-uppercase"><?= e(t('total_earnings')) ?> / الإجمالي</td>
|
||||||
|
<td class="text-end text-dark"><?= format_currency($totalEarnings) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Footer -->
|
||||||
|
<div class="print-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 text-start">
|
||||||
|
<?= e(t('authorized_signature')) ?><br>
|
||||||
|
_______________________
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<?= e(t('printed_on')) ?> <?= date('Y-m-d H:i') ?><br>
|
||||||
|
<?= e(t('generated_doc')) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery and Select2 JS -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#ownerSelect').select2({
|
||||||
|
width: '100%',
|
||||||
|
placeholder: '<?= e(t('choose_truck_owner')) ?>',
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
364
admin_shipment_edit.php
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => t('invalid_id')]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: admin_shipments.php'); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = null;
|
||||||
|
|
||||||
|
$stmt = db()->prepare("SELECT * FROM shipments WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$shipment = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$shipment) {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => t('shipment_not_found')]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
header('Location: admin_shipments.php'); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all countries for dropdowns
|
||||||
|
$countries = [];
|
||||||
|
try {
|
||||||
|
$countries = db()->query("SELECT * FROM countries ORDER BY name_en ASC")->fetchAll();
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
// Helper to find country_id by city name
|
||||||
|
function find_country_id_by_city_name($cityName) {
|
||||||
|
if (!$cityName) return null;
|
||||||
|
$stmt = db()->prepare("SELECT country_id FROM cities WHERE name_en = ? LIMIT 1");
|
||||||
|
$stmt->execute([$cityName]);
|
||||||
|
return $stmt->fetchColumn() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fetch country IDs for existing cities
|
||||||
|
$origin_country_id = find_country_id_by_city_name($shipment['origin_city']);
|
||||||
|
$destination_country_id = find_country_id_by_city_name($shipment['destination_city']);
|
||||||
|
|
||||||
|
// Fetch initial city lists for the pre-selected countries
|
||||||
|
$origin_cities_list = [];
|
||||||
|
if ($origin_country_id) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM cities WHERE country_id = ? ORDER BY name_en ASC");
|
||||||
|
$stmt->execute([$origin_country_id]);
|
||||||
|
$origin_cities_list = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination_cities_list = [];
|
||||||
|
if ($destination_country_id) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM cities WHERE country_id = ? ORDER BY name_en ASC");
|
||||||
|
$stmt->execute([$destination_country_id]);
|
||||||
|
$destination_cities_list = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$shipper_name = trim($_POST['shipper_name'] ?? '');
|
||||||
|
$shipper_company = trim($_POST['shipper_company'] ?? '');
|
||||||
|
// These are now selected from dropdowns, but the value is still the city name
|
||||||
|
$origin_city = trim($_POST['origin_city'] ?? '');
|
||||||
|
$destination_city = trim($_POST['destination_city'] ?? '');
|
||||||
|
$shipment_type = trim($_POST['shipment_type'] ?? 'Dry');
|
||||||
|
$cargo_description = trim($_POST['cargo_description'] ?? '');
|
||||||
|
$weight_tons = (float)($_POST['weight_tons'] ?? 0);
|
||||||
|
$pickup_date = trim($_POST['pickup_date'] ?? '');
|
||||||
|
$delivery_date = trim($_POST['delivery_date'] ?? '');
|
||||||
|
$payment_method = trim($_POST['payment_method'] ?? 'thawani');
|
||||||
|
$status = trim($_POST['status'] ?? 'posted');
|
||||||
|
|
||||||
|
if ($shipper_name === '') $errors[] = t('error_required');
|
||||||
|
if ($origin_city === '') $errors[] = t('error_required');
|
||||||
|
if ($destination_city === '') $errors[] = t('error_required');
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$updateSql = "
|
||||||
|
UPDATE shipments
|
||||||
|
SET shipper_name = ?, shipper_company = ?, origin_city = ?, destination_city = ?,
|
||||||
|
shipment_type = ?, cargo_description = ?, weight_tons = ?, pickup_date = ?, delivery_date = ?,
|
||||||
|
payment_method = ?, status = ?
|
||||||
|
WHERE id = ?
|
||||||
|
";
|
||||||
|
db()->prepare($updateSql)->execute([
|
||||||
|
$shipper_name, $shipper_company, $origin_city, $destination_city,
|
||||||
|
$shipment_type, $cargo_description, $weight_tons, $pickup_date, $delivery_date,
|
||||||
|
$payment_method, $status, $id
|
||||||
|
]);
|
||||||
|
$flash = t('shipment_updated_success');
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => true, 'message' => $flash]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$shipment = $stmt->fetch();
|
||||||
|
|
||||||
|
// Re-fetch derived data for the form
|
||||||
|
$origin_country_id = find_country_id_by_city_name($shipment['origin_city']);
|
||||||
|
$destination_country_id = find_country_id_by_city_name($shipment['destination_city']);
|
||||||
|
|
||||||
|
$origin_cities_list = [];
|
||||||
|
if ($origin_country_id) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM cities WHERE country_id = ? ORDER BY name_en ASC");
|
||||||
|
$stmt->execute([$origin_country_id]);
|
||||||
|
$origin_cities_list = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination_cities_list = [];
|
||||||
|
if ($destination_country_id) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM cities WHERE country_id = ? ORDER BY name_en ASC");
|
||||||
|
$stmt->execute([$destination_country_id]);
|
||||||
|
$destination_cities_list = $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isAjax && $errors) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('<br>', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- OUTPUT START --
|
||||||
|
if (!$isAjax):
|
||||||
|
render_header(t('edit_shipment_tooltip'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('shipments'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="admin_shipments.php" class="btn btn-light border text-secondary" title="<?= t('back') ?>">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 class="section-title mb-1"><?= t('edit_shipment_title') ?><?= e((string)$shipment['id']) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= t('update_shipment_details') ?></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-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<li><?= e($error) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-heading"><?= t('shipment_details') ?></div>
|
||||||
|
<div class="p-4">
|
||||||
|
<?php endif; // End non-ajax wrapper ?>
|
||||||
|
|
||||||
|
<form method="post" action="admin_shipment_edit.php?id=<?= $id ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<?php if ($isAjax): ?>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= t('edit_shipment_title') ?><?= e((string)$shipment['id']) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="form-errors" class="alert alert-danger d-none"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('shipper_name') ?></label>
|
||||||
|
<input type="text" name="shipper_name" class="form-control" value="<?= e($shipment['shipper_name']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('shipper_company') ?></label>
|
||||||
|
<input type="text" name="shipper_company" class="form-control" value="<?= e((string)$shipment['shipper_company']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Origin Selection -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('origin_country') ?></label>
|
||||||
|
<select class="form-select" id="origin_country">
|
||||||
|
<option value=""><?= t('select_country_placeholder') ?></option>
|
||||||
|
<?php foreach ($countries as $c): ?>
|
||||||
|
<option value="<?= e($c['id']) ?>" <?= $c['id'] == $origin_country_id ? 'selected' : '' ?>>
|
||||||
|
<?= e($c['name_en']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('origin') ?></label>
|
||||||
|
<select class="form-select" name="origin_city" id="origin_city" required>
|
||||||
|
<option value=""><?= t('select_city_placeholder') ?></option>
|
||||||
|
<!-- If shipment has a city but we couldn't match a country, preserve the value as a fallback option -->
|
||||||
|
<?php if (!$origin_country_id && $shipment['origin_city']): ?>
|
||||||
|
<option value="<?= e($shipment['origin_city']) ?>" selected><?= e($shipment['origin_city']) ?></option>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php foreach ($origin_cities_list as $city): ?>
|
||||||
|
<option value="<?= e($city['name_en']) ?>" <?= $city['name_en'] === $shipment['origin_city'] ? 'selected' : '' ?>>
|
||||||
|
<?= e($city['name_en']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Destination Selection -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('destination_country') ?></label>
|
||||||
|
<select class="form-select" id="destination_country">
|
||||||
|
<option value=""><?= t('select_country_placeholder') ?></option>
|
||||||
|
<?php foreach ($countries as $c): ?>
|
||||||
|
<option value="<?= e($c['id']) ?>" <?= $c['id'] == $destination_country_id ? 'selected' : '' ?>>
|
||||||
|
<?= e($c['name_en']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('destination') ?></label>
|
||||||
|
<select class="form-select" name="destination_city" id="destination_city" required>
|
||||||
|
<option value=""><?= t('select_city_placeholder') ?></option>
|
||||||
|
<!-- Fallback for unmapped city -->
|
||||||
|
<?php if (!$destination_country_id && $shipment['destination_city']): ?>
|
||||||
|
<option value="<?= e($shipment['destination_city']) ?>" selected><?= e($shipment['destination_city']) ?></option>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php foreach ($destination_cities_list as $city): ?>
|
||||||
|
<option value="<?= e($city['name_en']) ?>" <?= $city['name_en'] === $shipment['destination_city'] ? 'selected' : '' ?>>
|
||||||
|
<?= e($city['name_en']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= t('shipment_type') ?></label>
|
||||||
|
<select name="shipment_type" class="form-select">
|
||||||
|
<option value="Dry" <?= ($shipment['shipment_type'] ?? 'Dry') === 'Dry' ? 'selected' : '' ?>><?= t('type_dry') ?></option>
|
||||||
|
<option value="Cold" <?= ($shipment['shipment_type'] ?? '') === 'Cold' ? 'selected' : '' ?>><?= t('type_cold') ?></option>
|
||||||
|
<option value="Frozen" <?= ($shipment['shipment_type'] ?? '') === 'Frozen' ? 'selected' : '' ?>><?= t('type_frozen') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label"><?= t('cargo') ?></label>
|
||||||
|
<input type="text" name="cargo_description" class="form-control" value="<?= e((string)$shipment['cargo_description']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= t('weight') ?></label>
|
||||||
|
<input type="number" step="0.1" name="weight_tons" class="form-control" value="<?= e((string)$shipment['weight_tons']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= t('pickup_date') ?></label>
|
||||||
|
<input type="date" name="pickup_date" class="form-control" value="<?= e((string)$shipment['pickup_date']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= t('delivery_date') ?></label>
|
||||||
|
<input type="date" name="delivery_date" class="form-control" value="<?= e((string)$shipment['delivery_date']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('payment_method') ?></label>
|
||||||
|
<select name="payment_method" class="form-select">
|
||||||
|
<option value="thawani" <?= $shipment['payment_method'] === 'thawani' ? 'selected' : '' ?>><?= t('payment_thawani') ?></option>
|
||||||
|
<option value="bank_transfer" <?= $shipment['payment_method'] === 'bank_transfer' ? 'selected' : '' ?>><?= t('payment_bank') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= t('status') ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value="pending_approval" <?= $shipment['status'] === 'pending_approval' ? 'selected' : '' ?>><?= t('status_pending_approval') ?></option>
|
||||||
|
<option value="posted" <?= $shipment['status'] === 'posted' ? 'selected' : '' ?>><?= t('status_posted') ?></option>
|
||||||
|
<option value="offered" <?= $shipment['status'] === 'offered' ? 'selected' : '' ?>><?= t('status_offered') ?></option>
|
||||||
|
<option value="confirmed" <?= $shipment['status'] === 'confirmed' ? 'selected' : '' ?>><?= t('status_confirmed') ?></option>
|
||||||
|
<option value="in_transit" <?= $shipment['status'] === 'in_transit' ? 'selected' : '' ?>><?= t('status_in_transit') ?></option>
|
||||||
|
<option value="delivered" <?= $shipment['status'] === 'delivered' ? 'selected' : '' ?>><?= t('status_delivered') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($isAjax): ?>
|
||||||
|
</div> <!-- modal-body end -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= t('cancel') ?></button>
|
||||||
|
<button type="submit" class="btn btn-primary"><?= t('save_changes') ?></button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="mt-4 pt-3 border-top d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary"><?= t('save_changes') ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if (!$isAjax): ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
function setupCityLoader(countrySelectId, citySelectId) {
|
||||||
|
const countrySelect = document.getElementById(countrySelectId);
|
||||||
|
const citySelect = document.getElementById(citySelectId);
|
||||||
|
|
||||||
|
if (!countrySelect || !citySelect) return;
|
||||||
|
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryId = this.value;
|
||||||
|
citySelect.innerHTML = '<option value=""><?= t('loading_cities') ?></option>';
|
||||||
|
citySelect.disabled = true;
|
||||||
|
|
||||||
|
if (countryId) {
|
||||||
|
fetch('api/get_cities.php?country_id=' + countryId)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
citySelect.innerHTML = '<option value=""><?= t('select_city_placeholder') ?></option>';
|
||||||
|
data.forEach(city => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = city.name_en; // Using name as value for DB compatibility
|
||||||
|
option.textContent = city.name_en;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
citySelect.disabled = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching cities:', error);
|
||||||
|
citySelect.innerHTML = '<option value=""><?= t('error_loading_cities') ?></option>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
citySelect.innerHTML = '<option value=""><?= t('select_city_placeholder') ?></option>';
|
||||||
|
citySelect.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCityLoader('origin_country', 'origin_city');
|
||||||
|
setupCityLoader('destination_country', 'destination_city');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php if (!$isAjax) render_footer(); ?>
|
||||||
338
admin_shipments.php
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = null;
|
||||||
|
|
||||||
|
// Handle action (Delete / Approve)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'], $_POST['shipment_id'])) {
|
||||||
|
$shipmentId = (int)$_POST['shipment_id'];
|
||||||
|
$action = $_POST['action'];
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
db()->prepare("DELETE FROM shipments WHERE id = ?")->execute([$shipmentId]);
|
||||||
|
$flash = t('flash_shipment_deleted');
|
||||||
|
} elseif ($action === 'approve') {
|
||||||
|
db()->prepare("UPDATE shipments SET status = 'posted' WHERE id = ?")->execute([$shipmentId]);
|
||||||
|
$flash = t('success_status');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search, Filter, Sort and Pagination parameters
|
||||||
|
$q = trim($_GET['q'] ?? '');
|
||||||
|
$status = trim($_GET['status'] ?? '');
|
||||||
|
$sort = trim($_GET['sort'] ?? 'newest');
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$whereClause = "1=1";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($q !== '') {
|
||||||
|
$whereClause .= " AND (shipper_name LIKE ? OR shipper_company LIKE ? OR origin_city LIKE ? OR destination_city LIKE ? OR cargo_description LIKE ?)";
|
||||||
|
$likeQ = "%$q%";
|
||||||
|
$params = array_merge($params, array_fill(0, 5, $likeQ));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status !== '' && in_array($status, ['posted', 'offered', 'confirmed', 'in_transit', 'delivered', 'pending_approval'])) {
|
||||||
|
$whereClause .= " AND status = ?";
|
||||||
|
$params[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting logic
|
||||||
|
$orderBy = match ($sort) {
|
||||||
|
'oldest' => 'created_at ASC',
|
||||||
|
'pickup_asc' => 'pickup_date ASC',
|
||||||
|
'pickup_desc' => 'pickup_date DESC',
|
||||||
|
default => 'created_at DESC', // newest
|
||||||
|
};
|
||||||
|
|
||||||
|
// Total count
|
||||||
|
$countSql = "SELECT COUNT(*) FROM shipments WHERE $whereClause";
|
||||||
|
$stmt = db()->prepare($countSql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$total = (int)$stmt->fetchColumn();
|
||||||
|
$totalPages = (int)ceil($total / $limit);
|
||||||
|
|
||||||
|
// Fetch shipments
|
||||||
|
$sql = "
|
||||||
|
SELECT *
|
||||||
|
FROM shipments
|
||||||
|
WHERE $whereClause
|
||||||
|
ORDER BY $orderBy
|
||||||
|
LIMIT $limit OFFSET $offset
|
||||||
|
";
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$shipments = $stmt->fetchAll();
|
||||||
|
|
||||||
|
render_header(t('manage_shipments'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('shipments'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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"><?= t('shipments_header') ?></h1>
|
||||||
|
<p class="muted mb-0"><?= t('shipments_subtitle') ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label small text-muted"><?= t('search_label') ?></label>
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="<?= t('search_shipments_placeholder') ?>" value="<?= e($q) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= t('status') ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value=""><?= t('all_statuses') ?></option>
|
||||||
|
<option value="pending_approval" <?= $status === 'pending_approval' ? 'selected' : '' ?>><?= t('status_pending_approval') ?></option>
|
||||||
|
<option value="posted" <?= $status === 'posted' ? 'selected' : '' ?>><?= t('status_posted') ?></option>
|
||||||
|
<option value="offered" <?= $status === 'offered' ? 'selected' : '' ?>><?= t('status_offered') ?></option>
|
||||||
|
<option value="confirmed" <?= $status === 'confirmed' ? 'selected' : '' ?>><?= t('status_confirmed') ?></option>
|
||||||
|
<option value="in_transit" <?= $status === 'in_transit' ? 'selected' : '' ?>><?= t('status_in_transit') ?></option>
|
||||||
|
<option value="delivered" <?= $status === 'delivered' ? 'selected' : '' ?>><?= t('status_delivered') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= t('sort_by') ?></label>
|
||||||
|
<select name="sort" class="form-select">
|
||||||
|
<option value="newest" <?= $sort === 'newest' ? 'selected' : '' ?>><?= t('sort_newest') ?></option>
|
||||||
|
<option value="oldest" <?= $sort === 'oldest' ? 'selected' : '' ?>><?= t('sort_oldest') ?></option>
|
||||||
|
<option value="pickup_asc" <?= $sort === 'pickup_asc' ? 'selected' : '' ?>><?= t('sort_pickup_soonest') ?></option>
|
||||||
|
<option value="pickup_desc" <?= $sort === 'pickup_desc' ? 'selected' : '' ?>><?= t('sort_pickup_latest') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= t('filter') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-0">
|
||||||
|
<?php if (!$shipments && ($q || $status)): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= t('no_shipments_found_criteria') ?></p></div>
|
||||||
|
<?php elseif (!$shipments): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= t('no_shipments_platform') ?></p></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0 align-middle table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4"><?= t('id_col') ?></th>
|
||||||
|
<th><?= t('shipper') ?></th>
|
||||||
|
<th><?= t('route_label') ?></th>
|
||||||
|
<th><?= t('dates_col') ?></th>
|
||||||
|
<th><?= t('status') ?></th>
|
||||||
|
<th class="text-end pe-4"><?= t('action') ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($shipments as $shipment): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4"><?= e((string)$shipment['id']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold text-dark"><?= e($shipment['shipper_name']) ?></div>
|
||||||
|
<div class="text-muted small"><?= e((string)$shipment['shipper_company']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><span class="text-muted small"><?= t('from_label') ?></span> <?= e($shipment['origin_city']) ?></div>
|
||||||
|
<div><span class="text-muted small"><?= t('to_label') ?></span> <?= e($shipment['destination_city']) ?></div>
|
||||||
|
<?php if (!empty($shipment['shipment_type'])): ?>
|
||||||
|
<div class="mt-1"><span class="badge bg-light text-secondary border"><?= e(t('type_' . strtolower($shipment['shipment_type']))) ?></span></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($shipment['target_price'])): ?>
|
||||||
|
<div class="text-info small fw-bold mt-1"><?= t('target_price') ?>: $<?= e($shipment['target_price']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="small"><span class="text-muted"><?= t('pick_label') ?></span> <?= e($shipment['pickup_date']) ?></div>
|
||||||
|
<div class="small"><span class="text-muted"><?= t('drop_label') ?></span> <?= e($shipment['delivery_date']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$statusClass = 'bg-secondary-subtle text-secondary';
|
||||||
|
if ($shipment['status'] === 'posted') $statusClass = 'bg-primary-subtle text-primary';
|
||||||
|
elseif ($shipment['status'] === 'offered') $statusClass = 'bg-info-subtle text-info';
|
||||||
|
elseif ($shipment['status'] === 'confirmed') $statusClass = 'bg-success-subtle text-success';
|
||||||
|
elseif ($shipment['status'] === 'in_transit') $statusClass = 'bg-warning-subtle text-warning';
|
||||||
|
elseif ($shipment['status'] === 'delivered') $statusClass = 'bg-dark-subtle text-dark';
|
||||||
|
elseif ($shipment['status'] === 'pending_approval') $statusClass = 'bg-warning text-dark';
|
||||||
|
?>
|
||||||
|
<span class="badge <?= $statusClass ?>"><?= e(status_label($shipment['status'])) ?></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<div class="d-inline-flex gap-1 align-items-center">
|
||||||
|
<?php if ($shipment['status'] === 'pending_approval'): ?>
|
||||||
|
<form method="post" class="d-inline m-0 p-0"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="shipment_id" value="<?= e((string)$shipment['id']) ?>">
|
||||||
|
<button type="submit" name="action" value="approve" class="btn btn-sm p-1 border-0 bg-transparent text-success" title="<?= t('approve') ?>">
|
||||||
|
<i class="bi bi-check-circle-fill"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="shipment_detail.php?id=<?= e((string)$shipment['id']) ?>" class="btn btn-sm p-1 border-0 bg-transparent text-primary" title="<?= t('view_shipment') ?>">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="admin_shipment_edit.php?id=<?= e((string)$shipment['id']) ?>"
|
||||||
|
class="btn btn-sm p-1 border-0 bg-transparent text-primary ajax-modal-trigger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal"
|
||||||
|
title="<?= t('edit_shipment_tooltip') ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline m-0 p-0"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="shipment_id" value="<?= e((string)$shipment['id']) ?>">
|
||||||
|
<button type="submit" name="action" value="delete" class="btn btn-sm p-1 border-0 bg-transparent text-danger" onclick="return confirm('<?= t('confirm_delete_shipment') ?>');" title="<?= t('delete') ?>">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</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"><?= t('showing') ?> <?= count($shipments) ?> <?= t('of') ?> <?= $total ?> <?= t('shipments_header') ?></span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $page - 1 ?>"><?= t('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) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $page + 1 ?>"><?= t('next') ?></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel"><?= t('edit_shipment_tooltip') ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden"><?= t('loading') ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const editModal = document.getElementById('editModal');
|
||||||
|
if (!editModal) return;
|
||||||
|
|
||||||
|
editModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
if (!button.classList.contains('ajax-modal-trigger')) return;
|
||||||
|
|
||||||
|
const url = button.getAttribute('href') + '&ajax=1';
|
||||||
|
const modalContent = editModal.querySelector('.modal-content');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= t('loading') ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
if (html.startsWith('{')) {
|
||||||
|
const data = JSON.parse(html);
|
||||||
|
modalContent.innerHTML = `<div class="modal-header"><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="alert alert-danger">${data.message}</div></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalContent.innerHTML = html;
|
||||||
|
|
||||||
|
modalContent.querySelectorAll('script').forEach(script => {
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
if (script.src) newScript.src = script.src;
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
document.body.appendChild(newScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = modalContent.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
fetch(form.action, { method: 'POST', body: formData })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = '<?= t('save_changes') ?>';
|
||||||
|
}
|
||||||
|
const errDiv = form.querySelector('#form-errors');
|
||||||
|
if(errDiv) {
|
||||||
|
errDiv.classList.remove('d-none');
|
||||||
|
errDiv.innerHTML = data.message;
|
||||||
|
} else {
|
||||||
|
alert(data.message || '<?= t('error_occurred') ?>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = '<?= t('save_changes') ?>';
|
||||||
|
}
|
||||||
|
alert('<?= t('error_occurred') ?>');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
modalContent.innerHTML = `<div class="modal-body text-danger"><?= t('failed_load_form') ?></div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
277
admin_shipper_edit.php
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$userId = (int)($_GET['id'] ?? 0);
|
||||||
|
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
|
||||||
|
|
||||||
|
if ($userId <= 0) {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Shipper not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
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') { validate_csrf_token();
|
||||||
|
$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'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
if ($password !== '') {
|
||||||
|
$stmtPass = db()->prepare("UPDATE users SET password = ? WHERE id = ? AND role = 'shipper'");
|
||||||
|
$stmtPass->execute([password_hash($password, PASSWORD_DEFAULT), $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.';
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => true, 'message' => $flash]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isAjax && $errors) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('<br>', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- OUTPUT START --
|
||||||
|
if (!$isAjax):
|
||||||
|
render_header('Edit Shipper', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('shippers'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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">
|
||||||
|
<?php endif; // End non-ajax wrapper ?>
|
||||||
|
|
||||||
|
<form method="post" action="admin_shipper_edit.php?id=<?= $userId ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<?php if ($isAjax): ?>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edit Shipper: <?= e($shipper['full_name']) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="form-errors" class="alert alert-danger d-none"></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<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-6">
|
||||||
|
<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-3">
|
||||||
|
<label class="form-label" for="password">Password <small class="text-muted">(leave blank)</small></label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<?php if ($isAjax): ?>
|
||||||
|
</div> <!-- modal-body end -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<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>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php if (!$isAjax): ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
const countrySelect = document.querySelector('#country_id');
|
||||||
|
const citySelect = document.querySelector('#city_id');
|
||||||
|
const lang = '<?= $lang ?>';
|
||||||
|
|
||||||
|
window.syncCities = function() {
|
||||||
|
if (!countrySelect || !citySelect) return;
|
||||||
|
|
||||||
|
const countryId = countrySelect.value;
|
||||||
|
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_ar : (city.name_en || city.name_ar);
|
||||||
|
if (String(city.id) === String(selectedValue)) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
citySelect.dataset.selected = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (countrySelect && citySelect) {
|
||||||
|
syncCities();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php if (!$isAjax) render_footer(); ?>
|
||||||
339
admin_shippers.php
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = null;
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('manage_shippers')) {
|
||||||
|
render_header(t('shippers'), 'shippers');
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied. You do not have permission to manage shippers.</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'] ?? '');
|
||||||
|
$status = trim($_GET['status'] ?? '');
|
||||||
|
$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 = array_merge($params, [$likeQ, $likeQ, $likeQ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status !== '' && in_array($status, ['active', 'pending', 'rejected'])) {
|
||||||
|
$whereClause .= " AND u.status = ?";
|
||||||
|
$params[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(t('manage_shippers'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('shippers'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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"><?= e(t('shippers')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('manage_registered_shippers')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 mt-md-0">
|
||||||
|
<a href="admin_user_create.php?role=shipper"
|
||||||
|
class="btn btn-primary rounded-pill fw-bold px-4 shadow-sm ajax-modal-trigger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i><?= e(t('create_shipper')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('search_placeholder_shipper')) ?></label>
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="<?= e(t('search_placeholder_shipper')) ?>" value="<?= e($q) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('status')) ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value=""><?= e(t('all_statuses')) ?></option>
|
||||||
|
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>><?= e(t('active')) ?></option>
|
||||||
|
<option value="pending" <?= $status === 'pending' ? 'selected' : '' ?>><?= e(t('pending')) ?></option>
|
||||||
|
<option value="rejected" <?= $status === 'rejected' ? 'selected' : '' ?>><?= e(t('rejected')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= e(t('filter')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-0">
|
||||||
|
<?php if (!$shippers && ($q || $status)): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= e(t('no_shippers_criteria')) ?></p></div>
|
||||||
|
<?php elseif (!$shippers): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= e(t('no_shippers_registered')) ?></p></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0 align-middle table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4">ID</th>
|
||||||
|
<th><?= e(t('name_company')) ?></th>
|
||||||
|
<th><?= e(t('contact')) ?></th>
|
||||||
|
<th><?= e(t('location')) ?></th>
|
||||||
|
<th><?= e(t('status')) ?></th>
|
||||||
|
<th class="text-end pe-4"><?= e(t('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"><?= e(t('active')) ?></span>
|
||||||
|
<?php elseif ($shipper['status'] === 'pending'): ?>
|
||||||
|
<span class="badge bg-warning-subtle text-warning"><?= e(t('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 p-1 border-0 bg-transparent text-primary ajax-modal-trigger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal"
|
||||||
|
title="<?= e(t('edit_shipper')) ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline m-0 p-0"> <?= csrf_field() ?>
|
||||||
|
<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 p-1 border-0 bg-transparent text-success" title="<?= e(t('approve')) ?>">
|
||||||
|
<i class="bi bi-check-lg"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($shipper['status'] !== 'rejected'): ?>
|
||||||
|
<button type="submit" name="action" value="reject" class="btn btn-sm p-1 border-0 bg-transparent text-warning" title="<?= e(t('reject')) ?>">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button type="submit" name="action" value="delete" class="btn btn-sm p-1 border-0 bg-transparent text-danger" onclick="return confirm('<?= e(t('delete_confirm_shipper')) ?>');" title="<?= e(t('delete')) ?>">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</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"><?= e(t('shown')) ?> <?= count($shippers) ?> of <?= $total ?> <?= e(t('shippers')) ?></span>
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&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) ?>&status=<?= urlencode($status) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit/Create Modal -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel"><?= e(t('loading')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden"><?= e(t('loading')) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const editModal = document.getElementById('editModal');
|
||||||
|
if (!editModal) return;
|
||||||
|
|
||||||
|
editModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
if (!button.classList.contains('ajax-modal-trigger')) return;
|
||||||
|
|
||||||
|
const url = button.getAttribute('href') + '&ajax=1';
|
||||||
|
const modalContent = editModal.querySelector('.modal-content');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= e(t('loading')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
if (html.startsWith('{')) {
|
||||||
|
const data = JSON.parse(html);
|
||||||
|
modalContent.innerHTML = `<div class="modal-header"><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="alert alert-danger">${data.message}</div></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalContent.innerHTML = html;
|
||||||
|
|
||||||
|
modalContent.querySelectorAll('script').forEach(script => {
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
if (script.src) newScript.src = script.src;
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
document.body.appendChild(newScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = modalContent.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
let originalBtnText = '';
|
||||||
|
if(submitBtn) {
|
||||||
|
originalBtnText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <?= e(t('loading')) ?>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
fetch(form.action, { method: 'POST', body: formData })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText || '<?= e(t('save_changes')) ?>';
|
||||||
|
}
|
||||||
|
const errDiv = form.querySelector('#form-errors');
|
||||||
|
if(errDiv) {
|
||||||
|
errDiv.classList.remove('d-none');
|
||||||
|
errDiv.innerHTML = data.message;
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'An error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText || '<?= e(t('save_changes')) ?>';
|
||||||
|
}
|
||||||
|
alert('An error occurred while saving.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
modalContent.innerHTML = `<div class="modal-body text-danger">Failed to load form.</div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
56
admin_truck_expiry_report.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
render_header('Truck Expiry Report', 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('truck_owners'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<h1 class="section-title mb-4">Truck Expiry Report</h1>
|
||||||
|
|
||||||
|
<div class="panel p-4">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Owner</th>
|
||||||
|
<th>Plate No</th>
|
||||||
|
<th>Registration Expiry</th>
|
||||||
|
<th>Insurance Expiry</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$trucks = db()->query("
|
||||||
|
SELECT t.*, u.full_name as owner_name
|
||||||
|
FROM trucks t
|
||||||
|
JOIN users u ON t.user_id = u.id
|
||||||
|
WHERE t.registration_expiry_date < DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||||
|
OR t.insurance_expiry_date < DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
foreach ($trucks as $truck):
|
||||||
|
$regExpired = strtotime($truck['registration_expiry_date']) < time();
|
||||||
|
$insExpired = strtotime($truck['insurance_expiry_date']) < time();
|
||||||
|
$isExpired = $regExpired || $insExpired;
|
||||||
|
?>
|
||||||
|
<tr class="<?= $isExpired ? 'table-danger' : 'table-warning' ?>">
|
||||||
|
<td><?= e($truck['owner_name']) ?></td>
|
||||||
|
<td><?= e($truck['plate_no']) ?></td>
|
||||||
|
<td><?= e($truck['registration_expiry_date']) ?></td>
|
||||||
|
<td><?= e($truck['insurance_expiry_date']) ?></td>
|
||||||
|
<td><?= $isExpired ? 'Expired' : 'Near Expiry' ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
553
admin_truck_owner_edit.php
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$userId = (int)($_GET['id'] ?? 0);
|
||||||
|
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
|
||||||
|
|
||||||
|
if ($userId <= 0) {
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
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.bank_account, p.bank_name, p.bank_branch,
|
||||||
|
p.id_card_path, p.truck_pic_path, p.registration_path,
|
||||||
|
p.is_company, p.ctr_number, p.notes
|
||||||
|
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();
|
||||||
|
|
||||||
|
$trucks = db()->prepare("SELECT * FROM trucks WHERE user_id = ?");
|
||||||
|
$trucks->execute([$userId]);
|
||||||
|
$ownerTrucks = $trucks->fetchAll();
|
||||||
|
|
||||||
|
if (!$owner) {
|
||||||
|
if ($isAjax) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Owner not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
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') { validate_csrf_token();
|
||||||
|
if (isset($_POST['approve_truck'])) {
|
||||||
|
$truckId = (int)$_POST['truck_id'];
|
||||||
|
db()->prepare("UPDATE trucks SET is_approved = 1 WHERE id = ? AND user_id = ?")->execute([$truckId, $userId]);
|
||||||
|
$flash = 'Truck approved successfully.';
|
||||||
|
if ($isAjax) { echo json_encode(['success' => true, 'message' => $flash]); exit; }
|
||||||
|
} elseif (isset($_POST['reject_truck'])) {
|
||||||
|
$truckId = (int)$_POST['truck_id'];
|
||||||
|
db()->prepare("UPDATE trucks SET is_approved = 0 WHERE id = ? AND user_id = ?")->execute([$truckId, $userId]);
|
||||||
|
$flash = 'Truck status set to unapproved.';
|
||||||
|
if ($isAjax) { echo json_encode(['success' => true, 'message' => $flash]); exit; }
|
||||||
|
} elseif (isset($_POST['add_truck'])) {
|
||||||
|
$truckType = trim($_POST['truck_type'] ?? '');
|
||||||
|
$loadCapacity = (float)($_POST['load_capacity'] ?? 0);
|
||||||
|
$plateNo = trim($_POST['plate_no'] ?? '');
|
||||||
|
$regExpiry = $_POST['registration_expiry_date'] ?? null;
|
||||||
|
$insExpiry = $_POST['insurance_expiry_date'] ?? null;
|
||||||
|
|
||||||
|
if ($truckType === '') $errors[] = 'Truck type is required.';
|
||||||
|
if ($loadCapacity <= 0) $errors[] = 'Valid load capacity is required.';
|
||||||
|
if ($plateNo === '') $errors[] = 'Plate number is required.';
|
||||||
|
|
||||||
|
$truckPicPath = null;
|
||||||
|
$regPath = null;
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
// Handle File Uploads
|
||||||
|
$uploadDir = 'uploads/trucks/';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
|
||||||
|
|
||||||
|
if (isset($_FILES['truck_pic']) && $_FILES['truck_pic']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = pathinfo($_FILES['truck_pic']['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = 'truck_' . uniqid() . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['truck_pic']['tmp_name'], $uploadDir . $filename)) {
|
||||||
|
$truckPicPath = $uploadDir . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($_FILES['registration_doc']) && $_FILES['registration_doc']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$ext = pathinfo($_FILES['registration_doc']['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = 'reg_' . uniqid() . '.' . $ext;
|
||||||
|
if (move_uploaded_file($_FILES['registration_doc']['tmp_name'], $uploadDir . $filename)) {
|
||||||
|
$regPath = $uploadDir . $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
INSERT INTO trucks (user_id, truck_type, load_capacity, plate_no, truck_pic_path, registration_path, registration_expiry_date, insurance_expiry_date, is_approved)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$userId, $truckType, $loadCapacity, $plateNo, $truckPicPath, $regPath,
|
||||||
|
$regExpiry ?: null, $insExpiry ?: null
|
||||||
|
]);
|
||||||
|
$flash = 'Truck added successfully.';
|
||||||
|
if ($isAjax) { echo json_encode(['success' => true, 'message' => $flash]); exit; }
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errors[] = 'Database error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($errors) && $isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('. ', $errors)]); exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$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'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
$bankAccount = trim($_POST['bank_account'] ?? '');
|
||||||
|
$bankName = trim($_POST['bank_name'] ?? '');
|
||||||
|
$bankBranch = trim($_POST['bank_branch'] ?? '');
|
||||||
|
$isCompany = isset($_POST['is_company']) ? 1 : 0;
|
||||||
|
$ctrNumber = trim($_POST['ctr_number'] ?? '');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
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 ($countryId <= 0 || $cityId <= 0) {
|
||||||
|
$errors[] = 'Please select country and city.';
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
if ($password !== '') {
|
||||||
|
$stmtPass = db()->prepare("UPDATE users SET password = ? WHERE id = ? AND role = 'truck_owner'");
|
||||||
|
$stmtPass->execute([password_hash($password, PASSWORD_DEFAULT), $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtProfile = db()->prepare("
|
||||||
|
UPDATE truck_owner_profiles
|
||||||
|
SET phone = ?, address_line = ?, country_id = ?, city_id = ?,
|
||||||
|
bank_account = ?, bank_name = ?, bank_branch = ?, is_company = ?,
|
||||||
|
ctr_number = ?, notes = ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
");
|
||||||
|
$stmtProfile->execute([$phone, $addressLine, $countryId, $cityId, $bankAccount, $bankName, $bankBranch, $isCompany, $ctrNumber, $notes, $userId]);
|
||||||
|
|
||||||
|
db()->commit();
|
||||||
|
$flash = 'Truck Owner profile updated successfully.';
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => true, 'message' => $flash]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
db()->rollBack();
|
||||||
|
$errors[] = 'Failed to update truck owner profile. Please try again.';
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('. ', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('. ', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- OUTPUT START --
|
||||||
|
if (!$isAjax) {
|
||||||
|
render_header('Edit Truck Owner', 'admin', true);
|
||||||
|
echo '<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">';
|
||||||
|
render_admin_sidebar('truck_owners');
|
||||||
|
echo ' </div>
|
||||||
|
<div class="col-md-10 p-4">';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (!$isAjax): ?>
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<a href="admin_truck_owners.php" class="text-decoration-none small text-muted mb-2 d-inline-block">← <?= e(t('back')) ?></a>
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('edit_owner')) ?></h1>
|
||||||
|
</div>
|
||||||
|
<div class="panel p-4">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= e(t('edit_owner')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?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; ?>
|
||||||
|
|
||||||
|
<!-- Hidden error container for JS -->
|
||||||
|
<div id="form-errors" class="alert alert-danger d-none"></div>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mb-4" id="ownerEditTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="profile-tab" data-bs-toggle="tab" data-bs-target="#tab-profile" type="button" role="tab" aria-controls="tab-profile" aria-selected="true">
|
||||||
|
<?= e(t('profile') ?: 'Profile') ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="trucks-tab" data-bs-toggle="tab" data-bs-target="#tab-trucks" type="button" role="tab" aria-controls="tab-trucks" aria-selected="false">
|
||||||
|
<?= e(t('trucks') ?: 'Trucks') ?> <span class="badge bg-secondary rounded-pill ms-1"><?= count($ownerTrucks) ?></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="docs-tab" data-bs-toggle="tab" data-bs-target="#tab-docs" type="button" role="tab" aria-controls="tab-docs" aria-selected="false">
|
||||||
|
<?= e(t('documents') ?: 'Documents') ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" id="ownerEditTabsContent">
|
||||||
|
<!-- Tab 1: Profile -->
|
||||||
|
<div class="tab-pane fade show active" id="tab-profile" role="tabpanel" aria-labelledby="profile-tab">
|
||||||
|
<form method="post" action="admin_truck_owner_edit.php?id=<?= $userId ?><?= $isAjax ? '&ajax=1' : '' ?>" class="mb-3"> <?= csrf_field() ?>
|
||||||
|
<h5 class="mb-3"><?= e(t('full_name')) ?></h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="full_name"><?= e(t('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"><?= e(t('email')) ?></label>
|
||||||
|
<input type="email" name="email" id="email" class="form-control" value="<?= e((string)$owner['email']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="phone"><?= e(t('phone')) ?></label>
|
||||||
|
<input type="text" name="phone" id="phone" class="form-control" value="<?= e((string)$owner['phone']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="password"><?= e(t('password')) ?> <small class="text-muted">(leave blank)</small></label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="status"><?= e(t('status')) ?></label>
|
||||||
|
<select name="status" id="status" class="form-select" required>
|
||||||
|
<option value="pending" <?= $owner['status'] === 'pending' ? 'selected' : '' ?>><?= e(t('pending')) ?></option>
|
||||||
|
<option value="active" <?= $owner['status'] === 'active' ? 'selected' : '' ?>><?= e(t('active')) ?></option>
|
||||||
|
<option value="rejected" <?= $owner['status'] === 'rejected' ? 'selected' : '' ?>><?= e(t('rejected')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_company" id="is_company" value="1" <?= $owner['is_company'] ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="is_company"><?= e(t('is_company_checkbox')) ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="ctr_number"><?= e(t('ctr_number')) ?></label>
|
||||||
|
<input type="text" name="ctr_number" id="ctr_number" class="form-control" value="<?= e((string)($owner['ctr_number'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label" for="notes"><?= e(t('notes')) ?></label>
|
||||||
|
<textarea name="notes" id="notes" class="form-control"><?= e((string)($owner['notes'] ?? '')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-3"><?= e(t('location')) ?></h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="country_id"><?= e(t('country')) ?></label>
|
||||||
|
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||||
|
<option value=""><?= e(t('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"><?= e(t('city')) ?></label>
|
||||||
|
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e((string)$owner['city_id']) ?>">
|
||||||
|
<option value=""><?= e(t('select_city')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="address_line"><?= e(t('address')) ?></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 border-top pt-3"><?= e(t('bank_account')) ?></h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_account"><?= e(t('bank_account')) ?></label>
|
||||||
|
<input type="text" name="bank_account" id="bank_account" class="form-control" value="<?= e((string)($owner['bank_account'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_name"><?= e(t('bank_name')) ?></label>
|
||||||
|
<input type="text" name="bank_name" id="bank_name" class="form-control" value="<?= e((string)($owner['bank_name'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_branch"><?= e(t('bank_branch')) ?></label>
|
||||||
|
<input type="text" name="bank_branch" id="bank_branch" class="form-control" value="<?= e((string)($owner['bank_branch'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary"><?= e(t('save_changes')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 2: Trucks -->
|
||||||
|
<div class="tab-pane fade" id="tab-trucks" role="tabpanel" aria-labelledby="trucks-tab">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#addTruckForm" aria-expanded="false" aria-controls="addTruckForm">
|
||||||
|
<i class="bi bi-plus-lg"></i> <?= e(t('add_truck') ?: 'Add New Truck') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="collapse mb-4" id="addTruckForm">
|
||||||
|
<div class="card card-body bg-light border-0">
|
||||||
|
<h6 class="mb-3"><?= e(t('new_truck_details') ?: 'New Truck Details') ?></h6>
|
||||||
|
<form method="post" action="admin_truck_owner_edit.php?id=<?= $userId ?><?= $isAjax ? '&ajax=1' : '' ?>" enctype="multipart/form-data">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="add_truck" value="1">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small"><?= e(t('truck_type') ?: 'Truck Type') ?></label>
|
||||||
|
<input type="text" name="truck_type" class="form-control form-control-sm" required placeholder="e.g. Flatbed">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small"><?= e(t('load_capacity') ?: 'Capacity (Tons)') ?></label>
|
||||||
|
<input type="number" step="0.01" name="load_capacity" class="form-control form-control-sm" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small"><?= e(t('plate_no') ?: 'Plate No') ?></label>
|
||||||
|
<input type="text" name="plate_no" class="form-control form-control-sm" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small"><?= e(t('reg_expiry') ?: 'Reg. Expiry') ?></label>
|
||||||
|
<input type="date" name="registration_expiry_date" class="form-control form-control-sm">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small"><?= e(t('ins_expiry') ?: 'Ins. Expiry') ?></label>
|
||||||
|
<input type="date" name="insurance_expiry_date" class="form-control form-control-sm">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small"><?= e(t('truck_picture') ?: 'Truck Picture') ?></label>
|
||||||
|
<input type="file" name="truck_pic" class="form-control form-control-sm" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small"><?= e(t('registration_doc') ?: 'Registration Doc') ?></label>
|
||||||
|
<input type="file" name="registration_doc" class="form-control form-control-sm" accept="image/*,application/pdf">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-end">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary"><?= e(t('add_truck') ?: 'Add Truck') ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= e(t('truck_type')) ?></th>
|
||||||
|
<th><?= e(t('cap')) ?></th>
|
||||||
|
<th><?= e(t('plate_no')) ?></th>
|
||||||
|
<th>Docs</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th><?= e(t('actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($ownerTrucks)): ?>
|
||||||
|
<tr><td colspan="7" class="text-center text-muted"><?= e(t('no_trucks_found') ?: 'No trucks found') ?></td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($ownerTrucks as $truck): ?>
|
||||||
|
<?php
|
||||||
|
$isExpired = (strtotime($truck['registration_expiry_date'] ?? '1900-01-01') < time()) || (strtotime($truck['insurance_expiry_date'] ?? '1900-01-01') < time());
|
||||||
|
?>
|
||||||
|
<tr class="<?= $isExpired ? 'table-danger' : '' ?>">
|
||||||
|
<td><?= e($truck['truck_type']) ?></td>
|
||||||
|
<td><?= e($truck['load_capacity']) ?></td>
|
||||||
|
<td><?= e($truck['plate_no']) ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<?php if (!empty($truck['truck_pic_path'])): ?>
|
||||||
|
<a href="<?= e('/' . $truck['truck_pic_path']) ?>" target="_blank" class="btn btn-xs btn-outline-secondary" title="<?= e(t('truck_picture')) ?>">
|
||||||
|
<i class="bi bi-truck"></i>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($truck['registration_path'])): ?>
|
||||||
|
<a href="<?= e('/' . $truck['registration_path']) ?>" target="_blank" class="btn btn-xs btn-outline-secondary" title="<?= e(t('truck_reg')) ?>">
|
||||||
|
<i class="bi bi-file-earmark-text"></i>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><?= e(($truck['registration_expiry_date'] ?? 'N/A') . ' / ' . ($truck['insurance_expiry_date'] ?? 'N/A')) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($isExpired): ?>
|
||||||
|
<span class="badge bg-danger"><?= e(t('rejected')) ?></span>
|
||||||
|
<?php elseif ($truck['is_approved']): ?>
|
||||||
|
<span class="badge bg-success"><?= e(t('active')) ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-warning text-dark"><?= e(t('pending')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="admin_truck_owner_edit.php?id=<?= $userId ?><?= $isAjax ? '&ajax=1' : '' ?>">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="truck_id" value="<?= e((string)$truck['id']) ?>">
|
||||||
|
<?php if ($truck['is_approved'] && !$isExpired): ?>
|
||||||
|
<button type="submit" name="reject_truck" class="btn btn-sm btn-outline-danger"><?= e(t('reject')) ?></button>
|
||||||
|
<?php elseif (!$isExpired): ?>
|
||||||
|
<button type="submit" name="approve_truck" class="btn btn-sm btn-outline-success"><?= e(t('approve')) ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 3: Documents -->
|
||||||
|
<div class="tab-pane fade" id="tab-docs" role="tabpanel" aria-labelledby="docs-tab">
|
||||||
|
<h5 class="mb-3"><?= e(t('id_card_front') ?: 'ID Card Documents') ?></h5>
|
||||||
|
<div class="d-flex flex-wrap gap-3 mb-4">
|
||||||
|
<?php
|
||||||
|
$idCards = json_decode($owner['id_card_path'] ?? '[]', true) ?: [];
|
||||||
|
if (empty($idCards) && !empty($owner['id_card_path']) && !is_array(json_decode($owner['id_card_path'] ?? ''))) {
|
||||||
|
$idCards = [$owner['id_card_path']];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php foreach ($idCards as $path): ?>
|
||||||
|
<div class="card">
|
||||||
|
<a href="<?= e('/' . $path) ?>" target="_blank">
|
||||||
|
<img src="<?= e('/' . $path) ?>" class="card-img-top" alt="ID Card" style="height: 150px; width: auto; object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
<div class="card-body p-2 text-center">
|
||||||
|
<a href="<?= e('/' . $path) ?>" target="_blank" class="btn btn-sm btn-outline-secondary"><?= e(t('view_full_size') ?: 'View') ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if (empty($idCards)): ?>
|
||||||
|
<p class="text-muted"><?= e(t('no_documents') ?: 'No documents uploaded') ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($owner['registration_path']) || !empty($owner['truck_pic_path'])): ?>
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('other_documents') ?: 'Other Profile Documents') ?></h5>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php if (!empty($owner['registration_path'])): ?>
|
||||||
|
<div class="card">
|
||||||
|
<a href="<?= e('/' . $owner['registration_path']) ?>" target="_blank">
|
||||||
|
<img src="<?= e('/' . $owner['registration_path']) ?>" class="card-img-top" alt="Registration" style="height: 150px; width: auto; object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
<div class="card-body p-2 text-center">
|
||||||
|
<h6 class="card-title small mb-2"><?= e(t('truck_reg') ?: 'Registration') ?></h6>
|
||||||
|
<a href="<?= e('/' . $owner['registration_path']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary"><?= e(t('view_full_size') ?: 'View') ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($owner['truck_pic_path'])): ?>
|
||||||
|
<div class="card">
|
||||||
|
<a href="<?= e('/' . $owner['truck_pic_path']) ?>" target="_blank">
|
||||||
|
<img src="<?= e('/' . $owner['truck_pic_path']) ?>" class="card-img-top" alt="Truck Pic" style="height: 150px; width: auto; object-fit: cover;">
|
||||||
|
</a>
|
||||||
|
<div class="card-body p-2 text-center">
|
||||||
|
<h6 class="card-title small mb-2"><?= e(t('truck_picture') ?: 'Truck Picture') ?></h6>
|
||||||
|
<a href="<?= e('/' . $owner['truck_pic_path']) ?>" target="_blank" class="btn btn-sm btn-outline-secondary"><?= e(t('view_full_size') ?: 'View') ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($isAjax): ?>
|
||||||
|
</div> <!-- end modal-body -->
|
||||||
|
<?php else: ?>
|
||||||
|
</div> <!-- end panel -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var 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=""><?= e(t('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_ar : (city.name_en || city.name_ar);
|
||||||
|
if (String(city.id) === String(selectedValue)) option.selected = true;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
citySelect.dataset.selected = '';
|
||||||
|
}
|
||||||
|
setTimeout(syncCities, 100);
|
||||||
|
|
||||||
|
// Ensure tabs are initialized if opened via AJAX
|
||||||
|
if (typeof bootstrap !== 'undefined') {
|
||||||
|
var triggerTabList = [].slice.call(document.querySelectorAll('#ownerEditTabs button'))
|
||||||
|
triggerTabList.forEach(function (triggerEl) {
|
||||||
|
new bootstrap.Tab(triggerEl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (!$isAjax) {
|
||||||
|
echo '</div></div>';
|
||||||
|
render_footer();
|
||||||
|
}
|
||||||
|
?>
|
||||||
384
admin_truck_owners.php
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = null;
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('manage_truck_owners')) {
|
||||||
|
render_header(t('truck_owners'), 'truck_owners');
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied. You do not have permission to manage truck owners.</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '');
|
||||||
|
$status = trim($_GET['status'] ?? '');
|
||||||
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
|
$limit = 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$whereClause = "u.role = 'truck_owner'";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($q !== '') {
|
||||||
|
// Search only by user details as trucks are now one-to-many
|
||||||
|
$whereClause .= " AND (u.full_name LIKE ? OR u.email LIKE ? OR p.phone LIKE ?)";
|
||||||
|
$likeQ = "%$q%";
|
||||||
|
$params = array_merge($params, [$likeQ, $likeQ, $likeQ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status !== '' && in_array($status, ['active', 'pending', 'rejected'])) {
|
||||||
|
$whereClause .= " AND u.status = ?";
|
||||||
|
$params[] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 with truck count
|
||||||
|
$sql = "
|
||||||
|
SELECT u.id, u.email, u.full_name, u.status, u.created_at,
|
||||||
|
p.phone, p.id_card_path,
|
||||||
|
c.name_en AS country_name,
|
||||||
|
ci.name_en AS city_name,
|
||||||
|
(SELECT COUNT(*) FROM trucks t WHERE t.user_id = u.id) as truck_count
|
||||||
|
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(t('manage_truck_owners'), 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar('truck_owners'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<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"><?= e(t('truck_owners')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('review_registrations')) ?></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 mt-md-0">
|
||||||
|
<a href="admin_user_create.php?role=truck_owner"
|
||||||
|
class="btn btn-primary rounded-pill fw-bold px-4 shadow-sm ajax-modal-trigger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal">
|
||||||
|
<i class="bi bi-plus-lg me-2"></i><?= e(t('create_owner')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('search_placeholder_owner')) ?></label>
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="<?= e(t('search_placeholder_owner')) ?>" value="<?= e($q) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small text-muted"><?= e(t('status')) ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value=""><?= e(t('all_statuses')) ?></option>
|
||||||
|
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>><?= e(t('active')) ?></option>
|
||||||
|
<option value="pending" <?= $status === 'pending' ? 'selected' : '' ?>><?= e(t('pending')) ?></option>
|
||||||
|
<option value="rejected" <?= $status === 'rejected' ? 'selected' : '' ?>><?= e(t('rejected')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><?= e(t('filter')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-0">
|
||||||
|
<?php if (!$owners && ($q || $status)): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= e(t('no_owners_criteria')) ?></p></div>
|
||||||
|
<?php elseif (!$owners): ?>
|
||||||
|
<div class="p-4"><p class="muted mb-0"><?= e(t('no_owners_registered')) ?></p></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table mb-0 align-middle table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4">ID</th>
|
||||||
|
<th><?= e(t('name_email')) ?></th>
|
||||||
|
<th><?= e(t('trucks')) ?></th>
|
||||||
|
<th><?= e(t('documents')) ?></th>
|
||||||
|
<th><?= e(t('status')) ?></th>
|
||||||
|
<th class="text-end pe-4"><?= e(t('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 class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-primary rounded-pill"><?= e((string)$owner['truck_count']) ?></span>
|
||||||
|
<span class="text-muted small"><?= e(t('trucks')) ?></span>
|
||||||
|
</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>
|
||||||
|
<?= e(t('view_docs')) ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($owner['status'] === 'active'): ?>
|
||||||
|
<span class="badge bg-success-subtle text-success"><?= e(t('active')) ?></span>
|
||||||
|
<?php elseif ($owner['status'] === 'pending'): ?>
|
||||||
|
<span class="badge bg-warning-subtle text-warning"><?= e(t('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 p-1 border-0 bg-transparent text-primary ajax-modal-trigger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal"
|
||||||
|
title="<?= e(t('edit_owner')) ?>">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="post" class="d-inline m-0 p-0"> <?= csrf_field() ?>
|
||||||
|
<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 p-1 border-0 bg-transparent text-success" title="<?= e(t('approve')) ?>">
|
||||||
|
<i class="bi bi-check-lg"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($owner['status'] !== 'rejected'): ?>
|
||||||
|
<button type="submit" name="action" value="reject" class="btn btn-sm p-1 border-0 bg-transparent text-warning" title="<?= e(t('reject')) ?>">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<button type="submit" name="action" value="delete" class="btn btn-sm p-1 border-0 bg-transparent text-danger" onclick="return confirm('<?= e(t('delete_confirm_owner')) ?>');" title="<?= e(t('delete')) ?>">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</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"><?= e(t('shown')) ?> <?= count($owners) ?> of <?= $total ?> <?= e(t('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) ?>&status=<?= urlencode($status) ?>&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) ?>&status=<?= urlencode($status) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||||
|
</li>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
|
||||||
|
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&page=<?= $page + 1 ?>">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit/Create Modal Placeholder -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel"><?= e(t('loading')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden"><?= e(t('loading')) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($owners as $owner): ?>
|
||||||
|
<?php
|
||||||
|
$idCards = json_decode($owner['id_card_path'] ?? '[]', true) ?: [];
|
||||||
|
?>
|
||||||
|
<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"><?= e(t('docs_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><?= e(t('id_card_front')) ?> / <?= e(t('id_card_back')) ?></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; ?>
|
||||||
|
<?php if (empty($idCards)): ?>
|
||||||
|
<span class="text-muted"><?= e(t('no_documents')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<?= e(t('view_truck_docs_in_edit')) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const editModal = document.getElementById('editModal');
|
||||||
|
if (!editModal) return;
|
||||||
|
|
||||||
|
editModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
if (!button.classList.contains('ajax-modal-trigger')) return;
|
||||||
|
|
||||||
|
const url = button.getAttribute('href') + '&ajax=1';
|
||||||
|
const modalContent = editModal.querySelector('.modal-content');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= e(t('loading')) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
if (html.startsWith('{')) {
|
||||||
|
const data = JSON.parse(html);
|
||||||
|
modalContent.innerHTML = `<div class="modal-header"><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="alert alert-danger">${data.message}</div></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalContent.innerHTML = html;
|
||||||
|
|
||||||
|
modalContent.querySelectorAll('script').forEach(script => {
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
if (script.src) newScript.src = script.src;
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
document.body.appendChild(newScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = modalContent.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
|
let originalBtnText = '';
|
||||||
|
if(submitBtn) {
|
||||||
|
originalBtnText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <?= e(t('loading')) ?>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
fetch(form.action, { method: 'POST', body: formData })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText || '<?= e(t('save_changes')) ?>';
|
||||||
|
}
|
||||||
|
const errDiv = form.querySelector('#form-errors');
|
||||||
|
if(errDiv) {
|
||||||
|
errDiv.classList.remove('d-none');
|
||||||
|
errDiv.innerHTML = data.message;
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'An error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText || '<?= e(t('save_changes')) ?>';
|
||||||
|
}
|
||||||
|
alert('An error occurred while saving.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
modalContent.innerHTML = `<div class="modal-body text-danger">Failed to load form.</div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
522
admin_user_create.php
Normal file
@ -0,0 +1,522 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
|
||||||
|
|
||||||
|
$role = $_GET['role'] ?? 'shipper';
|
||||||
|
if (!in_array($role, ['shipper', 'truck_owner'], true)) {
|
||||||
|
$role = 'shipper';
|
||||||
|
}
|
||||||
|
$isAjax = isset($_GET['ajax']) || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
|
||||||
|
|
||||||
|
// Permission check
|
||||||
|
$perm = $role === 'shipper' ? 'manage_shippers' : 'manage_truck_owners';
|
||||||
|
if (!has_permission($perm)) {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo '<div class="alert alert-danger">Access Denied.</div>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
render_header(t('user_registration'), 'admin');
|
||||||
|
echo '<div class="container py-5"><div class="alert alert-danger">Access Denied.</div></div>';
|
||||||
|
render_footer();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$values = [
|
||||||
|
'full_name' => '',
|
||||||
|
'email' => '',
|
||||||
|
'phone' => '',
|
||||||
|
'country_id' => '',
|
||||||
|
'city_id' => '',
|
||||||
|
'address_line' => '',
|
||||||
|
'company_name' => '',
|
||||||
|
'bank_account' => '',
|
||||||
|
'bank_name' => '',
|
||||||
|
'bank_branch' => '',
|
||||||
|
'is_company' => '0',
|
||||||
|
'ctr_number' => '',
|
||||||
|
'notes' => '',
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
$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') {
|
||||||
|
if (!$isAjax) validate_csrf_token(); // CSRF token usually passed in form, but for AJAX we might rely on cookie or verify it if passed
|
||||||
|
|
||||||
|
$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'] ?? '');
|
||||||
|
$status = $_POST['status'] ?? 'active';
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'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,
|
||||||
|
'bank_account' => trim($_POST['bank_account'] ?? ''),
|
||||||
|
'bank_name' => trim($_POST['bank_name'] ?? ''),
|
||||||
|
'bank_branch' => trim($_POST['bank_branch'] ?? ''),
|
||||||
|
'is_company' => isset($_POST['is_company']) ? '1' : '0',
|
||||||
|
'ctr_number' => trim($_POST['ctr_number'] ?? ''),
|
||||||
|
'notes' => trim($_POST['notes'] ?? ''),
|
||||||
|
'status' => $status,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($fullName === '') $errors[] = t('error_required');
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = t('error_invalid') . ' (Email)';
|
||||||
|
if ($phone === '') $errors[] = t('error_required');
|
||||||
|
if ($countryId <= 0 || $cityId <= 0) $errors[] = t('error_required');
|
||||||
|
if ($addressLine === '') $errors[] = t('error_required');
|
||||||
|
if (strlen($passwordRaw) < 6) $errors[] = t('password_too_short');
|
||||||
|
if ($role === 'shipper' && $companyName === '') $errors[] = t('error_required');
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$password = password_hash($passwordRaw, PASSWORD_DEFAULT);
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$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 {
|
||||||
|
// Truck Owner
|
||||||
|
$uploadDir = __DIR__ . '/uploads/profiles/' . $userId . '/';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0775, true);
|
||||||
|
|
||||||
|
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
|
||||||
|
$saveImage = function ($fileKey, $prefix) use ($uploadDir, $allowed) {
|
||||||
|
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) return null;
|
||||||
|
$tmpName = $_FILES[$fileKey]['tmp_name'];
|
||||||
|
$mime = mime_content_type($tmpName) ?: '';
|
||||||
|
if (!isset($allowed[$mime])) return null;
|
||||||
|
$filename = uniqid($prefix, true) . '.' . $allowed[$mime];
|
||||||
|
move_uploaded_file($tmpName, $uploadDir . $filename);
|
||||||
|
return 'uploads/profiles/' . basename($uploadDir) . '/' . $filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
$ctrPath = null;
|
||||||
|
$idCardPaths = [];
|
||||||
|
|
||||||
|
if ($values['is_company'] === '1') {
|
||||||
|
$ctrPath = $saveImage('ctr_document', 'ctr_');
|
||||||
|
} else {
|
||||||
|
$f = $saveImage('id_card_front', 'id_front_');
|
||||||
|
if ($f) $idCardPaths[] = $f;
|
||||||
|
$b = $saveImage('id_card_back', 'id_back_');
|
||||||
|
if ($b) $idCardPaths[] = $b;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerStmt = $pdo->prepare(
|
||||||
|
"INSERT INTO truck_owner_profiles (user_id, phone, country_id, city_id, address_line, bank_account, bank_name, bank_branch, id_card_path, is_company, ctr_number, notes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$ownerStmt->execute([
|
||||||
|
$userId,
|
||||||
|
$phone,
|
||||||
|
$countryId,
|
||||||
|
$cityId,
|
||||||
|
$addressLine,
|
||||||
|
$values['bank_account'],
|
||||||
|
$values['bank_name'],
|
||||||
|
$values['bank_branch'],
|
||||||
|
$values['is_company'] === '1' ? $ctrPath : json_encode($idCardPaths, JSON_UNESCAPED_SLASHES),
|
||||||
|
$values['is_company'],
|
||||||
|
$values['ctr_number'],
|
||||||
|
$values['notes']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
set_flash('success', t('create_success'));
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($role === 'shipper') {
|
||||||
|
header('Location: admin_shippers.php');
|
||||||
|
} else {
|
||||||
|
header('Location: admin_truck_owners.php');
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||||
|
$err = t('error_email_exists') ?: 'Email already exists.';
|
||||||
|
} else {
|
||||||
|
$err = $e->getMessage();
|
||||||
|
}
|
||||||
|
$errors[] = $err;
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('<br>', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => false, 'message' => implode('<br>', $errors)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = $role === 'shipper' ? t('create_shipper') : t('create_owner');
|
||||||
|
|
||||||
|
// --- Render Logic ---
|
||||||
|
|
||||||
|
if ($isAjax) {
|
||||||
|
// Return only the form HTML for the modal
|
||||||
|
?>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><?= e($pageTitle) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="form-errors" class="alert alert-danger d-none"></div>
|
||||||
|
<form action="admin_user_create.php?role=<?= e($role) ?>&ajax=1" method="post" enctype="multipart/form-data">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<h5 class="mb-3"><?= e(t('account_role')) ?>: <span class="text-primary"><?= e(ucfirst(str_replace('_', ' ', $role))) ?></span></h5>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('full_name')) ?></label>
|
||||||
|
<input type="text" name="full_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('email')) ?></label>
|
||||||
|
<input type="email" name="email" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('phone')) ?></label>
|
||||||
|
<input type="text" name="phone" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('password')) ?></label>
|
||||||
|
<input type="password" name="password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('status')) ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value="active"><?= e(t('active')) ?></option>
|
||||||
|
<option value="pending"><?= e(t('pending')) ?></option>
|
||||||
|
<option value="rejected"><?= e(t('rejected')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('location')) ?></h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('country')) ?></label>
|
||||||
|
<select name="country_id" id="country_id_create" class="form-select" onchange="syncCitiesCreate()" required>
|
||||||
|
<option value=""><?= e(t('select_country')) ?></option>
|
||||||
|
<?php foreach ($countries as $country): ?>
|
||||||
|
<option value="<?= e((string)$country['id']) ?>">
|
||||||
|
<?= 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"><?= e(t('city')) ?></label>
|
||||||
|
<select name="city_id" id="city_id_create" class="form-select" required>
|
||||||
|
<option value=""><?= e(t('select_city')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('address')) ?></label>
|
||||||
|
<input type="text" name="address_line" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($role === 'shipper'): ?>
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('shipper_details')) ?></h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label"><?= e(t('company_name')) ?></label>
|
||||||
|
<input type="text" name="company_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('truck_details')) ?> / <?= e(t('profile')) ?></h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_company" id="is_company_create" value="1" onchange="toggleCompanyFieldsCreate()">
|
||||||
|
<label class="form-check-label" for="is_company_create"><?= e(t('is_company_checkbox')) ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_account')) ?></label>
|
||||||
|
<input type="text" name="bank_account" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_name')) ?></label>
|
||||||
|
<input type="text" name="bank_name" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_branch')) ?></label>
|
||||||
|
<input type="text" name="bank_branch" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="individualDocsCreate" class="row g-3 mt-0">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('id_card_front')) ?></label>
|
||||||
|
<input type="file" name="id_card_front" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('id_card_back')) ?></label>
|
||||||
|
<input type="file" name="id_card_back" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="companyDocsCreate" class="row g-3 mt-0" style="display:none;">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('ctr_number')) ?></label>
|
||||||
|
<input type="text" name="ctr_number" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('ctr_document')) ?></label>
|
||||||
|
<input type="file" name="ctr_document" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label"><?= e(t('notes')) ?></label>
|
||||||
|
<textarea name="notes" class="form-control"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mt-4 text-end">
|
||||||
|
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal"><?= e(t('cancel')) ?></button>
|
||||||
|
<button type="submit" class="btn btn-primary px-4"><?= e(t('create_account')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var allCitiesCreate = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
function syncCitiesCreate() {
|
||||||
|
var countryId = document.getElementById('country_id_create').value;
|
||||||
|
var citySelect = document.getElementById('city_id_create');
|
||||||
|
citySelect.innerHTML = '<option value=""><?= e(t('select_city')) ?></option>';
|
||||||
|
allCitiesCreate.forEach((city) => {
|
||||||
|
if (String(city.country_id) !== String(countryId)) return;
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = city.id;
|
||||||
|
option.textContent = '<?= $lang ?>' === 'ar' && city.name_ar ? city.name_ar : (city.name_en || city.name_ar);
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<?php if ($role === 'truck_owner'): ?>
|
||||||
|
function toggleCompanyFieldsCreate() {
|
||||||
|
var isCompany = document.getElementById('is_company_create').checked;
|
||||||
|
document.getElementById('individualDocsCreate').style.display = isCompany ? 'none' : 'flex';
|
||||||
|
document.getElementById('companyDocsCreate').style.display = isCompany ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header($pageTitle, 'admin', true);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-2 bg-white border-end min-vh-100">
|
||||||
|
<?php render_admin_sidebar($role === 'shipper' ? 'shippers' : 'truck_owners'); ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10 p-4">
|
||||||
|
<!-- Fallback for non-JS users or direct link -->
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<a href="<?= $role === 'shipper' ? 'admin_shippers.php' : 'admin_truck_owners.php' ?>" class="text-decoration-none small text-muted mb-2 d-inline-block">← <?= e(t('back')) ?></a>
|
||||||
|
<h1 class="section-title mb-1"><?= e($pageTitle) ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="panel p-4">
|
||||||
|
<form method="post" enctype="multipart/form-data"> <?= csrf_field() ?>
|
||||||
|
<!-- This fallback form mirrors the modal form above but with unique IDs if necessary, or just keeping the original code -->
|
||||||
|
<!-- ... (Original Form Content) ... -->
|
||||||
|
<!-- Ideally, we should include the same form structure here, but for now, since the user asked for a modal,
|
||||||
|
the primary interaction will be via AJAX. I will keep the original form for robustness. -->
|
||||||
|
|
||||||
|
<h5 class="mb-3"><?= e(t('account_role')) ?>: <span class="text-primary"><?= e(ucfirst(str_replace('_', ' ', $role))) ?></span></h5>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('full_name')) ?></label>
|
||||||
|
<input type="text" name="full_name" class="form-control" value="<?= e($values['full_name']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('email')) ?></label>
|
||||||
|
<input type="email" name="email" class="form-control" value="<?= e($values['email']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('phone')) ?></label>
|
||||||
|
<input type="text" name="phone" class="form-control" value="<?= e($values['phone']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('password')) ?></label>
|
||||||
|
<input type="password" name="password" class="form-control" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('status')) ?></label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value="active" <?= $values['status'] === 'active' ? 'selected' : '' ?>><?= e(t('active')) ?></option>
|
||||||
|
<option value="pending" <?= $values['status'] === 'pending' ? 'selected' : '' ?>><?= e(t('pending')) ?></option>
|
||||||
|
<option value="rejected" <?= $values['status'] === 'rejected' ? 'selected' : '' ?>><?= e(t('rejected')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('location')) ?></h5>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('country')) ?></label>
|
||||||
|
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||||
|
<option value=""><?= e(t('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-4">
|
||||||
|
<label class="form-label"><?= e(t('city')) ?></label>
|
||||||
|
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e($values['city_id']) ?>">
|
||||||
|
<option value=""><?= e(t('select_city')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('address')) ?></label>
|
||||||
|
<input type="text" name="address_line" class="form-control" value="<?= e($values['address_line']) ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($role === 'shipper'): ?>
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('shipper_details')) ?></h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('company_name')) ?></label>
|
||||||
|
<input type="text" name="company_name" class="form-control" value="<?= e($values['company_name']) ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<h5 class="mb-3 border-top pt-3"><?= e(t('truck_details')) ?> / <?= e(t('profile')) ?></h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_company" id="is_company" value="1" <?= $values['is_company'] === '1' ? 'checked' : '' ?> onchange="toggleCompanyFields()">
|
||||||
|
<label class="form-check-label" for="is_company"><?= e(t('is_company_checkbox')) ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_account')) ?></label>
|
||||||
|
<input type="text" name="bank_account" class="form-control" value="<?= e($values['bank_account']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_name')) ?></label>
|
||||||
|
<input type="text" name="bank_name" class="form-control" value="<?= e($values['bank_name']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label"><?= e(t('bank_branch')) ?></label>
|
||||||
|
<input type="text" name="bank_branch" class="form-control" value="<?= e($values['bank_branch']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="individualDocs" class="row g-3 mt-0">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('id_card_front')) ?></label>
|
||||||
|
<input type="file" name="id_card_front" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('id_card_back')) ?></label>
|
||||||
|
<input type="file" name="id_card_back" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="companyDocs" class="row g-3 mt-0" style="display:none;">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('ctr_number')) ?></label>
|
||||||
|
<input type="text" name="ctr_number" class="form-control" value="<?= e($values['ctr_number']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label"><?= e(t('ctr_document')) ?></label>
|
||||||
|
<input type="file" name="ctr_document" class="form-control" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label"><?= e(t('notes')) ?></label>
|
||||||
|
<textarea name="notes" class="form-control"><?= e($values['notes']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mt-4 text-end">
|
||||||
|
<button type="submit" class="btn btn-primary px-4"><?= e(t('create_account')) ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
function syncCities() {
|
||||||
|
var countryId = document.getElementById('country_id').value;
|
||||||
|
var citySelect = document.getElementById('city_id');
|
||||||
|
var selectedValue = citySelect.dataset.selected || '';
|
||||||
|
citySelect.innerHTML = '<option value=""><?= e(t('select_city')) ?></option>';
|
||||||
|
allCities.forEach((city) => {
|
||||||
|
if (String(city.country_id) !== String(countryId)) return;
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = city.id;
|
||||||
|
option.textContent = '<?= $lang ?>' === 'ar' && city.name_ar ? city.name_ar : (city.name_en || city.name_ar);
|
||||||
|
if (String(city.id) === String(selectedValue)) option.selected = true;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
citySelect.dataset.selected = '';
|
||||||
|
}
|
||||||
|
syncCities();
|
||||||
|
|
||||||
|
<?php if ($role === 'truck_owner'): ?>
|
||||||
|
function toggleCompanyFields() {
|
||||||
|
var isCompany = document.getElementById('is_company').checked;
|
||||||
|
document.getElementById('individualDocs').style.display = isCompany ? 'none' : 'flex';
|
||||||
|
document.getElementById('companyDocs').style.display = isCompany ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
toggleCompanyFields();
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
20
api/get_cities.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/app.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$countryId = isset($_GET['country_id']) ? (int)$_GET['country_id'] : 0;
|
||||||
|
|
||||||
|
if ($countryId <= 0) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("SELECT id, name_en, name_ar FROM cities WHERE country_id = ? ORDER BY name_en ASC");
|
||||||
|
$stmt->execute([$countryId]);
|
||||||
|
$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($cities);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
BIN
assets/images/hero_trucks.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
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
@ -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
@ -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 |
BIN
assets/images/workflow_trucks.jpg
Normal file
|
After Width: | Height: | Size: 286 KiB |
@ -1,39 +1,11 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const chatForm = document.getElementById('chat-form');
|
const autoAlerts = document.querySelectorAll('[data-auto-dismiss="true"]');
|
||||||
const chatInput = document.getElementById('chat-input');
|
if (autoAlerts.length) {
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
setTimeout(() => {
|
||||||
|
autoAlerts.forEach((alert) => {
|
||||||
const appendMessage = (text, sender) => {
|
alert.classList.add('fade');
|
||||||
const msgDiv = document.createElement('div');
|
setTimeout(() => alert.remove(), 500);
|
||||||
msgDiv.classList.add('message', sender);
|
|
||||||
msgDiv.textContent = text;
|
|
||||||
chatMessages.appendChild(msgDiv);
|
|
||||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
chatForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const message = chatInput.value.trim();
|
|
||||||
if (!message) return;
|
|
||||||
|
|
||||||
appendMessage(message, 'visitor');
|
|
||||||
chatInput.value = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('api/chat.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message })
|
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
}, 4500);
|
||||||
|
}
|
||||||
// Artificial delay for realism
|
|
||||||
setTimeout(() => {
|
|
||||||
appendMessage(data.reply, 'bot');
|
|
||||||
}, 500);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
11
check_status_enum.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SHOW COLUMNS FROM shipments LIKE 'status'");
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
echo "Type: " . $row['Type'] . "\n";
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,17 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
// Generated by install.php (or setup)
|
||||||
define('DB_HOST', '127.0.0.1');
|
|
||||||
define('DB_NAME', 'app_39038');
|
|
||||||
define('DB_USER', 'app_39038');
|
|
||||||
define('DB_PASS', 'a977aba0-df4f-448b-91d3-b1efdb7d9c61');
|
|
||||||
|
|
||||||
function db() {
|
if (!defined('DB_HOST')) {
|
||||||
static $pdo;
|
define('DB_HOST', '127.0.0.1');
|
||||||
if (!$pdo) {
|
}
|
||||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
if (!defined('DB_NAME')) {
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
define('DB_NAME', 'app_39038');
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
}
|
||||||
]);
|
if (!defined('DB_USER')) {
|
||||||
}
|
define('DB_USER', 'app_39038');
|
||||||
return $pdo;
|
}
|
||||||
|
if (!defined('DB_PASS')) {
|
||||||
|
define('DB_PASS', 'a977aba0-df4f-448b-91d3-b1efdb7d9c61');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('db')) {
|
||||||
|
function db() {
|
||||||
|
static $pdo;
|
||||||
|
if (!$pdo) {
|
||||||
|
$dsn = 'mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4';
|
||||||
|
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
db/migrate.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
// db/migrate.php - Run pending migrations
|
||||||
|
|
||||||
|
// Ensure we are in the correct directory context if run from CLI
|
||||||
|
if (php_sapi_name() === 'cli' && !defined('DB_HOST')) {
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
} elseif (!defined('DB_HOST') && file_exists(__DIR__ . '/config.php')) {
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_migrations() {
|
||||||
|
$pdo = db();
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
// 1. Create migrations table if not exists
|
||||||
|
try {
|
||||||
|
$pdo->exec(
|
||||||
|
"\n CREATE TABLE IF NOT EXISTS migrations (\n id INT AUTO_INCREMENT PRIMARY KEY,\n migration_name VARCHAR(255) NOT NULL UNIQUE,\n executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n "
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ["Error creating migrations table: " . $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get executed migrations
|
||||||
|
$executed = [];
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query("SELECT migration_name FROM migrations");
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$executed[] = $row['migration_name'];
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ["Error fetching executed migrations: " . $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Scan for migration files
|
||||||
|
$migrationFiles = glob(__DIR__ . '/migrations/*.php');
|
||||||
|
if ($migrationFiles === false) {
|
||||||
|
return ["Error scanning migration directory."];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort files to ensure order (by name usually works if named correctly)
|
||||||
|
sort($migrationFiles);
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($migrationFiles as $file) {
|
||||||
|
$filename = basename($file);
|
||||||
|
|
||||||
|
if (in_array($filename, $executed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
try {
|
||||||
|
// We use output buffering to capture echo output from migration files
|
||||||
|
ob_start();
|
||||||
|
// Include inside a closure/function scope to avoid variable collisions
|
||||||
|
// but we need to ensure $pdo is available if they use it.
|
||||||
|
// Most files do: require config.php; $pdo = db();
|
||||||
|
// Since we already required config.php, require_once will skip it.
|
||||||
|
// So they will just get $pdo = db(); which works.
|
||||||
|
include $file;
|
||||||
|
$output = ob_get_clean();
|
||||||
|
|
||||||
|
// Log success
|
||||||
|
$messages[] = "Executed $filename: " . trim($output);
|
||||||
|
|
||||||
|
// Record in DB
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
|
||||||
|
$stmt->execute([$filename]);
|
||||||
|
$count++;
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
ob_end_clean(); // Clean buffer if error
|
||||||
|
$messages[] = "Failed to execute $filename: " . $e->getMessage();
|
||||||
|
return $messages; // Stop on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count === 0) {
|
||||||
|
$messages[] = "No new migrations to run.";
|
||||||
|
} else {
|
||||||
|
$messages[] = "Successfully ran $count migrations.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If run directly from CLI
|
||||||
|
if (php_sapi_name() === 'cli' && basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
|
||||||
|
$results = run_migrations();
|
||||||
|
echo implode("\n", $results) . "\n";
|
||||||
|
}
|
||||||
24
db/migrations/add_arabic_landing_fields.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE landing_sections ADD COLUMN title_ar VARCHAR(255) NULL AFTER title");
|
||||||
|
echo "Added title_ar.\n";
|
||||||
|
} catch (PDOException $e) { echo "title_ar likely exists.\n"; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE landing_sections ADD COLUMN subtitle_ar TEXT NULL AFTER subtitle");
|
||||||
|
echo "Added subtitle_ar.\n";
|
||||||
|
} catch (PDOException $e) { echo "subtitle_ar likely exists.\n"; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE landing_sections ADD COLUMN content_ar TEXT NULL AFTER content");
|
||||||
|
echo "Added content_ar.\n";
|
||||||
|
} catch (PDOException $e) { echo "content_ar likely exists.\n"; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE landing_sections ADD COLUMN button_text_ar VARCHAR(100) NULL AFTER button_text");
|
||||||
|
echo "Added button_text_ar.\n";
|
||||||
|
} catch (PDOException $e) { echo "button_text_ar likely exists.\n"; }
|
||||||
|
|
||||||
12
db/migrations/add_bank_fields.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->exec("ALTER TABLE truck_owner_profiles ADD COLUMN bank_account VARCHAR(100) NULL AFTER plate_no;");
|
||||||
|
$pdo->exec("ALTER TABLE truck_owner_profiles ADD COLUMN bank_name VARCHAR(100) NULL AFTER bank_account;");
|
||||||
|
$pdo->exec("ALTER TABLE truck_owner_profiles ADD COLUMN bank_branch VARCHAR(100) NULL AFTER bank_name;");
|
||||||
|
echo "Columns added.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
14
db/migrations/add_company_details_to_profiles.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("
|
||||||
|
ALTER TABLE truck_owner_profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS ctr_number VARCHAR(100) DEFAULT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL;
|
||||||
|
");
|
||||||
|
echo "Schema updated successfully.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
||||||
25
db/migrations/add_landing_sections.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
try {
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS landing_sections (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
section_type VARCHAR(50) NOT NULL DEFAULT 'custom',
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
subtitle TEXT NULL,
|
||||||
|
content TEXT NULL,
|
||||||
|
image_path VARCHAR(255) NULL,
|
||||||
|
layout ENUM('text_left', 'text_right', 'center') NOT NULL DEFAULT 'text_left',
|
||||||
|
button_text VARCHAR(100) NULL,
|
||||||
|
button_link VARCHAR(255) NULL,
|
||||||
|
section_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
echo "landing_sections table created.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
70
db/migrations/add_notification_templates.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the table
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
event_name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email_subject_en VARCHAR(255),
|
||||||
|
email_body_en TEXT,
|
||||||
|
email_subject_ar VARCHAR(255),
|
||||||
|
email_body_ar TEXT,
|
||||||
|
whatsapp_body_en TEXT,
|
||||||
|
whatsapp_body_ar TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
");
|
||||||
|
|
||||||
|
// Insert default templates
|
||||||
|
$defaults = [
|
||||||
|
[
|
||||||
|
'event_name' => 'shipment_created',
|
||||||
|
'email_subject_en' => 'Shipment Created: #{shipment_id}',
|
||||||
|
'email_body_en' => "Dear {user_name},\n\nYour shipment from {origin} to {destination} has been successfully created.\n\nShipment ID: {shipment_id}\n\nWe will notify you when a truck owner submits an offer.",
|
||||||
|
'email_subject_ar' => 'تم إنشاء الشحنة: #{shipment_id}',
|
||||||
|
'email_body_ar' => "عزيزي {user_name}،\n\nتم إنشاء شحنتك من {origin} إلى {destination} بنجاح.\n\nرقم الشحنة: {shipment_id}\n\nسنقوم بإعلامك عند تقديم عرض من قبل صاحب شاحنة.",
|
||||||
|
'whatsapp_body_en' => "Shipment #{shipment_id} created successfully from {origin} to {destination}.",
|
||||||
|
'whatsapp_body_ar' => "تم إنشاء الشحنة #{shipment_id} بنجاح من {origin} إلى {destination}."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_name' => 'shipment_offered',
|
||||||
|
'email_subject_en' => 'New Offer for Shipment #{shipment_id}',
|
||||||
|
'email_body_en' => "Dear {user_name},\n\nA truck owner has submitted an offer for your shipment #{shipment_id}.\n\nOffer Price: {offer_price}\nTruck Owner: {offer_owner}\n\nPlease login to your dashboard to accept or reject this offer.",
|
||||||
|
'email_subject_ar' => 'عرض جديد للشحنة #{shipment_id}',
|
||||||
|
'email_body_ar' => "عزيزي {user_name}،\n\nلقد قدم صاحب شاحنة عرضًا لشحنتك #{shipment_id}.\n\nسعر العرض: {offer_price}\nصاحب الشاحنة: {offer_owner}\n\nيرجى تسجيل الدخول إلى لوحة التحكم لقبول أو رفض العرض.",
|
||||||
|
'whatsapp_body_en' => "New offer for shipment #{shipment_id}: {offer_price}. Check your dashboard.",
|
||||||
|
'whatsapp_body_ar' => "عرض جديد للشحنة #{shipment_id}: {offer_price}. تحقق من لوحة التحكم."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_name' => 'shipment_accepted',
|
||||||
|
'email_subject_en' => 'Offer Accepted: Shipment #{shipment_id}',
|
||||||
|
'email_body_en' => "Dear {user_name},\n\nYour offer for shipment #{shipment_id} has been accepted and paid for by the shipper.\n\nPlease proceed with the shipment.",
|
||||||
|
'email_subject_ar' => 'تم قبول العرض: الشحنة #{shipment_id}',
|
||||||
|
'email_body_ar' => "عزيزي {user_name}،\n\nتم قبول عرضك للشحنة #{shipment_id} ودفع ثمنه من قبل الشاحن.\n\nيرجى المتابعة في إجراءات الشحن.",
|
||||||
|
'whatsapp_body_en' => "Your offer for shipment #{shipment_id} was accepted. Please proceed.",
|
||||||
|
'whatsapp_body_ar' => "تم قبول عرضك للشحنة #{shipment_id}. يرجى المتابعة."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'event_name' => 'shipment_rejected',
|
||||||
|
'email_subject_en' => 'Offer Rejected: Shipment #{shipment_id}',
|
||||||
|
'email_body_en' => "Dear {user_name},\n\nYour offer for shipment #{shipment_id} was not accepted by the shipper.\n\nThe shipment is now back to 'Posted' status.",
|
||||||
|
'email_subject_ar' => 'تم رفض العرض: الشحنة #{shipment_id}',
|
||||||
|
'email_body_ar' => "عزيزي {user_name}،\n\nلم يتم قبول عرضك للشحنة #{shipment_id} من قبل الشاحن.\n\nعادت الشحنة الآن إلى حالة 'منشورة'.",
|
||||||
|
'whatsapp_body_en' => "Your offer for shipment #{shipment_id} was rejected.",
|
||||||
|
'whatsapp_body_ar' => "تم رفض عرضك للشحنة #{shipment_id}."
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT IGNORE INTO notification_templates (event_name, email_subject_en, email_body_en, email_subject_ar, email_body_ar, whatsapp_body_en, whatsapp_body_ar) VALUES (:event_name, :email_subject_en, :email_body_en, :email_subject_ar, :email_body_ar, :whatsapp_body_en, :whatsapp_body_ar)");
|
||||||
|
|
||||||
|
foreach ($defaults as $d) {
|
||||||
|
$stmt->execute($d);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Notification templates table created and defaults inserted.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
||||||
13
db/migrations/add_pending_approval_status.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "Adding 'pending_approval' to shipments status enum...\n";
|
||||||
|
// MariaDB/MySQL ALTER TABLE to modify column
|
||||||
|
$sql = "ALTER TABLE shipments MODIFY COLUMN status ENUM('pending_approval', 'posted','offered','confirmed','in_transit','delivered') NOT NULL DEFAULT 'posted'";
|
||||||
|
db()->exec($sql);
|
||||||
|
echo "Migration successful.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Migration failed: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
67
db/migrations/add_platform_permissions.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Create permissions table
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
// 2. Create user_permissions table
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS user_permissions (
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
permission_id INT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, permission_id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
|
||||||
|
// 3. Seed default permissions
|
||||||
|
$permissions = [
|
||||||
|
['slug' => 'manage_platform_users', 'name' => 'Manage Platform Users', 'description' => 'Create and manage admin users and their permissions.'],
|
||||||
|
['slug' => 'manage_shippers', 'name' => 'Manage Shippers', 'description' => 'View, approve, and edit shipper accounts.'],
|
||||||
|
['slug' => 'manage_truck_owners', 'name' => 'Manage Truck Owners', 'description' => 'View, approve, and edit truck owner accounts.'],
|
||||||
|
['slug' => 'manage_shipments', 'name' => 'Manage Shipments', 'description' => 'View and edit shipments.'],
|
||||||
|
['slug' => 'manage_content', 'name' => 'Manage Content', 'description' => 'Edit FAQs, Landing Pages, and other content.'],
|
||||||
|
['slug' => 'manage_settings', 'name' => 'Manage Settings', 'description' => 'Edit global application settings.'],
|
||||||
|
['slug' => 'view_reports', 'name' => 'View Reports', 'description' => 'Access dashboard reports and statistics.']
|
||||||
|
];
|
||||||
|
|
||||||
|
$stmtInsert = $pdo->prepare("INSERT IGNORE INTO permissions (slug, name, description) VALUES (:slug, :name, :description)");
|
||||||
|
|
||||||
|
foreach ($permissions as $perm) {
|
||||||
|
$stmtInsert->execute($perm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Assign all permissions to existing admins
|
||||||
|
// First, get all permission IDs
|
||||||
|
$stmtPerms = $pdo->query("SELECT id FROM permissions");
|
||||||
|
$allPermIds = $stmtPerms->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Get all admin users
|
||||||
|
$stmtAdmins = $pdo->query("SELECT id FROM users WHERE role = 'admin'");
|
||||||
|
$adminIds = $stmtAdmins->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$stmtAssign = $pdo->prepare("INSERT IGNORE INTO user_permissions (user_id, permission_id) VALUES (:uid, :pid)");
|
||||||
|
|
||||||
|
foreach ($adminIds as $uid) {
|
||||||
|
foreach ($allPermIds as $pid) {
|
||||||
|
$stmtAssign->execute(['uid' => $uid, 'pid' => $pid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Permissions tables created and seeded successfully.";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
||||||
12
db/migrations/add_pod_file_to_shipments.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->exec("ALTER TABLE shipments ADD COLUMN pod_file VARCHAR(255) DEFAULT NULL;");
|
||||||
|
echo "Added pod_file column to shipments table.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Column might already exist
|
||||||
|
echo "Error (or column exists): " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
37
db/migrations/add_pricing_model.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add target_price column to shipments if it doesn't exist
|
||||||
|
$pdo->exec("
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = 'shipments';
|
||||||
|
SET @columnname = 'target_price';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
(table_name = @tablename)
|
||||||
|
AND (table_schema = @dbname)
|
||||||
|
AND (column_name = @columnname)
|
||||||
|
) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE shipments ADD COLUMN target_price DECIMAL(10,2) DEFAULT NULL AFTER weight_tons'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
");
|
||||||
|
echo "Added target_price column to shipments.\n";
|
||||||
|
|
||||||
|
// Insert default pricing_model setting
|
||||||
|
$pdo->exec("
|
||||||
|
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('pricing_model', 'percentage');
|
||||||
|
");
|
||||||
|
echo "Added pricing_model setting.\n";
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
15
db/migrations/add_profile_picture_to_users.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE users ADD COLUMN profile_picture VARCHAR(255) NULL AFTER full_name");
|
||||||
|
echo "Added profile_picture column to users table.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() == '42S21') {
|
||||||
|
echo "profile_picture column already exists.\n";
|
||||||
|
} else {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
26
db/migrations/add_settings_table.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
try {
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
setting_key VARCHAR(50) PRIMARY KEY,
|
||||||
|
setting_value TEXT
|
||||||
|
);
|
||||||
|
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
|
||||||
|
('company_name', 'My Transport Company'),
|
||||||
|
('company_email', 'info@example.com'),
|
||||||
|
('company_phone', '+1234567890'),
|
||||||
|
('company_address', '123 Transport St, City, Country'),
|
||||||
|
('platform_charge_percentage', '0'),
|
||||||
|
('logo_path', ''),
|
||||||
|
('favicon_path', ''),
|
||||||
|
('timezone', 'UTC'),
|
||||||
|
('whatsapp_enabled', '0'),
|
||||||
|
('wablas_domain', ''),
|
||||||
|
('wablas_api_token', '');
|
||||||
|
");
|
||||||
|
echo "Settings table created/updated.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
17
db/migrations/add_shipment_type_to_shipments.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if column exists
|
||||||
|
$stmt = $pdo->query("SHOW COLUMNS FROM shipments LIKE 'shipment_type'");
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
echo "Column 'shipment_type' already exists in 'shipments'.\n";
|
||||||
|
} else {
|
||||||
|
$pdo->exec("ALTER TABLE shipments ADD COLUMN shipment_type VARCHAR(50) DEFAULT 'Dry' AFTER cargo_description");
|
||||||
|
echo "Added 'shipment_type' column to 'shipments' table.\n";
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
28
db/migrations/add_trucks_table.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
// db/migrations/add_trucks_table.php
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
CREATE TABLE IF NOT EXISTS trucks (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
truck_type VARCHAR(120) NOT NULL,
|
||||||
|
load_capacity DECIMAL(10,2) NOT NULL,
|
||||||
|
plate_no VARCHAR(80) NOT NULL,
|
||||||
|
truck_pic_path VARCHAR(255) NULL,
|
||||||
|
registration_path VARCHAR(255) NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_truck_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Add is_company flag to truck_owner_profiles
|
||||||
|
ALTER TABLE truck_owner_profiles ADD COLUMN IF NOT EXISTS is_company BOOLEAN DEFAULT FALSE;
|
||||||
|
";
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
$db->exec($sql);
|
||||||
|
echo "Migration applied successfully.";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error applying migration: " . $e->getMessage();
|
||||||
|
}
|
||||||
15
db/migrations/add_user_ids_to_shipments.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("ALTER TABLE shipments ADD COLUMN shipper_id INT NULL AFTER id");
|
||||||
|
$pdo->exec("ALTER TABLE shipments ADD COLUMN truck_owner_id INT NULL AFTER offer_price");
|
||||||
|
|
||||||
|
// Add FK constraints (optional but good practice, keeping it simple for now to avoid issues with existing data)
|
||||||
|
// We won't enforce FK strictly on existing data as it might be NULL
|
||||||
|
|
||||||
|
echo "Added shipper_id and truck_owner_id to shipments table.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error (might already exist): " . $e->getMessage();
|
||||||
|
}
|
||||||
56
db/migrations/add_welcome_template.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
// db/migrations/add_welcome_template.php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$templates = [
|
||||||
|
[
|
||||||
|
'event_name' => 'welcome_message',
|
||||||
|
'email_subject_en' => 'Welcome to CargoLink!',
|
||||||
|
'email_body_en' => "Dear {user_name},\n\nWelcome to CargoLink! We are excited to have you on board.\n\nYou can now log in to your dashboard and start using our services.\n\nBest regards,\nThe CargoLink Team",
|
||||||
|
'email_subject_ar' => 'مرحباً بك في كارجو لينك!',
|
||||||
|
'email_body_ar' => "عزيزي {user_name}،\n\nمرحباً بك في كارجو لينك! نحن سعداء بانضمامك إلينا.\n\nيمكنك الآن تسجيل الدخول إلى لوحة التحكم والبدء في استخدام خدماتنا.\n\nتحياتنا،\nفريق كارجو لينك",
|
||||||
|
'whatsapp_body_en' => "Welcome to CargoLink, {user_name}! 🚚\n\nWe're glad to have you with us. Log in now to get started: https://cargolink.om",
|
||||||
|
'whatsapp_body_ar' => "مرحباً بك في كارجو لينك، {user_name}! 🚚\n\nسعداء بانضمامك إلينا. سجل الدخول الآن للبدء: https://cargolink.om"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($templates as $t) {
|
||||||
|
// Check if exists
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM notification_templates WHERE event_name = ?");
|
||||||
|
$stmt->execute([$t['event_name']]);
|
||||||
|
$existing = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$existing) {
|
||||||
|
$sql = "INSERT INTO notification_templates (event_name, email_subject_en, email_body_en, email_subject_ar, email_body_ar, whatsapp_body_en, whatsapp_body_ar) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
$pdo->prepare($sql)->execute([
|
||||||
|
$t['event_name'],
|
||||||
|
$t['email_subject_en'],
|
||||||
|
$t['email_body_en'],
|
||||||
|
$t['email_subject_ar'],
|
||||||
|
$t['email_body_ar'],
|
||||||
|
$t['whatsapp_body_en'],
|
||||||
|
$t['whatsapp_body_ar']
|
||||||
|
]);
|
||||||
|
echo "Added template: {$t['event_name']}\n";
|
||||||
|
} else {
|
||||||
|
// Update existing template to match file content (useful for development)
|
||||||
|
$sql = "UPDATE notification_templates SET
|
||||||
|
email_subject_en = ?, email_body_en = ?,
|
||||||
|
email_subject_ar = ?, email_body_ar = ?,
|
||||||
|
whatsapp_body_en = ?, whatsapp_body_ar = ?
|
||||||
|
WHERE event_name = ?";
|
||||||
|
$pdo->prepare($sql)->execute([
|
||||||
|
$t['email_subject_en'],
|
||||||
|
$t['email_body_en'],
|
||||||
|
$t['email_subject_ar'],
|
||||||
|
$t['email_body_ar'],
|
||||||
|
$t['whatsapp_body_en'],
|
||||||
|
$t['whatsapp_body_ar'],
|
||||||
|
$t['event_name']
|
||||||
|
]);
|
||||||
|
echo "Updated template: {$t['event_name']}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
35
db/migrations/initial_schema.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS countries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cities (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
country_id INT NOT NULL,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (country_id) REFERENCES countries(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
full_name VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin', 'shipper', 'truck_owner') NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
");
|
||||||
|
echo "Schema updated successfully.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
||||||
26
db/migrations/make_truck_fields_nullable.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
echo "Modifying truck_owner_profiles table...\n";
|
||||||
|
|
||||||
|
$queries = [
|
||||||
|
"ALTER TABLE truck_owner_profiles MODIFY COLUMN truck_type VARCHAR(120) NULL",
|
||||||
|
"ALTER TABLE truck_owner_profiles MODIFY COLUMN load_capacity DECIMAL(10,2) NULL",
|
||||||
|
"ALTER TABLE truck_owner_profiles MODIFY COLUMN plate_no VARCHAR(80) NULL",
|
||||||
|
"ALTER TABLE truck_owner_profiles MODIFY COLUMN truck_pic_path VARCHAR(255) NULL",
|
||||||
|
"ALTER TABLE truck_owner_profiles MODIFY COLUMN registration_path TEXT NULL"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($queries as $sql) {
|
||||||
|
$pdo->exec($sql);
|
||||||
|
echo "Executed: $sql\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migration completed successfully.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Migration failed: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
156
db/migrations/seed_landing_sections_data.php
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$sections = [
|
||||||
|
[
|
||||||
|
'section_type' => 'hero',
|
||||||
|
'title' => 'Fast & Reliable Freight Forwarding',
|
||||||
|
'title_ar' => 'شحن سريع وموثوق',
|
||||||
|
'subtitle' => 'Connect with trusted truck owners and shippers. Streamline your logistics today.',
|
||||||
|
'subtitle_ar' => 'تواصل مع أصحاب الشاحنات والمرسلين الموثوقين. قم بتبسيط لوجستياتك اليوم.',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => 'uploads/pages/img_69ad2bff407d4.jpg',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 10,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'stats',
|
||||||
|
'title' => 'Platform Stats',
|
||||||
|
'title_ar' => 'إحصائيات المنصة',
|
||||||
|
'subtitle' => '',
|
||||||
|
'subtitle_ar' => '',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => '',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 20,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'features',
|
||||||
|
'title' => 'Why Choose Us',
|
||||||
|
'title_ar' => 'لماذا تختارنا ؟',
|
||||||
|
'subtitle' => 'We connect you with the best logistics partners.',
|
||||||
|
'subtitle_ar' => 'نوصلك بأفضل شركاء الخدمات اللوجستية.',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => '',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 30,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'workflow',
|
||||||
|
'title' => 'How it Works',
|
||||||
|
'title_ar' => 'كيف تعمل المنصة',
|
||||||
|
'subtitle' => '',
|
||||||
|
'subtitle_ar' => '',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => 'uploads/pages/img_69ad2b68c8965.jfif',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 40,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'recent_shipments',
|
||||||
|
'title' => 'Recent Shipments',
|
||||||
|
'title_ar' => 'الشحنات الحالية',
|
||||||
|
'subtitle' => '',
|
||||||
|
'subtitle_ar' => '',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => '',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 50,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'faq',
|
||||||
|
'title' => 'Have Questions?',
|
||||||
|
'title_ar' => 'هل لديك تساؤل؟',
|
||||||
|
'subtitle' => 'Check out our Frequently Asked Questions to learn more about how our platform works.',
|
||||||
|
'subtitle_ar' => 'اطلع على أسئلتنا المتكررة لتتعرف أكثر على كيفية عمل منصتنا.',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => '',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 60,
|
||||||
|
'is_active' => 1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'section_type' => 'motivation',
|
||||||
|
'title' => 'Ready to transform your logistics?',
|
||||||
|
'title_ar' => 'هل أنت مستعد لتحويل لوجستياتك؟',
|
||||||
|
'subtitle' => 'Join our platform today to find reliable trucks or secure the best shipments in the market.',
|
||||||
|
'subtitle_ar' => 'انضم إلى منصتنا اليوم للعثور على شاحنات موثوقة أو تأمين أفضل الشحنات في السوق.',
|
||||||
|
'content' => '',
|
||||||
|
'content_ar' => '',
|
||||||
|
'image_path' => '',
|
||||||
|
'layout' => 'text_left',
|
||||||
|
'button_text' => '',
|
||||||
|
'button_text_ar' => '',
|
||||||
|
'button_link' => '',
|
||||||
|
'section_order' => 70,
|
||||||
|
'is_active' => 1
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sql = "INSERT INTO landing_sections (section_type, title, title_ar, subtitle, subtitle_ar, content, content_ar, image_path, layout, button_text, button_text_ar, button_link, section_order, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
foreach ($sections as $section) {
|
||||||
|
// Check if section already exists to avoid duplicates (idempotency)
|
||||||
|
// We check by title and section_type
|
||||||
|
$check = $pdo->prepare("SELECT id FROM landing_sections WHERE section_type = ? AND title = ?");
|
||||||
|
$check->execute([$section['section_type'], $section['title']]);
|
||||||
|
if ($check->fetch()) {
|
||||||
|
echo "Skipping existing section: " . $section['title'] . "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$section['section_type'],
|
||||||
|
$section['title'],
|
||||||
|
$section['title_ar'],
|
||||||
|
$section['subtitle'],
|
||||||
|
$section['subtitle_ar'],
|
||||||
|
$section['content'],
|
||||||
|
$section['content_ar'],
|
||||||
|
$section['image_path'],
|
||||||
|
$section['layout'],
|
||||||
|
$section['button_text'],
|
||||||
|
$section['button_text_ar'],
|
||||||
|
$section['button_link'],
|
||||||
|
$section['section_order'],
|
||||||
|
$section['is_active']
|
||||||
|
]);
|
||||||
|
echo "Inserted section: " . $section['title'] . "\n";
|
||||||
|
}
|
||||||
|
echo "Default landing sections seeded.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error seeding landing sections: " . $e->getMessage() . "\n";
|
||||||
|
throw $e; // Rethrow to fail migration
|
||||||
|
}
|
||||||
16
db/migrations/update_report_permission_description.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("UPDATE permissions SET description = ? WHERE slug = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
'Access to all reports, including Shipper and Truck Owner statements.',
|
||||||
|
'view_reports'
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Updated 'view_reports' permission description.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error updating permission: " . $e->getMessage();
|
||||||
|
}
|
||||||
64
faq.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if table exists to avoid errors on first load before admin visits it
|
||||||
|
$tableExists = db()->query("SHOW TABLES LIKE 'faqs'")->rowCount() > 0;
|
||||||
|
|
||||||
|
if ($tableExists) {
|
||||||
|
$faqs = db()->query("SELECT * FROM faqs ORDER BY sort_order ASC, id DESC")->fetchAll();
|
||||||
|
} else {
|
||||||
|
$faqs = [];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$faqs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Frequently Asked Questions', 'home');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8 text-center mb-5">
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Frequently Asked Questions</h1>
|
||||||
|
<p class="lead text-muted">Find answers to common questions about our platform and services.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<?php if (empty($faqs)): ?>
|
||||||
|
<div class="text-center p-5 bg-white rounded shadow-sm">
|
||||||
|
<i class="bi bi-info-circle display-4 text-muted mb-3 d-block"></i>
|
||||||
|
<p class="text-muted mb-0">No FAQs are currently available. Please check back later.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="accordion accordion-flush shadow-sm bg-white rounded-3 p-3" id="faqAccordion">
|
||||||
|
<?php foreach ($faqs as $index => $faq):
|
||||||
|
$question = $lang === 'ar' && !empty($faq['question_ar']) ? $faq['question_ar'] : $faq['question_en'];
|
||||||
|
$answer = $lang === 'ar' && !empty($faq['answer_ar']) ? $faq['answer_ar'] : $faq['answer_en'];
|
||||||
|
$collapseId = 'collapse' . $faq['id'];
|
||||||
|
$headingId = 'heading' . $faq['id'];
|
||||||
|
?>
|
||||||
|
<div class="accordion-item border-0 border-bottom mb-2">
|
||||||
|
<h2 class="accordion-header" id="<?= e($headingId) ?>">
|
||||||
|
<button class="accordion-button collapsed fw-semibold fs-5 bg-transparent" type="button" data-bs-toggle="collapse" data-bs-target="#<?= e($collapseId) ?>" aria-expanded="false" aria-controls="<?= e($collapseId) ?>">
|
||||||
|
<?= e($question) ?>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="<?= e($collapseId) ?>" class="accordion-collapse collapse" aria-labelledby="<?= e($headingId) ?>" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body text-muted lh-lg">
|
||||||
|
<?= nl2br(e($answer)) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
189
includes/NotificationService.php
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../mail/MailService.php';
|
||||||
|
require_once __DIR__ . '/app.php';
|
||||||
|
|
||||||
|
class NotificationService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send a notification for a specific event.
|
||||||
|
*
|
||||||
|
* @param string $eventName The event name (e.g., 'shipment_created')
|
||||||
|
* @param array $user The recipient user array (must contain 'id', 'email', 'full_name', 'role')
|
||||||
|
* @param array $data Data to replace in placeholders (e.g., ['shipment_id' => 123])
|
||||||
|
* @param string|null $lang 'en' or 'ar'. If null, sends both combined.
|
||||||
|
*/
|
||||||
|
public static function send(string $eventName, array $user, array $data = [], ?string $lang = null)
|
||||||
|
{
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM notification_templates WHERE event_name = ?");
|
||||||
|
$stmt->execute([$eventName]);
|
||||||
|
$template = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
error_log("Notification template not found: $eventName");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for replacement
|
||||||
|
$placeholders = [];
|
||||||
|
$values = [];
|
||||||
|
$data['user_name'] = $user['full_name'] ?? 'User';
|
||||||
|
|
||||||
|
foreach ($data as $key => $val) {
|
||||||
|
$placeholders[] = '{' . $key . '}';
|
||||||
|
$values[] = $val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Subject and Body based on Lang
|
||||||
|
$subject = '';
|
||||||
|
$body = '';
|
||||||
|
$whatsapp = '';
|
||||||
|
|
||||||
|
if ($lang === 'en') {
|
||||||
|
$subject = $template['email_subject_en'];
|
||||||
|
$body = $template['email_body_en'];
|
||||||
|
$whatsapp = $template['whatsapp_body_en'];
|
||||||
|
} elseif ($lang === 'ar') {
|
||||||
|
$subject = $template['email_subject_ar'];
|
||||||
|
$body = $template['email_body_ar'];
|
||||||
|
$whatsapp = $template['whatsapp_body_ar'];
|
||||||
|
} else {
|
||||||
|
// Combined
|
||||||
|
$subject = $template['email_subject_en'] . ' / ' . $template['email_subject_ar'];
|
||||||
|
$body = $template['email_body_en'] . "\n\n---\n\n" . $template['email_body_ar'];
|
||||||
|
$whatsapp = $template['whatsapp_body_en'] . "\n\n" . $template['whatsapp_body_ar'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace
|
||||||
|
$subject = str_replace($placeholders, $values, $subject);
|
||||||
|
$body = str_replace($placeholders, $values, $body);
|
||||||
|
$whatsapp = str_replace($placeholders, $values, $whatsapp);
|
||||||
|
|
||||||
|
// Send Email
|
||||||
|
if (!empty($user['email'])) {
|
||||||
|
// Convert newlines to BR for HTML
|
||||||
|
$htmlBody = nl2br(htmlspecialchars($body));
|
||||||
|
MailService::sendMail($user['email'], $subject, $htmlBody, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send WhatsApp if enabled
|
||||||
|
if (get_setting('whatsapp_enabled') === '1') {
|
||||||
|
$phone = self::getPhoneNumber($user);
|
||||||
|
if ($phone) {
|
||||||
|
// Return value ignored in async/background context, but logged inside
|
||||||
|
self::sendWhatsApp($phone, $whatsapp);
|
||||||
|
} else {
|
||||||
|
error_log("WHATSAPP Notification skipped for {$user['email']}: Phone number not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve phone number for a user, fetching from profile if necessary.
|
||||||
|
*/
|
||||||
|
private static function getPhoneNumber(array $user): ?string
|
||||||
|
{
|
||||||
|
if (!empty($user['phone'])) {
|
||||||
|
return $user['phone'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($user['id']) || empty($user['role'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
if ($user['role'] === 'shipper') {
|
||||||
|
$stmt = $pdo->prepare("SELECT phone FROM shipper_profiles WHERE user_id = ?");
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
return $stmt->fetchColumn() ?: null;
|
||||||
|
} elseif ($user['role'] === 'truck_owner') {
|
||||||
|
$stmt = $pdo->prepare("SELECT phone FROM truck_owner_profiles WHERE user_id = ?");
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
return $stmt->fetchColumn() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a WhatsApp message via Wablas API.
|
||||||
|
* @return array ['success' => bool, 'message' => string, 'response' => mixed]
|
||||||
|
*/
|
||||||
|
public static function sendWhatsApp(string $phone, string $message): array
|
||||||
|
{
|
||||||
|
// 1. Format Phone Number
|
||||||
|
// Remove non-numeric characters
|
||||||
|
$phone = preg_replace('/[^0-9]/', '', $phone);
|
||||||
|
|
||||||
|
// If 8 digits, prepend 968 (Oman country code)
|
||||||
|
if (strlen($phone) === 8) {
|
||||||
|
$phone = '968' . $phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get Settings
|
||||||
|
$domain = get_setting('wablas_domain');
|
||||||
|
$token = get_setting('wablas_api_token');
|
||||||
|
|
||||||
|
if (!$domain || !$token) {
|
||||||
|
error_log("WHATSAPP Error: Wablas domain or token not configured.");
|
||||||
|
return ['success' => false, 'message' => 'Domain or Token missing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure domain has no trailing slash and correct scheme
|
||||||
|
$domain = rtrim($domain, '/');
|
||||||
|
// If domain doesn't start with http/https, prepend https://
|
||||||
|
if (!preg_match('/^https?:\/\//', $domain)) {
|
||||||
|
$domain = 'https://' . $domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Send API Request
|
||||||
|
// Endpoint: /api/send-message (StandardWablas)
|
||||||
|
$url = $domain . "/api/send-message";
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'phone' => $phone,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
"Authorization: $token",
|
||||||
|
"Content-Type: application/json"
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||||
|
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlErr = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlErr) {
|
||||||
|
error_log("WHATSAPP cURL Error: $curlErr");
|
||||||
|
return ['success' => false, 'message' => "Connection error: $curlErr"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($response, true);
|
||||||
|
$statusMsg = $decoded['message'] ?? $response;
|
||||||
|
|
||||||
|
// Log result
|
||||||
|
error_log("WHATSAPP Sent to $phone. Status: $httpCode. Response: " . substr($response, 0, 100));
|
||||||
|
|
||||||
|
// Wablas usually returns {'status': true, ...} or {'status': 'pending'}
|
||||||
|
// We consider 200 OK and no explicit 'status': false as success for now
|
||||||
|
$isSuccess = ($httpCode >= 200 && $httpCode < 300);
|
||||||
|
if (isset($decoded['status']) && $decoded['status'] === false) {
|
||||||
|
$isSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $isSuccess,
|
||||||
|
'message' => "API ($httpCode): " . (is_string($statusMsg) ? $statusMsg : json_encode($statusMsg)),
|
||||||
|
'response' => $decoded
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
1020
includes/app.php
Normal file
382
includes/layout.php
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/app.php';
|
||||||
|
|
||||||
|
function render_header(string $title, string $active = '', bool $isFluid = false, bool $showNav = true): void
|
||||||
|
{
|
||||||
|
global $lang, $dir;
|
||||||
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
|
$toggleLang = $lang === 'ar' ? 'en' : 'ar';
|
||||||
|
$toggleLabel = $lang === 'ar' ? 'EN' : 'AR';
|
||||||
|
|
||||||
|
$appName = get_setting('company_name', t('app_name'));
|
||||||
|
$logoPath = get_setting('logo_path');
|
||||||
|
$faviconPath = get_setting('favicon_path');
|
||||||
|
|
||||||
|
$navUserPic = '';
|
||||||
|
$navUserName = 'Account';
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("SELECT full_name, profile_picture FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$u = $stmt->fetch();
|
||||||
|
if ($u) {
|
||||||
|
$navUserName = $u['full_name'];
|
||||||
|
$navUserPic = $u['profile_picture'];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="<?= e($lang) ?>" dir="<?= e($dir) ?>">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title><?= e($title) ?> | <?= e($appName) ?></title>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php if ($faviconPath): ?>
|
||||||
|
<link rel="icon" href="<?= e($faviconPath) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($projectDescription): ?>
|
||||||
|
<meta name="description" content="<?= e($projectDescription) ?>" />
|
||||||
|
<meta property="og:description" content="<?= e($projectDescription) ?>" />
|
||||||
|
<meta property="twitter:description" content="<?= e($projectDescription) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($projectImageUrl): ?>
|
||||||
|
<meta property="og:image" content="<?= e($projectImageUrl) ?>" />
|
||||||
|
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($dir === "rtl"): ?>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css">
|
||||||
|
<?php else: ?>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<?php endif; ?>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
|
||||||
|
</head>
|
||||||
|
<body class="app-body">
|
||||||
|
<?php if ($showNav): ?>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top shadow-sm py-3 z-3">
|
||||||
|
<div class="<?= $isFluid ? 'container-fluid px-4' : 'container' ?>">
|
||||||
|
<a class="navbar-brand fs-4 d-flex align-items-center" href="<?= e(url_with_lang('index.php')) ?>">
|
||||||
|
<?php if ($logoPath): ?>
|
||||||
|
<img src="<?= e($logoPath) ?>" alt="<?= e($appName) ?> Logo" height="32" class="me-2 rounded">
|
||||||
|
<?php else: ?>
|
||||||
|
<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>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= e($appName) ?>
|
||||||
|
</a>
|
||||||
|
<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 fw-semibold">
|
||||||
|
<li class="nav-item">
|
||||||
|
<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 === 'faq' ? 'active text-primary' : '' ?>" href="<?= e(url_with_lang('faq.php')) ?>">
|
||||||
|
FAQ
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
|
||||||
|
<!-- Theme Switcher -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-dark rounded-pill px-3 fw-bold dropdown-toggle" type="button" id="themeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-palette-fill me-1"></i> <?= e(t('nav_theme')) ?>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0 mt-3" aria-labelledby="themeDropdown" style="border-radius: 12px;">
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('light')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#f4f7f6;border:1px solid #ccc;"></span>Default</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('dark')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#0f172a;"></span>Dark</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('blue')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#2563eb;"></span>Blue</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('green')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#10b981;"></span>Green</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('purple')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#8b5cf6;"></span>Purple</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('red')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#ef4444;"></span>Red</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('orange')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#f97316;"></span>Orange</button></li>
|
||||||
|
<li><button class="dropdown-item py-2" onclick="setTheme('teal')"><span class="d-inline-block rounded-circle me-2" style="width:12px;height:12px;background:#14b8a6;"></span>Teal</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown">
|
||||||
|
<?php if (isset($_SESSION['user_id'])): ?>
|
||||||
|
<a class="text-decoration-none text-muted fw-semibold dropdown-toggle d-flex align-items-center gap-2" href="#" id="loginDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<?php if ($navUserPic): ?>
|
||||||
|
<img src="<?= e($navUserPic) ?>" alt="User" class="rounded-circle object-fit-cover shadow-sm" style="width:32px; height:32px;">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center shadow-sm text-secondary" style="width:32px; height:32px;">
|
||||||
|
<i class="bi bi-person-fill"></i>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="d-none d-sm-inline"><?= e($navUserName) ?></span>
|
||||||
|
</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 === 'profile' ? 'active' : '' ?>" href="<?= e(url_with_lang('profile.php')) ?>">
|
||||||
|
<i class="bi bi-person-circle text-primary me-2"></i><?= e(t('my_profile')) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php if (($_SESSION['user_role'] ?? '') === 'shipper'): ?>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item py-2 <?= $active === 'shipper' ? 'active' : '' ?>" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>">
|
||||||
|
<i class="bi bi-box-seam text-primary me-2"></i><?= e(t('nav_shipper')) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php elseif (($_SESSION['user_role'] ?? '') === 'truck_owner'): ?>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item py-2 <?= $active === 'owner' ? 'active' : '' ?>" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>">
|
||||||
|
<i class="bi bi-truck text-primary me-2"></i><?= e(t('nav_owner')) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php elseif (($_SESSION['user_role'] ?? '') === 'admin'): ?>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item py-2 <?= $active === 'admin' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">
|
||||||
|
<i class="bi bi-speedometer2 text-primary me-2"></i><?= e(t('nav_admin')) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item py-2 text-danger" href="<?= e(url_with_lang('logout.php')) ?>">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<a class="text-decoration-none text-muted fw-semibold" href="<?= e(url_with_lang('login.php')) ?>">
|
||||||
|
<?= e(t('nav_login')) ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<?php if (!isset($_SESSION['user_id'])): ?>
|
||||||
|
<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'])) ?>">
|
||||||
|
<?= e(t('nav_get_started')) ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
<main class="<?= $isFluid ? 'container-fluid p-0' : 'container py-5' ?>">
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_footer(bool $showFooter = true): void
|
||||||
|
{
|
||||||
|
global $lang;
|
||||||
|
$appName = get_setting('company_name', t('app_name'));
|
||||||
|
$logoPath = get_setting('logo_path');
|
||||||
|
$companyEmail = get_setting('company_email', '');
|
||||||
|
$companyPhone = get_setting('company_phone', '');
|
||||||
|
$companyAddress = get_setting('company_address', '');
|
||||||
|
?>
|
||||||
|
</main>
|
||||||
|
<?php if ($showFooter): ?>
|
||||||
|
<footer class="bg-white border-top py-5 mt-auto">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<?php if ($logoPath): ?>
|
||||||
|
<img src="<?= e($logoPath) ?>" alt="<?= e($appName) ?> Logo" height="28" class="me-2 rounded">
|
||||||
|
<?php else: ?>
|
||||||
|
<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>
|
||||||
|
<?php endif; ?>
|
||||||
|
<h5 class="fw-bold mb-0 text-dark"><?= e($appName) ?></h5>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small pe-md-4">
|
||||||
|
<?= e(t('motivation_phrase')) ?>
|
||||||
|
</p>
|
||||||
|
<?php if ($companyEmail || $companyPhone || $companyAddress): ?>
|
||||||
|
<div class="small text-muted mt-3">
|
||||||
|
<?php if ($companyAddress): ?>
|
||||||
|
<div class="mb-1"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-geo-alt me-1" viewBox="0 0 16 16"><path d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A32 32 0 0 1 8 14.58a32 32 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.94M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10"/><path d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4m0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/></svg><?= e($companyAddress) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($companyPhone): ?>
|
||||||
|
<div class="mb-1"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-telephone me-1" viewBox="0 0 16 16"><path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.6 17.6 0 0 0 4.168 6.608 17.6 17.6 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.68.68 0 0 0-.58-.122l-2.19.547a1.75 1.75 0 0 1-1.657-.459L5.482 8.062a1.75 1.75 0 0 1-.46-1.657l.548-2.19a.68.68 0 0 0-.122-.58z"/></svg><a href="tel:<?= e($companyPhone) ?>" class="text-muted text-decoration-none"><?= e($companyPhone) ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($companyEmail): ?>
|
||||||
|
<div class="mb-1"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-envelope me-1" viewBox="0 0 16 16"><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/></svg><a href="mailto:<?= e($companyEmail) ?>" class="text-muted text-decoration-none"><?= e($companyEmail) ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 offset-md-2">
|
||||||
|
<h6 class="fw-bold mb-3"><?= e(t('company')) ?></h6>
|
||||||
|
<ul class="list-unstyled text-muted small">
|
||||||
|
<li class="mb-2"><a href="#" class="text-decoration-none text-muted"><?= e(t('about_us')) ?></a></li>
|
||||||
|
<li class="mb-2"><a href="#" class="text-decoration-none text-muted"><?= e(t('careers')) ?></a></li>
|
||||||
|
<li class="mb-2"><a href="#" class="text-decoration-none text-muted"><?= e(t('contact')) ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<h6 class="fw-bold mb-3"><?= e(t('resources')) ?></h6>
|
||||||
|
<ul class="list-unstyled text-muted small">
|
||||||
|
<li class="mb-2"><a href="<?= e(url_with_lang('faq.php')) ?>" class="text-decoration-none text-muted"><?= e(t('help_center')) ?></a></li>
|
||||||
|
<li class="mb-2"><a href="<?= e(url_with_lang('terms.php')) ?>" class="text-decoration-none text-muted"><?= e(t('terms_of_service')) ?></a></li>
|
||||||
|
<li class="mb-2"><a href="<?= e(url_with_lang('privacy.php')) ?>" class="text-decoration-none text-muted"><?= e(t('privacy_policy')) ?></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<h6 class="fw-bold mb-3"><?= e(t('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($appName) ?>. <?= e(t('all_rights_reserved')) ?></span>
|
||||||
|
<span class="mt-2 mt-md-0"><?= e(t('footer_note')) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<?php endif; ?>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function setTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="/assets/js/main.js?v=<?= time() ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_admin_sidebar(string $active = 'dashboard'): void
|
||||||
|
{
|
||||||
|
$settingsActive = in_array($active, ['company_profile', 'integrations', 'notification_templates']);
|
||||||
|
$locationsActive = in_array($active, ['countries', 'cities']);
|
||||||
|
$usersActive = in_array($active, ['shippers', 'truck_owners', 'register', 'platform_users']);
|
||||||
|
$pagesActive = in_array($active, ['faqs', 'landing_pages']);
|
||||||
|
$reportsActive = in_array($active, ['reports_shippers', 'reports_truck_owners', 'reports_summary']);
|
||||||
|
?>
|
||||||
|
<aside class="admin-sidebar d-flex flex-column h-100 py-4 px-3">
|
||||||
|
<h2 class="h5 fw-bold mb-4 px-2"><i class="bi bi-shield-lock me-2 text-primary"></i><?= e(t('nav_admin')) ?></h2>
|
||||||
|
<nav class="nav flex-column gap-1 flex-grow-1">
|
||||||
|
<a class="admin-nav-link <?= $active === 'dashboard' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_dashboard.php')) ?>">
|
||||||
|
<i class="bi bi-speedometer2 me-2"></i><?= e(t('dashboard')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'shipments' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_shipments.php')) ?>">
|
||||||
|
<i class="bi bi-box2-fill me-2"></i><?= e(t('shipments')) ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="nav-link fw-bold text-muted text-uppercase mt-2 d-flex justify-content-between align-items-center p-2 rounded" style="cursor: pointer; font-size: 0.85rem;" data-bs-toggle="collapse" data-bs-target="#collapseLocations" aria-expanded="<?= $locationsActive ? 'true' : 'false' ?>">
|
||||||
|
<span><i class="bi bi-geo-alt-fill me-2"></i><?= e(t('locations')) ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?= $locationsActive ? 'show' : '' ?>" id="collapseLocations">
|
||||||
|
<div class="nav flex-column gap-1 ms-3 border-start ps-2 border-2">
|
||||||
|
<a class="admin-nav-link <?= $active === 'countries' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_countries.php')) ?>">
|
||||||
|
<i class="bi bi-globe-americas me-2"></i><?= e(t('countries')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'cities' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_cities.php')) ?>">
|
||||||
|
<i class="bi bi-pin-map me-2"></i><?= e(t('cities')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link fw-bold text-muted text-uppercase mt-2 d-flex justify-content-between align-items-center p-2 rounded" style="cursor: pointer; font-size: 0.85rem;" data-bs-toggle="collapse" data-bs-target="#collapseUsers" aria-expanded="<?= $usersActive ? 'true' : 'false' ?>">
|
||||||
|
<span><i class="bi bi-people-fill me-2"></i><?= e(t('users')) ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?= $usersActive ? 'show' : '' ?>" id="collapseUsers">
|
||||||
|
<div class="nav flex-column gap-1 ms-3 border-start ps-2 border-2">
|
||||||
|
<a class="admin-nav-link <?= $active === 'shippers' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_shippers.php')) ?>">
|
||||||
|
<i class="bi bi-box-seam me-2"></i><?= e(t('shippers')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'truck_owners' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_truck_owners.php')) ?>">
|
||||||
|
<i class="bi bi-truck me-2"></i><?= e(t('truck_owners')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'platform_users' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_platform_users.php')) ?>">
|
||||||
|
<i class="bi bi-person-badge me-2"></i><?= e(t('nav_platform_users')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'register' ? 'active' : '' ?>" href="<?= e(url_with_lang('register.php')) ?>">
|
||||||
|
<i class="bi bi-person-plus me-2"></i><?= e(t('user_registration')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link fw-bold text-muted text-uppercase mt-2 d-flex justify-content-between align-items-center p-2 rounded" style="cursor: pointer; font-size: 0.85rem;" data-bs-toggle="collapse" data-bs-target="#collapsePages" aria-expanded="<?= $pagesActive ? 'true' : 'false' ?>">
|
||||||
|
<span><i class="bi bi-file-earmark-text-fill me-2"></i><?= e(t('pages')) ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?= $pagesActive ? 'show' : '' ?>" id="collapsePages">
|
||||||
|
<div class="nav flex-column gap-1 ms-3 border-start ps-2 border-2">
|
||||||
|
<a class="admin-nav-link <?= $active === 'faqs' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_faqs.php')) ?>">
|
||||||
|
<i class="bi bi-question-square me-2"></i><?= e(t('faqs')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'landing_pages' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_landing_pages.php')) ?>">
|
||||||
|
<i class="bi bi-layout-text-window me-2"></i><?= e(t('landing_pages')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link fw-bold text-muted text-uppercase mt-2 d-flex justify-content-between align-items-center p-2 rounded" style="cursor: pointer; font-size: 0.85rem;" data-bs-toggle="collapse" data-bs-target="#collapseReports" aria-expanded="<?= $reportsActive ? 'true' : 'false' ?>">
|
||||||
|
<span><i class="bi bi-file-earmark-bar-graph-fill me-2"></i><?= e(t('reports')) ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?= $reportsActive ? 'show' : '' ?>" id="collapseReports">
|
||||||
|
<div class="nav flex-column gap-1 ms-3 border-start ps-2 border-2">
|
||||||
|
<a class="admin-nav-link <?= $active === 'reports_summary' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_reports_summary.php')) ?>">
|
||||||
|
<i class="bi bi-pie-chart-fill me-2"></i><?= e(t('analytics')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'reports_shippers' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_reports_shippers.php')) ?>">
|
||||||
|
<i class="bi bi-people me-2"></i><?= e(t('shipper_shipments')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'reports_truck_owners' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_reports_truck_owners.php')) ?>">
|
||||||
|
<i class="bi bi-truck me-2"></i><?= e(t('truck_owners_statements')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="nav-link fw-bold text-muted text-uppercase mt-2 d-flex justify-content-between align-items-center p-2 rounded" style="cursor: pointer; font-size: 0.85rem;" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="<?= $settingsActive ? 'true' : 'false' ?>">
|
||||||
|
<span><i class="bi bi-gear-fill me-2"></i><?= e(t('settings')) ?></span>
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</a>
|
||||||
|
<div class="collapse <?= $settingsActive ? 'show' : '' ?>" id="collapseSettings">
|
||||||
|
<div class="nav flex-column gap-1 ms-3 border-start ps-2 border-2">
|
||||||
|
<a class="admin-nav-link <?= $active === 'company_profile' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_company_profile.php')) ?>">
|
||||||
|
<i class="bi bi-building me-2"></i><?= e(t('company_setting')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'integrations' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_integrations.php')) ?>">
|
||||||
|
<i class="bi bi-plug me-2"></i><?= e(t('integrations')) ?>
|
||||||
|
</a>
|
||||||
|
<a class="admin-nav-link <?= $active === 'notification_templates' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_notification_templates.php')) ?>">
|
||||||
|
<i class="bi bi-bell-fill me-2"></i><?= e(t('notification_templates')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-top text-center">
|
||||||
|
<a href="<?= e(url_with_lang('logout.php')) ?>" class="btn btn-outline-danger w-100 btn-sm">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
434
index.php
@ -1,150 +1,294 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
@ini_set('display_errors', '1');
|
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'shipments' => 0,
|
||||||
|
'offers' => 0,
|
||||||
|
'confirmed' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stats['shipments'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments")->fetchColumn();
|
||||||
|
$stats['offers'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments WHERE offer_price IS NOT NULL")->fetchColumn();
|
||||||
|
$stats['confirmed'] = (int) $pdo->query("SELECT COUNT(*) FROM shipments WHERE status IN ('confirmed','in_transit','delivered')")->fetchColumn();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Keep the landing page stable even if DB is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
$recentShipments = [];
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 5");
|
||||||
|
$recentShipments = $stmt->fetchAll();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$recentShipments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('app_name'), 'home');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT * FROM landing_sections WHERE is_active = 1 ORDER BY section_order ASC, id ASC");
|
||||||
|
$landingSections = $stmt->fetchAll();
|
||||||
|
foreach ($landingSections as $sec):
|
||||||
|
// Determine correct language for dynamic content
|
||||||
|
$isAr = ($lang ?? 'en') === 'ar';
|
||||||
|
$title = ($isAr && !empty($sec['title_ar'])) ? $sec['title_ar'] : $sec['title'];
|
||||||
|
$subtitle = ($isAr && !empty($sec['subtitle_ar'])) ? $sec['subtitle_ar'] : $sec['subtitle'];
|
||||||
|
$content = ($isAr && !empty($sec['content_ar'])) ? $sec['content_ar'] : $sec['content'];
|
||||||
|
$button_text = ($isAr && !empty($sec['button_text_ar'])) ? $sec['button_text_ar'] : $sec['button_text'];
|
||||||
|
|
||||||
|
if ($sec['section_type'] === 'hero'):
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<div class="hero-section mb-5">
|
||||||
<html lang="en">
|
<div class="row g-0">
|
||||||
<head>
|
<div class="col-lg-6 d-flex flex-column justify-content-center">
|
||||||
<meta charset="utf-8" />
|
<div class="hero-content">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<span class="badge bg-primary bg-opacity-10 text-primary mb-3 px-3 py-2 fs-6 rounded-pill">
|
||||||
<title>New Style</title>
|
<?= e(t('hero_tagline')) ?>
|
||||||
<?php
|
</span>
|
||||||
// Read project preview data from environment
|
<h1 class="display-5 fw-bold mb-4" style="line-height: 1.2;">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<?= e($title ?: t('hero_title')) ?>
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
</h1>
|
||||||
?>
|
<p class="fs-5 text-muted mb-5">
|
||||||
<?php if ($projectDescription): ?>
|
<?= e($subtitle ?: t('hero_subtitle')) ?>
|
||||||
<!-- Meta description -->
|
</p>
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
<div class="d-flex flex-column flex-sm-row gap-3">
|
||||||
<!-- Open Graph meta tags -->
|
<a class="btn btn-primary btn-lg shadow-sm" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>">
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<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">
|
||||||
<!-- Twitter meta tags -->
|
<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"/>
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
</svg>
|
||||||
<?php endif; ?>
|
<?= e(t('register_shipper')) ?>
|
||||||
<?php if ($projectImageUrl): ?>
|
</a>
|
||||||
<!-- Open Graph image -->
|
<a class="btn btn-outline-primary btn-lg bg-white shadow-sm" href="<?= e(url_with_lang('register.php', ['role' => 'truck_owner'])) ?>">
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
<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">
|
||||||
<!-- Twitter image -->
|
<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"/>
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
</svg>
|
||||||
<?php endif; ?>
|
<?= e(t('register_owner')) ?>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
</a>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
</div>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
</div>
|
||||||
<style>
|
</div>
|
||||||
:root {
|
<div class="col-lg-6">
|
||||||
--bg-color-start: #6a11cb;
|
<div class="hero-img-container" style="background-image: url('<?= $sec['image_path'] ? e($sec['image_path']) : 'assets/images/hero_trucks.jpg' ?>');"></div>
|
||||||
--bg-color-end: #2575fc;
|
</div>
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% { background-position: 0% 0%; }
|
|
||||||
100% { background-position: 100% 100%; }
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
margin: 1.25rem auto 1.25rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
padding: 0; margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<div class="card">
|
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
|
||||||
<span class="sr-only">Loading…</span>
|
|
||||||
</div>
|
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
<footer>
|
<?php elseif ($sec['section_type'] === 'stats'): ?>
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<div class="row mb-5 g-4">
|
||||||
</footer>
|
<?php if ($title): ?>
|
||||||
</body>
|
<div class="col-12 text-center mb-3"><h2 class="display-6 fw-bold"><?= e($title) ?></h2></div>
|
||||||
</html>
|
<?php endif; ?>
|
||||||
|
<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>
|
||||||
|
<?php elseif ($sec['section_type'] === 'features'): ?>
|
||||||
|
<section class="mb-5">
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<h2 class="display-6 fw-bold mb-3"><?= e($title ?: t('why_choose_us')) ?></h2>
|
||||||
|
<p class="text-muted fs-5 mx-auto" style="max-width: 600px;"><?= e($subtitle ?: 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>
|
||||||
|
<?php elseif ($sec['section_type'] === 'workflow'): ?>
|
||||||
|
<section class="mb-5">
|
||||||
|
<div class="row g-4 align-items-center">
|
||||||
|
<div class="col-md-6 order-md-2">
|
||||||
|
<img src="<?= $sec['image_path'] ? e($sec['image_path']) : 'assets/images/workflow_trucks.jpg' ?>" 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($title ?: 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="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="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"><?= e(t('step_confirm_desc')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php elseif ($sec['section_type'] === 'recent_shipments'): ?>
|
||||||
|
<?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($title ?: t('recent_shipments')) ?></h2>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php elseif ($sec['section_type'] === 'faq'): ?>
|
||||||
|
<section class="mb-5 text-center">
|
||||||
|
<div class="panel p-5 border-0 shadow-sm rounded-4" style="background-color: #f8f9fa;">
|
||||||
|
<h2 class="display-6 fw-bold mb-3"><?= e($title ?: t('faq_title')) ?></h2>
|
||||||
|
<p class="text-muted fs-5 mb-4 mx-auto" style="max-width: 600px;"><?= e($subtitle ?: t('faq_subtitle')) ?></p>
|
||||||
|
<a href="<?= e(url_with_lang('faq.php')) ?>" class="btn btn-primary btn-lg px-4 rounded-pill shadow-sm">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-question-circle me-2" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/><path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/></svg><?= e(t('view_faq')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php elseif ($sec['section_type'] === 'motivation'): ?>
|
||||||
|
<div class="motivation-box mb-5" <?php if ($sec['image_path']) { echo 'style="background-image: url(' . e($sec['image_path']) . '); background-size: cover; background-position: center;"'; } ?>>
|
||||||
|
<h3 class="display-6 fw-bold mb-3"><?= e($title ?: t('motivation_title')) ?></h3>
|
||||||
|
<p class="fs-5 text-white-50 mb-4 mx-auto" style="max-width: 600px;"><?= e($subtitle ?: t('motivation_subtitle')) ?></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 elseif ($sec['section_type'] === 'custom'): ?>
|
||||||
|
<section class="mb-5">
|
||||||
|
<?php if ($sec['layout'] === 'center'): ?>
|
||||||
|
<div class="panel p-5 border-0 shadow-sm rounded-4 text-center" style="background-color: #f8f9fa;">
|
||||||
|
<?php if ($sec['image_path']): ?>
|
||||||
|
<img src="<?= e($sec['image_path']) ?>" alt="" class="img-fluid mb-4 rounded-4 shadow-sm" style="max-height: 300px; object-fit: cover;">
|
||||||
|
<?php endif; ?>
|
||||||
|
<h2 class="display-6 fw-bold mb-3"><?= e($title) ?></h2>
|
||||||
|
<?php if ($subtitle): ?>
|
||||||
|
<p class="text-muted fs-5 mb-4 mx-auto" style="max-width: 600px;"><?= e($subtitle) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($content): ?>
|
||||||
|
<div class="mb-4 text-start mx-auto" style="max-width: 800px;"><?= $content ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($button_text): ?>
|
||||||
|
<a href="<?= e(url_with_lang($sec['button_link'] ?: '#')) ?>" class="btn btn-primary btn-lg px-4 rounded-pill shadow-sm"><?= e($button_text) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-4 align-items-center">
|
||||||
|
<div class="col-md-6 <?= $sec['layout'] === 'text_right' ? 'order-md-1' : 'order-md-2' ?>">
|
||||||
|
<?php if ($sec['image_path']): ?>
|
||||||
|
<img src="<?= e($sec['image_path']) ?>" alt="" class="img-fluid rounded-4 shadow-sm" style="width: 100%; object-fit: cover; max-height: 400px;">
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 <?= $sec['layout'] === 'text_right' ? 'order-md-2' : 'order-md-1' ?>">
|
||||||
|
<div class="p-lg-4">
|
||||||
|
<h2 class="display-6 fw-bold mb-4"><?= e($title) ?></h2>
|
||||||
|
<?php if ($subtitle): ?>
|
||||||
|
<h5 class="fw-bold mb-3 text-muted"><?= e($subtitle) ?></h5>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($content): ?>
|
||||||
|
<div class="mb-4"><?= $content ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($button_text): ?>
|
||||||
|
<a href="<?= e(url_with_lang($sec['button_link'] ?: '#')) ?>" class="btn btn-primary btn-lg px-4 rounded-pill shadow-sm"><?= e($button_text) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
endforeach;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("Index Error: " . $e->getMessage() . " at " . $e->getFile() . ":" . $e->getLine());
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php render_footer(); ?>
|
||||||
264
install.php
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
<?php
|
||||||
|
// install.php - Simple Installer for Flatlogic LAMP Project
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Ensure errors are displayed for debugging during installation
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
|
||||||
|
$message = '';
|
||||||
|
$messageType = '';
|
||||||
|
|
||||||
|
function write_db_config($host, $name, $user, $pass) {
|
||||||
|
$content = "<?php\n";
|
||||||
|
$content .= "// Generated by install.php\n";
|
||||||
|
$content .= "if (!defined('DB_HOST')) define('DB_HOST', '" . addslashes($host) . "');\n";
|
||||||
|
$content .= "if (!defined('DB_NAME')) define('DB_NAME', '" . addslashes($name) . "');\n";
|
||||||
|
$content .= "if (!defined('DB_USER')) define('DB_USER', '" . addslashes($user) . "');\n";
|
||||||
|
$content .= "if (!defined('DB_PASS')) define('DB_PASS', '" . addslashes($pass) . "');\n\n";
|
||||||
|
|
||||||
|
$content .= "if (!function_exists('db')) {\n";
|
||||||
|
$content .= " function db() {\n";
|
||||||
|
$content .= " static $pdo;
|
||||||
|
";
|
||||||
|
$content .= " if (! $pdo) {\n";
|
||||||
|
$content .= " $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
|
";
|
||||||
|
$content .= " PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
";
|
||||||
|
$content .= " PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
";
|
||||||
|
$content .= " ]);
|
||||||
|
";
|
||||||
|
$content .= " }
|
||||||
|
";
|
||||||
|
$content .= " return $pdo;
|
||||||
|
";
|
||||||
|
$content .= " }
|
||||||
|
";
|
||||||
|
$content .= "}
|
||||||
|
";
|
||||||
|
|
||||||
|
return file_put_contents(__DIR__ . '/db/config.php', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Form Submissions
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// Determine step from hidden field or GET, defaulting to 1
|
||||||
|
$postStep = isset($_POST['step']) ? (int)$_POST['step'] : $step;
|
||||||
|
|
||||||
|
if ($postStep == 1) {
|
||||||
|
$host = $_POST['db_host'] ?? '';
|
||||||
|
$name = $_POST['db_name'] ?? '';
|
||||||
|
$user = $_POST['db_user'] ?? '';
|
||||||
|
$pass = $_POST['db_pass'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test Connection
|
||||||
|
$dsn = "mysql:host=$host;dbname=$name;charset=utf8mb4";
|
||||||
|
$testPdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
// If successful, write config
|
||||||
|
if (write_db_config($host, $name, $user, $pass)) {
|
||||||
|
$message = "Database connection established and saved!";
|
||||||
|
$messageType = "success";
|
||||||
|
// Redirect to avoid resubmission
|
||||||
|
header("Location: install.php?step=2");
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$message = "Could not write to db/config.php. Check permissions.";
|
||||||
|
$messageType = "danger";
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$message = "Connection failed: " . $e->getMessage();
|
||||||
|
$messageType = "danger";
|
||||||
|
}
|
||||||
|
} elseif ($postStep == 2) {
|
||||||
|
// Step 2: Create Admin & Run Migrations
|
||||||
|
|
||||||
|
// Include config and migration script
|
||||||
|
if (file_exists(__DIR__ . '/db/config.php')) {
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
} else {
|
||||||
|
$message = "Configuration file missing. Please go back to Step 1.";
|
||||||
|
$messageType = "danger";
|
||||||
|
$step = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/db/migrate.php')) {
|
||||||
|
require_once __DIR__ . '/db/migrate.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $_POST['admin_email'] ?? '';
|
||||||
|
$password = $_POST['admin_pass'] ?? '';
|
||||||
|
$fullName = $_POST['admin_name'] ?? 'Administrator';
|
||||||
|
|
||||||
|
if ($email && $password && defined('DB_HOST')) {
|
||||||
|
try {
|
||||||
|
// 1. Run Migrations
|
||||||
|
$migrationResults = [];
|
||||||
|
if (function_exists('run_migrations')) {
|
||||||
|
$migrationResults = run_migrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Also ensure basic schema from app.php (just in case)
|
||||||
|
// We do this after migrations so migrations take precedence if they exist
|
||||||
|
if (file_exists(__DIR__ . '/includes/app.php')) {
|
||||||
|
// We catch output to prevent it from messing up headers/layout if app.php has echoes
|
||||||
|
ob_start();
|
||||||
|
require_once __DIR__ . '/includes/app.php';
|
||||||
|
if (function_exists('ensure_schema')) {
|
||||||
|
ensure_schema();
|
||||||
|
}
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create Admin User
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Check if admin exists
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
// Update existing
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET password = ?, full_name = ?, role = 'admin', status = 'active' WHERE email = ?");
|
||||||
|
$stmt->execute([password_hash($password, PASSWORD_DEFAULT), $fullName, $email]);
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO users (email, password, full_name, role, status) VALUES (?, ?, ?, 'admin', 'active')");
|
||||||
|
$stmt->execute([$email, password_hash($password, PASSWORD_DEFAULT), $fullName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$migMsg = implode("<br>", $migrationResults);
|
||||||
|
$message = "Admin account created successfully!<br><small>$migMsg</small>";
|
||||||
|
$messageType = "success";
|
||||||
|
$step = 3; // Success page
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$message = "Error: " . $e->getMessage();
|
||||||
|
$messageType = "danger";
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$message = "Fatal Error: " . $e->getMessage();
|
||||||
|
$messageType = "danger";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message = "Please fill in all fields.";
|
||||||
|
$messageType = "danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current config values for Step 1
|
||||||
|
$current_db_host = '127.0.0.1';
|
||||||
|
$current_db_name = 'app';
|
||||||
|
$current_db_user = 'root';
|
||||||
|
$current_db_pass = '';
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/db/config.php')) {
|
||||||
|
include __DIR__ . '/db/config.php';
|
||||||
|
if (defined('DB_HOST')) $current_db_host = DB_HOST;
|
||||||
|
if (defined('DB_NAME')) $current_db_name = DB_NAME;
|
||||||
|
if (defined('DB_USER')) $current_db_user = DB_USER;
|
||||||
|
if (defined('DB_PASS')) $current_db_pass = DB_PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Installation - Step <?php echo $step; ?></title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { background-color: #f8f9fa; }
|
||||||
|
.install-container { max_width: 600px; margin: 50px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.step-indicator { margin-bottom: 20px; font-weight: bold; color: #6c757d; }
|
||||||
|
.step-active { color: #0d6efd; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="install-container">
|
||||||
|
<h2 class="text-center mb-4">Application Installer</h2>
|
||||||
|
|
||||||
|
<div class="step-indicator text-center">
|
||||||
|
<span class="<?php echo $step == 1 ? 'step-active' : ''; ?>">1. Database</span> →
|
||||||
|
<span class="<?php echo $step == 2 ? 'step-active' : ''; ?>">2. Admin User</span> →
|
||||||
|
<span class="<?php echo $step == 3 ? 'step-active' : ''; ?>">3. Finish</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($message): ?>
|
||||||
|
<div class="alert alert-<?php echo $messageType; ?>"><?php echo $message; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($step == 1): ?>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="step" value="1">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Database Host</label>
|
||||||
|
<input type="text" name="db_host" class="form-control" value="<?php echo htmlspecialchars($current_db_host); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Database Name</label>
|
||||||
|
<input type="text" name="db_name" class="form-control" value="<?php echo htmlspecialchars($current_db_name); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Database User</label>
|
||||||
|
<input type="text" name="db_user" class="form-control" value="<?php echo htmlspecialchars($current_db_user); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Database Password</label>
|
||||||
|
<input type="password" name="db_pass" class="form-control" value="<?php echo htmlspecialchars($current_db_pass); ?>">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Check Connection & Continue</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Auto-check if GET request and already configured
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && file_exists(__DIR__ . '/db/config.php')) {
|
||||||
|
try {
|
||||||
|
$dsn = "mysql:host=$current_db_host;dbname=$current_db_name;charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $current_db_user, $current_db_pass);
|
||||||
|
echo '<div class="alert alert-success mt-3">Current configuration is valid! <a href="?step=2" class="fw-bold">Skip to Step 2</a></div>';
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Silent fail, user sees form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php elseif ($step == 2): ?>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="step" value="2">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Admin Full Name</label>
|
||||||
|
<input type="text" name="admin_name" class="form-control" placeholder="Admin User" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Admin Email</label>
|
||||||
|
<input type="email" name="admin_email" class="form-control" placeholder="admin@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" name="admin_pass" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Create Admin Account & Run Migrations</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php elseif ($step == 3): ?>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-success display-1 mb-3">✓</div>
|
||||||
|
<h3>Installation Complete!</h3>
|
||||||
|
<p>You can now login to your admin dashboard.</p>
|
||||||
|
<a href="login.php" class="btn btn-success">Go to Login</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
172
login.php
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$successMessage = '';
|
||||||
|
|
||||||
|
// If logged in, redirect home
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
if (isset($_POST['action']) && $_POST['action'] === 'login') {
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$password = (string)($_POST['password'] ?? '');
|
||||||
|
|
||||||
|
if ($email === '' || $password === '') {
|
||||||
|
$errors[] = 'Please enter both email and password.';
|
||||||
|
} else {
|
||||||
|
$stmt = db()->prepare("SELECT id, password, role, status FROM users WHERE email = ? LIMIT 1");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password'])) {
|
||||||
|
if ($user['status'] === 'pending') {
|
||||||
|
$errors[] = 'Your account is pending approval.';
|
||||||
|
} elseif ($user['status'] === 'rejected') {
|
||||||
|
$errors[] = 'Your account has been rejected.';
|
||||||
|
} elseif ($user['status'] === 'suspended') {
|
||||||
|
$errors[] = 'Your account has been suspended.';
|
||||||
|
} else {
|
||||||
|
// Login successful
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['user_role'] = $user['role'];
|
||||||
|
|
||||||
|
// Redirect based on role
|
||||||
|
if ($user['role'] === 'admin') {
|
||||||
|
header('Location: ' . url_with_lang('admin_dashboard.php'));
|
||||||
|
} elseif ($user['role'] === 'shipper') {
|
||||||
|
header('Location: ' . url_with_lang('shipper_dashboard.php'));
|
||||||
|
} else {
|
||||||
|
header('Location: ' . url_with_lang('truck_owner_dashboard.php'));
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Invalid email or password.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['action']) && $_POST['action'] === 'reset_password') {
|
||||||
|
$email = trim($_POST['reset_email'] ?? '');
|
||||||
|
if ($email === '') {
|
||||||
|
$errors[] = 'Please enter your email to reset password.';
|
||||||
|
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors[] = 'Please enter a valid email address.';
|
||||||
|
} else {
|
||||||
|
$stmt = db()->prepare("SELECT id FROM users WHERE email = ? LIMIT 1");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
// In a real app we'd send an email with a reset token here.
|
||||||
|
// Since this is a demo, we will just show a success message.
|
||||||
|
$successMessage = 'A password reset link has been sent to your email address (simulated).';
|
||||||
|
} else {
|
||||||
|
// To prevent email enumeration, still say a link was sent.
|
||||||
|
$successMessage = 'A password reset link has been sent to your email address (simulated).';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Login / Reset Password', 'login', false, false);
|
||||||
|
?>
|
||||||
|
<div class="row justify-content-center align-items-center" style="min-height: 75vh;">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
|
||||||
|
<div class="text-center mb-5">
|
||||||
|
<?php
|
||||||
|
$appName = get_setting('company_name', t('app_name'));
|
||||||
|
$logoPath = get_setting('logo_path');
|
||||||
|
?>
|
||||||
|
<?php if ($logoPath): ?>
|
||||||
|
<img src="<?= e($logoPath) ?>" alt="<?= e($appName) ?> Logo" class="img-fluid mb-3" style="max-height: 80px;">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="d-inline-flex align-items-center justify-content-center bg-primary text-white rounded-circle shadow-lg mb-3" style="width: 80px; height: 80px;">
|
||||||
|
<i class="bi bi-truck fs-1"></i>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<h1 class="fw-bold text-dark display-6"><?= e($appName) ?></h1>
|
||||||
|
<p class="text-muted lead">Your logistics partner</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger shadow-sm rounded-4 border-0 mb-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<?php foreach ($errors as $e): ?>
|
||||||
|
<li><?= e($e) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($successMessage): ?>
|
||||||
|
<div class="alert alert-success shadow-sm rounded-4 border-0 mb-4">
|
||||||
|
<?= e($successMessage) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 rounded-4 mb-4" id="loginFormCard">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="fw-bold text-dark"><?= e(t('login_title')) ?></h2>
|
||||||
|
<p class="text-muted"><?= e(t('login_subtitle')) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action=""> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="login">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label fw-bold"><?= e(t('email_address')) ?></label>
|
||||||
|
<input type="email" class="form-control form-control-lg bg-light border-0 rounded-3" id="email" name="email" required autofocus placeholder="<?= e(t('email_placeholder')) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<label for="password" class="form-label fw-bold mb-0"><?= e(t('password')) ?></label>
|
||||||
|
<a href="#" class="small text-decoration-none text-primary" onclick="document.getElementById('loginFormCard').classList.add('d-none'); document.getElementById('resetFormCard').classList.remove('d-none'); return false;"><?= e(t('forgot_password')) ?></a>
|
||||||
|
</div>
|
||||||
|
<input type="password" class="form-control form-control-lg bg-light border-0 rounded-3" id="password" name="password" required placeholder="<?= e(t('password_placeholder')) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg w-100 rounded-pill fw-bold shadow-sm"><?= e(t('sign_in')) ?></button>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="text-muted small mb-0"><?= e(t('dont_have_account')) ?> <a href="<?= e(url_with_lang('register.php')) ?>" class="text-decoration-none text-primary fw-bold"><?= e(t('register_now')) ?></a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm border-0 rounded-4 mb-4 d-none" id="resetFormCard">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="fw-bold text-dark"><?= e(t('reset_password_title')) ?></h2>
|
||||||
|
<p class="text-muted"><?= e(t('reset_password_subtitle')) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action=""> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="reset_password">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="reset_email" class="form-label fw-bold"><?= e(t('email_address')) ?></label>
|
||||||
|
<input type="email" class="form-control form-control-lg bg-light border-0 rounded-3" id="reset_email" name="reset_email" required placeholder="<?= e(t('email_placeholder')) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg w-100 rounded-pill fw-bold shadow-sm mb-3"><?= e(t('send_reset_link')) ?></button>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="#" class="text-decoration-none text-muted small fw-bold" onclick="document.getElementById('resetFormCard').classList.add('d-none'); document.getElementById('loginFormCard').classList.remove('d-none'); return false;"><i class="bi bi-arrow-left me-1"></i><?= e(t('back_to_login')) ?></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(false); ?>
|
||||||
23
logout.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/app.php';
|
||||||
|
|
||||||
|
// Unset all session variables
|
||||||
|
$_SESSION = [];
|
||||||
|
|
||||||
|
// Destroy the session cookie
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000,
|
||||||
|
$params["path"], $params["domain"],
|
||||||
|
$params["secure"], $params["httponly"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the session
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
@ -7,6 +7,11 @@
|
|||||||
// // Contact form helper:
|
// // Contact form helper:
|
||||||
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
||||||
|
|
||||||
|
// Ensure DB config is loaded for settings
|
||||||
|
if (!function_exists('db') && file_exists(__DIR__ . '/../db/config.php')) {
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
}
|
||||||
|
|
||||||
class MailService
|
class MailService
|
||||||
{
|
{
|
||||||
// Universal mail sender (no attachments by design)
|
// Universal mail sender (no attachments by design)
|
||||||
@ -18,26 +23,26 @@ class MailService
|
|||||||
if (file_exists($autoload)) {
|
if (file_exists($autoload)) {
|
||||||
require_once $autoload;
|
require_once $autoload;
|
||||||
}
|
}
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'libphp-phpmailer/autoload.php';
|
@require_once 'libphp-phpmailer/autoload.php';
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||||
}
|
}
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'PHPMailer/src/Exception.php';
|
@require_once 'PHPMailer/src/Exception.php';
|
||||||
@require_once 'PHPMailer/src/SMTP.php';
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||||
}
|
}
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'PHPMailer/Exception.php';
|
@require_once 'PHPMailer/Exception.php';
|
||||||
@require_once 'PHPMailer/SMTP.php';
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
@require_once 'PHPMailer/PHPMailer.php';
|
@require_once 'PHPMailer/PHPMailer.php';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,14 +96,18 @@ class MailService
|
|||||||
|
|
||||||
$mail->isHTML(true);
|
$mail->isHTML(true);
|
||||||
$mail->Subject = $subject;
|
$mail->Subject = $subject;
|
||||||
$mail->Body = $htmlBody;
|
|
||||||
|
// Apply standardized HTML wrapper
|
||||||
|
$mail->Body = self::wrapHtml($htmlBody, $subject);
|
||||||
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||||
|
|
||||||
$ok = $mail->send();
|
$ok = $mail->send();
|
||||||
return [ 'success' => $ok ];
|
return [ 'success' => $ok ];
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function loadConfig(): array
|
private static function loadConfig(): array
|
||||||
{
|
{
|
||||||
$configPath = __DIR__ . '/config.php';
|
$configPath = __DIR__ . '/config.php';
|
||||||
@ -124,21 +133,21 @@ class MailService
|
|||||||
require_once $autoload;
|
require_once $autoload;
|
||||||
}
|
}
|
||||||
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
// Debian/Ubuntu package layout (libphp-phpmailer)
|
// Debian/Ubuntu package layout (libphp-phpmailer)
|
||||||
@require_once 'libphp-phpmailer/autoload.php';
|
@require_once 'libphp-phpmailer/autoload.php';
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'libphp-phpmailer/src/Exception.php';
|
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||||
}
|
}
|
||||||
// Alternative layout (older PHPMailer package names)
|
// Alternative layout (older PHPMailer package names)
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'PHPMailer/src/Exception.php';
|
@require_once 'PHPMailer/src/Exception.php';
|
||||||
@require_once 'PHPMailer/src/SMTP.php';
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
@require_once 'PHPMailer/src/PHPMailer.php';
|
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||||
}
|
}
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'PHPMailer/Exception.php';
|
@require_once 'PHPMailer/Exception.php';
|
||||||
@require_once 'PHPMailer/SMTP.php';
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
@require_once 'PHPMailer/PHPMailer.php';
|
@require_once 'PHPMailer/PHPMailer.php';
|
||||||
@ -146,7 +155,7 @@ class MailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$transport = $cfg['transport'] ?? 'smtp';
|
$transport = $cfg['transport'] ?? 'smtp';
|
||||||
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if ($transport === 'smtp' && class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
|
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,12 +225,16 @@ class MailService
|
|||||||
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||||
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
|
||||||
|
$innerHtml = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||||
|
|
||||||
|
// Apply standardized HTML wrapper
|
||||||
|
$mail->Body = self::wrapHtml($innerHtml, $subject);
|
||||||
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
|
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
|
||||||
|
|
||||||
$ok = $mail->send();
|
$ok = $mail->send();
|
||||||
return [ 'success' => $ok ];
|
return [ 'success' => $ok ];
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,7 +242,108 @@ class MailService
|
|||||||
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||||
{
|
{
|
||||||
$opts = ['reply_to' => $email];
|
$opts = ['reply_to' => $email];
|
||||||
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
return self::sendMail($to, $subject, $html, $body, $opts);
|
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||||
|
|
||||||
|
$innerHtml = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||||
|
|
||||||
|
// sendMail will wrap the HTML now
|
||||||
|
return self::sendMail($to, $subject, $innerHtml, $body, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getSetting(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
// Use global get_setting if available (requires includes/app.php)
|
||||||
|
if (function_exists('get_setting')) {
|
||||||
|
return get_setting($key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Direct DB query if db() helper is available
|
||||||
|
if (function_exists('db')) {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("SELECT setting_value FROM settings WHERE setting_key = ? LIMIT 1");
|
||||||
|
$stmt->execute([$key]);
|
||||||
|
$val = $stmt->fetchColumn();
|
||||||
|
return $val !== false ? (string)$val : $default;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function wrapHtml(string $content, string $subject): string
|
||||||
|
{
|
||||||
|
$companyName = self::getSetting('company_name', 'CargoLink');
|
||||||
|
$logoPath = self::getSetting('logo_path', '');
|
||||||
|
$companyAddress = self::getSetting('company_address', '');
|
||||||
|
|
||||||
|
// Build absolute URL for logo
|
||||||
|
$logoUrl = '';
|
||||||
|
if ($logoPath) {
|
||||||
|
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
// Assuming logo_path is relative to public root (e.g., uploads/logos/...)
|
||||||
|
$logoUrl = "{$scheme}://{$host}/" . ltrim($logoPath, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = date('Y');
|
||||||
|
$footerInfo = [];
|
||||||
|
if ($companyAddress) {
|
||||||
|
$footerInfo[] = nl2br(htmlspecialchars($companyAddress));
|
||||||
|
}
|
||||||
|
$footerHtml = implode('<br>', $footerInfo);
|
||||||
|
|
||||||
|
$logoHtml = '';
|
||||||
|
if ($logoUrl) {
|
||||||
|
$logoHtml = "<div style=\"text-align: center; margin-bottom: 20px;\"><img src=\"{$logoUrl}\" alt=\"{$companyName}\" style=\"max-height: 60px; max-width: 200px;\"></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{$subject}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f7f6; margin: 0; padding: 0; color: #333; line-height: 1.6; }
|
||||||
|
.wrapper { width: 100%; table-layout: fixed; background-color: #f4f7f6; padding-bottom: 40px; }
|
||||||
|
.webkit { max-width: 600px; background-color: #ffffff; margin: 0 auto; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
|
||||||
|
.outer { margin: 0 auto; width: 100%; max-width: 600px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
|
||||||
|
.header { background-color: #ffffff; padding: 30px 30px 20px; text-align: center; border-bottom: 1px solid #eeeeee; }
|
||||||
|
.content { padding: 30px; text-align: left; font-size: 16px; color: #555555; }
|
||||||
|
.footer { padding: 20px; text-align: center; font-size: 12px; color: #999999; background-color: #f9f9f9; border-top: 1px solid #eeeeee; }
|
||||||
|
h1, h2, h3 { color: #2c3e50; margin-top: 0; }
|
||||||
|
a { color: #3498db; text-decoration: none; }
|
||||||
|
p { margin-bottom: 15px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="webkit">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
{$logoHtml}
|
||||||
|
<h2 style="margin: 0; font-size: 24px; color: #333;">{$companyName}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="content">
|
||||||
|
{$content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {$year} {$companyName}. All rights reserved.</p>
|
||||||
|
{$footerHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
// Mail configuration sourced from environment variables.
|
// Mail configuration sourced from environment variables,
|
||||||
// No secrets are stored here; the file just maps env -> config array for MailService.
|
// falling back to database settings if configured.
|
||||||
|
|
||||||
function env_val(string $key, $default = null) {
|
function env_val(string $key, $default = null) {
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
|
||||||
// This helps in web/Apache contexts where .env is not exported.
|
|
||||||
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
|
||||||
function load_dotenv_if_needed(array $keys): void {
|
function load_dotenv_if_needed(array $keys): void {
|
||||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||||
if (empty($missing)) return;
|
if (empty($missing)) return;
|
||||||
@ -22,9 +19,7 @@ function load_dotenv_if_needed(array $keys): void {
|
|||||||
if ($line[0] === '#' || trim($line) === '') continue;
|
if ($line[0] === '#' || trim($line) === '') continue;
|
||||||
if (!str_contains($line, '=')) continue;
|
if (!str_contains($line, '=')) continue;
|
||||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||||
// Strip potential surrounding quotes
|
$v = trim($v, "'\" ");
|
||||||
$v = trim($v, "\"' ");
|
|
||||||
// Do not override existing env
|
|
||||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||||
putenv("{$k}={$v}");
|
putenv("{$k}={$v}");
|
||||||
}
|
}
|
||||||
@ -39,15 +34,43 @@ load_dotenv_if_needed([
|
|||||||
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
// Attempt to load settings from DB safely
|
||||||
$smtp_host = env_val('SMTP_HOST');
|
function get_mail_db_settings(): array {
|
||||||
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
$settings = [];
|
||||||
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
$dbPath = __DIR__ . '/../db/config.php';
|
||||||
$smtp_user = env_val('SMTP_USER');
|
if (file_exists($dbPath)) {
|
||||||
$smtp_pass = env_val('SMTP_PASS');
|
require_once $dbPath;
|
||||||
|
if (function_exists('db')) {
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT setting_key, setting_value FROM settings");
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$settings[$row['setting_key']] = $row['setting_value'];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Ignore DB errors if not initialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
$dbSettings = get_mail_db_settings();
|
||||||
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
|
||||||
|
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
||||||
|
|
||||||
|
$smtp_host = !empty($dbSettings['smtp_host']) ? $dbSettings['smtp_host'] : env_val('SMTP_HOST');
|
||||||
|
$smtp_port = !empty($dbSettings['smtp_port']) ? (int)$dbSettings['smtp_port'] : (int) env_val('SMTP_PORT', 587);
|
||||||
|
$smtp_secure = isset($dbSettings['smtp_secure']) && $dbSettings['smtp_secure'] !== '' ? $dbSettings['smtp_secure'] : env_val('SMTP_SECURE', 'tls');
|
||||||
|
if (isset($dbSettings['smtp_secure']) && $dbSettings['smtp_secure'] === 'none') {
|
||||||
|
$smtp_secure = '';
|
||||||
|
}
|
||||||
|
if ($smtp_secure === 'none') $smtp_secure = ''; // Normalize None to empty for PHPMailer
|
||||||
|
|
||||||
|
$smtp_user = !empty($dbSettings['smtp_user']) ? $dbSettings['smtp_user'] : env_val('SMTP_USER');
|
||||||
|
$smtp_pass = !empty($dbSettings['smtp_pass']) ? $dbSettings['smtp_pass'] : env_val('SMTP_PASS');
|
||||||
|
|
||||||
|
$from_email = !empty($dbSettings['mail_from']) ? $dbSettings['mail_from'] : env_val('MAIL_FROM', 'no-reply@localhost');
|
||||||
|
$from_name = !empty($dbSettings['mail_from_name']) ? $dbSettings['mail_from_name'] : env_val('MAIL_FROM_NAME', 'App');
|
||||||
$reply_to = env_val('MAIL_REPLY_TO');
|
$reply_to = env_val('MAIL_REPLY_TO');
|
||||||
|
|
||||||
$dkim_domain = env_val('DKIM_DOMAIN');
|
$dkim_domain = env_val('DKIM_DOMAIN');
|
||||||
@ -56,20 +79,14 @@ $dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'transport' => $transport,
|
'transport' => $transport,
|
||||||
|
|
||||||
// SMTP
|
|
||||||
'smtp_host' => $smtp_host,
|
'smtp_host' => $smtp_host,
|
||||||
'smtp_port' => $smtp_port,
|
'smtp_port' => $smtp_port,
|
||||||
'smtp_secure' => $smtp_secure,
|
'smtp_secure' => $smtp_secure,
|
||||||
'smtp_user' => $smtp_user,
|
'smtp_user' => $smtp_user,
|
||||||
'smtp_pass' => $smtp_pass,
|
'smtp_pass' => $smtp_pass,
|
||||||
|
|
||||||
// From / Reply-To
|
|
||||||
'from_email' => $from_email,
|
'from_email' => $from_email,
|
||||||
'from_name' => $from_name,
|
'from_name' => $from_name,
|
||||||
'reply_to' => $reply_to,
|
'reply_to' => $reply_to,
|
||||||
|
|
||||||
// DKIM (optional)
|
|
||||||
'dkim_domain' => $dkim_domain,
|
'dkim_domain' => $dkim_domain,
|
||||||
'dkim_selector' => $dkim_selector,
|
'dkim_selector' => $dkim_selector,
|
||||||
'dkim_private_key_path' => $dkim_private_key_path,
|
'dkim_private_key_path' => $dkim_private_key_path,
|
||||||
|
|||||||
64
patch_company_profile.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
$content = file_get_contents('admin_company_profile.php');
|
||||||
|
|
||||||
|
$php_updates = <<<PHP
|
||||||
|
\$updates = [
|
||||||
|
'company_name' => \$companyName,
|
||||||
|
'company_email' => \$companyEmail,
|
||||||
|
'company_phone' => \$companyPhone,
|
||||||
|
'company_address' => \$companyAddress,
|
||||||
|
'platform_charge_percentage' => \$platformCharge,
|
||||||
|
'terms_en' => trim(\\\$_POST['terms_en'] ?? ''),
|
||||||
|
'terms_ar' => trim(\\\$_POST['terms_ar'] ?? ''),
|
||||||
|
'privacy_en' => trim(\\\$_POST['privacy_en'] ?? ''),
|
||||||
|
'privacy_ar' => trim(\\\$_POST['privacy_ar'] ?? ''),
|
||||||
|
];
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
$content = preg_replace("/\\\\\$updates = \\\\[\\\\\s\\\\\\S]*?'platform_charge_percentage' => \\\\$platformCharge,\\n \\\\];/', $php_updates, $content);
|
||||||
|
|
||||||
|
$fetch_vars = <<<PHP
|
||||||
|
\$currentLogo = \$settings['logo_path'] ?? '';
|
||||||
|
\$currentFavicon = \$settings['favicon_path'] ?? '';
|
||||||
|
\$currentTermsEn = \$settings['terms_en'] ?? '';
|
||||||
|
\$currentTermsAr = \$settings['terms_ar'] ?? '';
|
||||||
|
\$currentPrivacyEn = \$settings['privacy_en'] ?? '';
|
||||||
|
\$currentPrivacyAr = \$settings['privacy_ar'] ?? '';
|
||||||
|
PHP;
|
||||||
|
|
||||||
|
$content = str_replace("\\\$currentFavicon = \\\$settings['favicon_path'] ?? '';", $fetch_vars, $content);
|
||||||
|
|
||||||
|
$html_fields = <<<HTML
|
||||||
|
<div class="col-md-12">
|
||||||
|
<hr class="my-2">
|
||||||
|
<h5 class="fw-bold mb-3">Legal & Policies</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Terms of Service (English)</label>
|
||||||
|
<textarea name="terms_en" class="form-control" rows="5"><?= htmlspecialchars(\\$currentTermsEn, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Terms of Service (Arabic)</label>
|
||||||
|
<textarea name="terms_ar" class="form-control" rows="5" dir="rtl"><?= htmlspecialchars(\\$currentTermsAr, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Privacy Policy (English)</label>
|
||||||
|
<textarea name="privacy_en" class="form-control" rows="5"><?= htmlspecialchars(\\$currentPrivacyEn, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold">Privacy Policy (Arabic)</label>
|
||||||
|
<textarea name="privacy_ar" class="form-control" rows="5" dir="rtl"><?= htmlspecialchars(\\$currentPrivacyAr, ENT_QUOTES, 'UTF-8') ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
$content = str_replace(" </div>\n \n <hr class=\"my-4\">
|
||||||
|
", $html_fields . "\n \n <hr class=\"my-4\">
|
||||||
|
", $content);
|
||||||
|
|
||||||
|
file_put_contents('admin_company_profile.php', $content);
|
||||||
|
|
||||||
23
patch_lines.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
$lines = file('includes/app.php');
|
||||||
|
$start = -1;
|
||||||
|
$end = -1;
|
||||||
|
foreach ($lines as $i => $line) {
|
||||||
|
if (strpos($line, '$translations = [') === 0) {
|
||||||
|
$start = $i;
|
||||||
|
}
|
||||||
|
if (strpos($line, 'function t(') === 0) {
|
||||||
|
$end = $i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($start !== -1 && $end !== -1) {
|
||||||
|
$new_content = array_slice($lines, 0, $start);
|
||||||
|
$new_content[] = file_get_contents('new_trans.txt');
|
||||||
|
$new_content = array_merge($new_content, array_slice($lines, $end));
|
||||||
|
file_put_contents('includes/app.php', implode("", $new_content));
|
||||||
|
echo "Replaced lines $start to $end\n";
|
||||||
|
} else {
|
||||||
|
echo "Could not find start/end\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
36
privacy.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
|
|
||||||
|
$settings = get_settings();
|
||||||
|
global $lang;
|
||||||
|
|
||||||
|
$title = $lang === 'ar' ? 'سياسة الخصوصية' : 'Privacy Policy';
|
||||||
|
$content = $lang === 'ar' ? ($settings['privacy_ar'] ?? '') : ($settings['privacy_en'] ?? '');
|
||||||
|
|
||||||
|
render_header($title, 'home');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container py-5 my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<h1 class="display-5 fw-bold mb-4 <?= $lang === 'ar' ? 'text-end' : '' ?>"><?= e($title) ?></h1>
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4 p-md-5 <?= $lang === 'ar' ? 'text-end' : '' ?>" <?= $lang === 'ar' ? 'dir="rtl"' : '' ?>>
|
||||||
|
<?php if (trim($content) === ''): ?>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<?= $lang === 'ar' ? 'لم يتم تقديم سياسة الخصوصية بعد.' : 'Privacy policy has not been provided yet.' ?>
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="policy-content">
|
||||||
|
<?= nl2br(e($content)) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
218
profile.php
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_login();
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
// Demo auth: if not logged in, pretend to be user 1
|
||||||
|
$userId = $_SESSION['user_id'] ?? 1;
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Fetch current user data
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM users WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $userId]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
if (!$user) {
|
||||||
|
die("Demo user not found. Please register an account first.");
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
die("Database error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_profile') {
|
||||||
|
$fullName = trim($_POST['full_name'] ?? '');
|
||||||
|
|
||||||
|
if ($fullName === '') {
|
||||||
|
$errors[] = t('error_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$profilePicPath = $user['profile_picture'];
|
||||||
|
|
||||||
|
// Handle file upload
|
||||||
|
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
$fileInfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mimeType = finfo_file($fileInfo, $_FILES['profile_picture']['tmp_name']);
|
||||||
|
finfo_close($fileInfo);
|
||||||
|
|
||||||
|
if (!in_array($mimeType, $allowedTypes)) {
|
||||||
|
$errors[] = t('invalid_image');
|
||||||
|
} else {
|
||||||
|
$uploadDir = __DIR__ . '/uploads/profiles/';
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = pathinfo($_FILES['profile_picture']['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = 'profile_' . $userId . '_' . time() . '.' . $extension;
|
||||||
|
$destination = $uploadDir . $filename;
|
||||||
|
|
||||||
|
if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $destination)) {
|
||||||
|
$profilePicPath = '/uploads/profiles/' . $filename;
|
||||||
|
} else {
|
||||||
|
$errors[] = t('upload_failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password Update Logic
|
||||||
|
$newPassword = trim($_POST['new_password'] ?? '');
|
||||||
|
$confirmPassword = trim($_POST['confirm_password'] ?? '');
|
||||||
|
|
||||||
|
if ($newPassword !== '') {
|
||||||
|
if ($newPassword !== $confirmPassword) {
|
||||||
|
$errors[] = t('passwords_do_not_match');
|
||||||
|
} elseif (strlen($newPassword) < 6) {
|
||||||
|
$errors[] = t('password_too_short');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
try {
|
||||||
|
$sql = "UPDATE users SET full_name = :name, profile_picture = :pic";
|
||||||
|
$params = [
|
||||||
|
':name' => $fullName,
|
||||||
|
':pic' => $profilePicPath,
|
||||||
|
':id' => $userId
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($newPassword !== '') {
|
||||||
|
$sql .= ", password = :pass";
|
||||||
|
$params[':pass'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " WHERE id = :id";
|
||||||
|
|
||||||
|
$updateStmt = db()->prepare($sql);
|
||||||
|
$updateStmt->execute($params);
|
||||||
|
|
||||||
|
$msg = t('profile_updated');
|
||||||
|
if ($newPassword !== '') {
|
||||||
|
$msg .= ' ' . t('password_updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
set_flash('success', $msg);
|
||||||
|
header("Location: " . url_with_lang('profile.php'));
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errors[] = "Database update failed: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('my_profile'), 'profile');
|
||||||
|
$flash = get_flash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="panel p-4 p-md-5">
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<i class="bi bi-person-badge fs-2 text-primary me-3"></i>
|
||||||
|
<h2 class="section-title mb-0"><?= e(t('my_profile')) ?></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= $flash['type'] === 'error' ? 'danger' : 'success' ?> mb-4" data-auto-dismiss="true">
|
||||||
|
<?= e($flash['message']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-danger mb-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<?php foreach ($errors as $err): ?>
|
||||||
|
<li><?= e($err) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="update_profile">
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="position-relative d-inline-block">
|
||||||
|
<?php if (!empty($user['profile_picture'])): ?>
|
||||||
|
<img src="<?= e($user['profile_picture']) ?>" alt="<?= e(t('profile_picture')) ?>" class="rounded-circle shadow-sm object-fit-cover" style="width: 120px; height: 120px;">
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center shadow-sm" style="width: 120px; height: 120px;">
|
||||||
|
<i class="bi bi-person text-secondary" style="font-size: 4rem;"></i>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="profile_picture" class="position-absolute bottom-0 end-0 bg-primary text-white rounded-circle p-2 cursor-pointer shadow" style="cursor: pointer;" title="<?= e(t('change_picture')) ?>">
|
||||||
|
<i class="bi bi-camera-fill"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="profile_picture" name="profile_picture" class="d-none" accept="image/jpeg,image/png,image/gif,image/webp" onchange="previewImage(this)">
|
||||||
|
<div class="small text-muted mt-2"><?= e(t('picture_hint')) ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('full_name')) ?></label>
|
||||||
|
<input type="text" class="form-control form-control-lg" name="full_name" value="<?= e($user['full_name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('email_address')) ?></label>
|
||||||
|
<input type="email" class="form-control form-control-lg bg-light" value="<?= e($user['email'] ?? '') ?>" readonly disabled>
|
||||||
|
<div class="form-text"><?= e(t('email_hint')) ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('account_role')) ?></label>
|
||||||
|
<input type="text" class="form-control form-control-lg bg-light text-capitalize" value="<?= e($user['role'] ?? '') ?>" readonly disabled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h4 class="mb-3"><?= e(t('change_password')) ?></h4>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('new_password')) ?></label>
|
||||||
|
<input type="password" class="form-control form-control-lg" name="new_password" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold"><?= e(t('confirm_password')) ?></label>
|
||||||
|
<input type="password" class="form-control form-control-lg" name="confirm_password" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg w-100 shadow-sm rounded-pill fw-bold">
|
||||||
|
<?= e(t('save_changes')) ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function previewImage(input) {
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var imgContainer = input.previousElementSibling;
|
||||||
|
if (imgContainer.querySelector('img')) {
|
||||||
|
imgContainer.querySelector('img').src = e.target.result;
|
||||||
|
} else {
|
||||||
|
var newImg = document.createElement('img');
|
||||||
|
newImg.src = e.target.result;
|
||||||
|
newImg.className = 'rounded-circle shadow-sm object-fit-cover';
|
||||||
|
newImg.style.width = '120px';
|
||||||
|
newImg.style.height = '120px';
|
||||||
|
var iconDiv = imgContainer.querySelector('div');
|
||||||
|
if (iconDiv) {
|
||||||
|
iconDiv.replaceWith(newImg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
409
register.php
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$saved = false;
|
||||||
|
$role = $_GET['role'] ?? 'shipper';
|
||||||
|
if (!in_array($role, ['shipper', 'truck_owner'], true)) {
|
||||||
|
$role = 'shipper';
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'full_name' => '',
|
||||||
|
'email' => '',
|
||||||
|
'phone' => '',
|
||||||
|
'country_id' => '',
|
||||||
|
'city_id' => '',
|
||||||
|
'address_line' => '',
|
||||||
|
'company_name' => '',
|
||||||
|
'bank_account' => '',
|
||||||
|
'bank_name' => '',
|
||||||
|
'bank_branch' => '',
|
||||||
|
'is_company' => '0',
|
||||||
|
'ctr_number' => '',
|
||||||
|
'notes' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$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') { validate_csrf_token();
|
||||||
|
$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 = [
|
||||||
|
'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,
|
||||||
|
'bank_account' => trim($_POST['bank_account'] ?? ''),
|
||||||
|
'bank_name' => trim($_POST['bank_name'] ?? ''),
|
||||||
|
'bank_branch' => trim($_POST['bank_branch'] ?? ''),
|
||||||
|
'is_company' => isset($_POST['is_company']) ? '1' : '0',
|
||||||
|
'ctr_number' => trim($_POST['ctr_number'] ?? ''),
|
||||||
|
'notes' => trim($_POST['notes'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($fullName === '') {
|
||||||
|
$errors[] = t('error_fullname_required');
|
||||||
|
}
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors[] = t('error_email_invalid');
|
||||||
|
}
|
||||||
|
if ($phone === '') {
|
||||||
|
$errors[] = t('error_phone_required');
|
||||||
|
}
|
||||||
|
if ($countryId <= 0 || $cityId <= 0) {
|
||||||
|
$errors[] = t('error_location_required');
|
||||||
|
} else {
|
||||||
|
$cityCheck = db()->prepare("SELECT COUNT(*) FROM cities WHERE id = ? AND country_id = ?");
|
||||||
|
$cityCheck->execute([$cityId, $countryId]);
|
||||||
|
if ((int)$cityCheck->fetchColumn() === 0) {
|
||||||
|
$errors[] = t('error_city_mismatch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($addressLine === '') {
|
||||||
|
$errors[] = t('error_address_required');
|
||||||
|
}
|
||||||
|
if (strlen($passwordRaw) < 6) {
|
||||||
|
$errors[] = t('error_password_length');
|
||||||
|
}
|
||||||
|
if ($role === 'shipper' && $companyName === '') {
|
||||||
|
$errors[] = t('error_company_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$password = password_hash($passwordRaw, PASSWORD_DEFAULT);
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$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 {
|
||||||
|
$uploadDir = __DIR__ . '/uploads/profiles/' . $userId . '/';
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
$filename = uniqid($prefix, true) . '.' . $allowed[$mime];
|
||||||
|
$target = $uploadDir . $filename;
|
||||||
|
if (!move_uploaded_file($tmpName, $target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 'uploads/profiles/' . basename($uploadDir) . '/' . $filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
$idCardPaths = [];
|
||||||
|
$ctrPath = null;
|
||||||
|
|
||||||
|
if ($values['is_company'] === '1') {
|
||||||
|
if (is_uploaded_file($_FILES['ctr_document']['tmp_name'] ?? '')) {
|
||||||
|
$ctrPath = $saveImage($_FILES['ctr_document']['tmp_name'], (int)$_FILES['ctr_document']['size'], 'ctr_');
|
||||||
|
}
|
||||||
|
if (!$ctrPath) $errors[] = t('error_ctr_required');
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (count($idCardPaths) < 2) $errors[] = t('error_id_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$ownerStmt = $pdo->prepare(
|
||||||
|
"INSERT INTO truck_owner_profiles (user_id, phone, country_id, city_id, address_line, bank_account, bank_name, bank_branch, id_card_path, is_company, ctr_number, notes)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
$ownerStmt->execute([
|
||||||
|
$userId,
|
||||||
|
$phone,
|
||||||
|
$countryId,
|
||||||
|
$cityId,
|
||||||
|
$addressLine,
|
||||||
|
$values['bank_account'],
|
||||||
|
$values['bank_name'],
|
||||||
|
$values['bank_branch'],
|
||||||
|
$values['is_company'] === '1' ? $ctrPath : json_encode($idCardPaths, JSON_UNESCAPED_SLASHES),
|
||||||
|
$values['is_company'],
|
||||||
|
$values['ctr_number'],
|
||||||
|
$values['notes']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
} else {
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$user = [
|
||||||
|
'id' => $userId,
|
||||||
|
'email' => $email,
|
||||||
|
'full_name' => $fullName,
|
||||||
|
'role' => $role,
|
||||||
|
'phone' => $phone
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once __DIR__ . '/includes/NotificationService.php';
|
||||||
|
NotificationService::send('welcome_message', $user, [], $lang);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('Failed to send welcome notification: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$saved = true;
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||||
|
$errors[] = t('error_email_exists');
|
||||||
|
} else {
|
||||||
|
$errors[] = t('error_registration_failed') . ' (' . $e->getMessage() . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header('Shipper & Truck Owner Registration');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-intro">
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('reg_title')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('reg_subtitle')) ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-4">
|
||||||
|
<?php if ($saved): ?>
|
||||||
|
<?php if ($role === 'truck_owner'): ?>
|
||||||
|
<div class="alert alert-success"><?= e(t('reg_success_pending')) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-success"><?= e(t('reg_success')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" id="regForm" novalidate> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="role" value="<?= e($role) ?>">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="full_name"><?= e(t('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">
|
||||||
|
<label class="form-label" for="email"><?= e(t('email')) ?></label>
|
||||||
|
<input type="email" name="email" id="email" class="form-control" value="<?= e($values['email']) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="password"><?= e(t('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"><?= e(t('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"><?= e(t('country')) ?></label>
|
||||||
|
<select name="country_id" id="country_id" class="form-select" onchange="syncCities()" required>
|
||||||
|
<option value=""><?= e(t('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"><?= e(t('city')) ?></label>
|
||||||
|
<select name="city_id" id="city_id" class="form-select" required data-selected="<?= e($values['city_id']) ?>">
|
||||||
|
<option value=""><?= e(t('select_city')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" for="address_line"><?= e(t('address')) ?></label>
|
||||||
|
<input type="text" name="address_line" id="address_line" class="form-control" value="<?= e($values['address_line']) ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($role === 'shipper'): ?>
|
||||||
|
<div id="shipperFields" class="mt-4">
|
||||||
|
<h2 class="h5 mb-3"><?= e(t('shipper_details')) ?></h2>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="company_name"><?= e(t('company_name')) ?></label>
|
||||||
|
<input type="text" name="company_name" id="company_name" class="form-control" value="<?= e($values['company_name']) ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div id="truckFields" class="mt-4">
|
||||||
|
<h2 class="h5 mb-3"><?= e(t('truck_details')) ?></h2>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_company" id="is_company" value="1" <?= $values['is_company'] === '1' ? 'checked' : '' ?> onchange="toggleCompanyFields()">
|
||||||
|
<label class="form-check-label" for="is_company"><?= e(t('is_company_checkbox')) ?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_account"><?= e(t('bank_account')) ?></label>
|
||||||
|
<input type="text" name="bank_account" id="bank_account" class="form-control" value="<?= e($values['bank_account']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_name"><?= e(t('bank_name')) ?></label>
|
||||||
|
<input type="text" name="bank_name" id="bank_name" class="form-control" value="<?= e($values['bank_name']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="bank_branch"><?= e(t('bank_branch')) ?></label>
|
||||||
|
<input type="text" name="bank_branch" id="bank_branch" class="form-control" value="<?= e($values['bank_branch']) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="individualDocs" class="row g-3 mt-0">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="id_card_front"><?= e(t('id_card_front')) ?></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-6">
|
||||||
|
<label class="form-label" for="id_card_back"><?= e(t('id_card_back')) ?></label>
|
||||||
|
<input type="file" name="id_card_back" id="id_card_back" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="companyDocs" class="row g-3 mt-0" style="display:none;">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="ctr_number"><?= e(t('ctr_number')) ?></label>
|
||||||
|
<input type="text" name="ctr_number" id="ctr_number" class="form-control" value="<?= e($values['ctr_number']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" for="ctr_document"><?= e(t('ctr_document')) ?></label>
|
||||||
|
<input type="file" name="ctr_document" id="ctr_document" class="form-control" accept="image/png,image/jpeg,image/webp">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label" for="notes"><?= e(t('notes')) ?></label>
|
||||||
|
<textarea name="notes" id="notes" class="form-control"><?= e($values['notes']) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary"><?= e(t('create_account')) ?></button>
|
||||||
|
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('index.php')) ?>"><?= e(t('back_to_home')) ?></a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
<?php if ($lang === 'ar'): ?>
|
||||||
|
/* Override default browser file input text */
|
||||||
|
input[type="file"]::file-selector-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const allCities = <?= json_encode($cities, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
const lang = '<?= e($lang) ?>';
|
||||||
|
|
||||||
|
// Force browser-specific file input text override
|
||||||
|
function updateFileInputPlaceholder() {
|
||||||
|
if (lang === 'ar') {
|
||||||
|
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||||
|
fileInputs.forEach(input => {
|
||||||
|
// Standard browser "No file chosen" isn't directly settable via CSS
|
||||||
|
// but we can wrap it or use custom UI if needed.
|
||||||
|
// For now, we apply basic directionality for better UI.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCities() {
|
||||||
|
const countryId = document.getElementById('country_id').value;
|
||||||
|
const citySelect = document.getElementById('city_id');
|
||||||
|
const selectedValue = citySelect.dataset.selected || '';
|
||||||
|
citySelect.innerHTML = '<option value=""><?= e(t('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 toggleCompanyFields() {
|
||||||
|
const isCompany = document.getElementById('is_company').checked;
|
||||||
|
const front = document.getElementById('id_card_front');
|
||||||
|
const back = document.getElementById('id_card_back');
|
||||||
|
const ctr = document.getElementById('ctr_document');
|
||||||
|
|
||||||
|
if (front) front.required = !isCompany;
|
||||||
|
if (back) back.required = !isCompany;
|
||||||
|
if (ctr) ctr.required = isCompany;
|
||||||
|
|
||||||
|
document.getElementById('individualDocs').style.display = isCompany ? 'none' : 'flex';
|
||||||
|
document.getElementById('companyDocs').style.display = isCompany ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
syncCities();
|
||||||
|
updateFileInputPlaceholder();
|
||||||
|
<?php if ($role === 'truck_owner'): ?>
|
||||||
|
toggleCompanyFields();
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer();
|
||||||
15
scan_sidebar_pattern.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import glob
|
||||||
|
import re
|
||||||
|
|
||||||
|
files = glob.glob("admin_*.php")
|
||||||
|
pattern = re.compile(r'<div class="col-md-10(.*?)"')
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
with open(file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
match = pattern.search(content)
|
||||||
|
if match:
|
||||||
|
print(f"{file}: {match.group(1)}")
|
||||||
|
else:
|
||||||
|
if "render_admin_sidebar" in content:
|
||||||
|
print(f"{file}: NO MATCH for col-md-10")
|
||||||
465
shipment_detail.php
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_login();
|
||||||
|
require_once __DIR__ . '/includes/NotificationService.php';
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$shipmentId = (int) ($_GET['id'] ?? 0);
|
||||||
|
$shipment = null;
|
||||||
|
|
||||||
|
if ($shipmentId > 0) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM shipments WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $shipmentId]);
|
||||||
|
$shipment = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$flash = get_flash();
|
||||||
|
$userRole = $_SESSION['user_role'] ?? '';
|
||||||
|
$currentUserId = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
|
||||||
|
$isAdmin = $userRole === 'admin';
|
||||||
|
$isShipper = $userRole === 'shipper';
|
||||||
|
$isTruckOwner = $userRole === 'truck_owner';
|
||||||
|
$isAssignedTruckOwner = $shipment && $shipment['truck_owner_id'] == $currentUserId;
|
||||||
|
|
||||||
|
// Platform Fee Configuration
|
||||||
|
$settings = get_settings();
|
||||||
|
$platformFeePercentage = (float)($settings['platform_charge_percentage'] ?? 0) / 100;
|
||||||
|
$pricingModel = $settings['pricing_model'] ?? 'percentage';
|
||||||
|
|
||||||
|
// Handle POST actions
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'submit_offer') { if (!$isTruckOwner && !$isAdmin) { $errors[] = 'Only truck owners can submit offers.'; }
|
||||||
|
$offerOwner = trim($_POST['offer_owner'] ?? '');
|
||||||
|
$offerPrice = trim($_POST['offer_price'] ?? '');
|
||||||
|
|
||||||
|
// Enforce fixed price logic if applicable
|
||||||
|
if ($pricingModel === 'fixed_price' && !empty($shipment['target_price'])) {
|
||||||
|
$offerPrice = $shipment['target_price'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($offerOwner === '' || $offerPrice === '') {
|
||||||
|
$errors[] = t('error_required');
|
||||||
|
} elseif (!is_numeric($offerPrice)) {
|
||||||
|
$errors[] = t('error_invalid');
|
||||||
|
} else {
|
||||||
|
$truckOwnerId = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||||
|
|
||||||
|
$stmt = db()->prepare(
|
||||||
|
"UPDATE shipments SET offer_owner = :offer_owner, offer_price = :offer_price, status = 'offered', truck_owner_id = :truck_owner_id
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':offer_owner' => $offerOwner,
|
||||||
|
':offer_price' => $offerPrice,
|
||||||
|
':truck_owner_id' => $truckOwnerId,
|
||||||
|
':id' => $shipmentId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Notify Shipper
|
||||||
|
if (!empty($shipment['shipper_id'])) {
|
||||||
|
$shipperUser = db()->query("SELECT * FROM users WHERE id = " . (int)$shipment['shipper_id'])->fetch();
|
||||||
|
if ($shipperUser) {
|
||||||
|
NotificationService::send('shipment_offered', $shipperUser, [
|
||||||
|
'shipment_id' => $shipmentId,
|
||||||
|
'offer_price' => $offerPrice,
|
||||||
|
'offer_owner' => $offerOwner
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_flash('success', t('success_offer'));
|
||||||
|
header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId]));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($action === 'accept_offer' && $isShipper) {
|
||||||
|
if ($shipment && $shipment['status'] === 'offered' && $shipment['offer_price'] > 0) {
|
||||||
|
$offerPrice = (float)$shipment['offer_price'];
|
||||||
|
$platformFee = $offerPrice * $platformFeePercentage;
|
||||||
|
$totalPrice = $offerPrice + $platformFee;
|
||||||
|
|
||||||
|
$stmt = db()->prepare(
|
||||||
|
"UPDATE shipments
|
||||||
|
SET status = 'confirmed',
|
||||||
|
payment_status = 'paid',
|
||||||
|
platform_fee = :fee,
|
||||||
|
total_price = :total
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':fee' => $platformFee,
|
||||||
|
':total' => $totalPrice,
|
||||||
|
':id' => $shipmentId
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Notify Truck Owner
|
||||||
|
if (!empty($shipment['truck_owner_id'])) {
|
||||||
|
$truckOwnerUser = db()->query("SELECT * FROM users WHERE id = " . (int)$shipment['truck_owner_id'])->fetch();
|
||||||
|
if ($truckOwnerUser) {
|
||||||
|
NotificationService::send('shipment_accepted', $truckOwnerUser, [
|
||||||
|
'shipment_id' => $shipmentId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_flash('success', t('Payment successful. Shipment confirmed!'));
|
||||||
|
header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId]));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($action === 'reject_offer' && $isShipper) {
|
||||||
|
if ($shipment && $shipment['status'] === 'offered') {
|
||||||
|
// Notify Truck Owner first (before clearing ID)
|
||||||
|
if (!empty($shipment['truck_owner_id'])) {
|
||||||
|
$truckOwnerUser = db()->query("SELECT * FROM users WHERE id = " . (int)$shipment['truck_owner_id'])->fetch();
|
||||||
|
if ($truckOwnerUser) {
|
||||||
|
NotificationService::send('shipment_rejected', $truckOwnerUser, [
|
||||||
|
'shipment_id' => $shipmentId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = db()->prepare(
|
||||||
|
"UPDATE shipments
|
||||||
|
SET status = 'posted',
|
||||||
|
offer_price = NULL,
|
||||||
|
offer_owner = NULL,
|
||||||
|
truck_owner_id = NULL
|
||||||
|
WHERE id = :id"
|
||||||
|
);
|
||||||
|
$stmt->execute([':id' => $shipmentId]);
|
||||||
|
|
||||||
|
set_flash('success', 'Offer rejected. Shipment is now open for new offers.');
|
||||||
|
header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId]));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif ($action === 'upload_pod') {
|
||||||
|
if (($isAdmin || ($isTruckOwner && $isAssignedTruckOwner)) && in_array($shipment['status'], ['confirmed', 'in_transit', 'delivered'])) {
|
||||||
|
if (isset($_FILES['pod_file']) && $_FILES['pod_file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$fileTmpPath = $_FILES['pod_file']['tmp_name'];
|
||||||
|
$fileName = $_FILES['pod_file']['name'];
|
||||||
|
$fileSize = $_FILES['pod_file']['size'];
|
||||||
|
$fileType = $_FILES['pod_file']['type'];
|
||||||
|
$fileNameCmps = explode(".", $fileName);
|
||||||
|
$fileExtension = strtolower(end($fileNameCmps));
|
||||||
|
|
||||||
|
$allowedfileExtensions = ['jpg', 'jpeg', 'png', 'pdf'];
|
||||||
|
if (in_array($fileExtension, $allowedfileExtensions)) {
|
||||||
|
$newFileName = 'pod_' . $shipmentId . '_' . md5(time() . $fileName) . '.' . $fileExtension;
|
||||||
|
$uploadFileDir = __DIR__ . '/uploads/pods/';
|
||||||
|
if (!is_dir($uploadFileDir)) {
|
||||||
|
mkdir($uploadFileDir, 0777, true);
|
||||||
|
}
|
||||||
|
$dest_path = $uploadFileDir . $newFileName;
|
||||||
|
|
||||||
|
if(move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||||
|
$dbPath = 'uploads/pods/' . $newFileName;
|
||||||
|
$stmt = db()->prepare("UPDATE shipments SET pod_file = :pod_file, status = 'delivered' WHERE id = :id");
|
||||||
|
$stmt->execute([':pod_file' => $dbPath, ':id' => $shipmentId]);
|
||||||
|
|
||||||
|
set_flash('success', 'Proof of Delivery uploaded successfully.');
|
||||||
|
header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId]));
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$errors[] = 'There was some error moving the file to upload directory.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Upload failed. Allowed file types: ' . implode(',', $allowedfileExtensions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'No file uploaded or upload error.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'You are not authorized to upload POD for this shipment.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_header(t('shipment_detail'));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (!$shipment): ?>
|
||||||
|
<div class="panel p-4">
|
||||||
|
<p class="muted mb-0"><?= e(t('no_shipments')) ?></p>
|
||||||
|
<div class="mt-3">
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<a href="<?= e(url_with_lang('admin_shipments.php')) ?>" class="btn btn-outline-primary">Back to Shipments</a>
|
||||||
|
<?php elseif ($isShipper): ?>
|
||||||
|
<a href="<?= e(url_with_lang('shipper_dashboard.php')) ?>" class="btn btn-outline-primary">Back to Dashboard</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?= e(url_with_lang('index.php')) ?>" class="btn btn-outline-primary"><?= e(t('nav_home')) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="panel p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title mb-1"><?= e(t('shipment_detail')) ?> #<?= e($shipment['id']) ?></h2>
|
||||||
|
<p class="muted mb-0"><?= e(t('created_at')) ?>: <?= e($shipment['created_at']) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$statusBadgeClass = ($shipment['status'] === 'pending_approval') ? 'bg-warning text-dark' : $shipment['status'];
|
||||||
|
?>
|
||||||
|
<span class="badge-status <?= e($statusBadgeClass) ?>"><?= e(status_label($shipment['status'])) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('shipper_company')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['shipper_company']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('shipper_name')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['shipper_name']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('origin')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['origin_city']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('destination')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['destination_city']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('shipment_type')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e(t('type_' . strtolower($shipment['shipment_type'] ?? 'dry'))) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('cargo')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['cargo_description']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('weight')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['weight_tons']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('pickup_date')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['pickup_date']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('delivery_date')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['delivery_date']) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('payment_method')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= e($shipment['payment_method'] === 'bank_transfer' ? t('payment_bank') : t('payment_thawani')) ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('best_offer')) ?></div>
|
||||||
|
<div class="fw-semibold"><?= $shipment['offer_price'] ? e($shipment['offer_price'] . ' / ' . ($shipment['offer_owner'] ?? '')) : e(t('no_offers')) ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($shipment['target_price'])): ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="small text-muted"><?= e(t('target_price')) ?></div>
|
||||||
|
<div class="fw-semibold text-info">$<?= e($shipment['target_price']) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($shipment['payment_status'] === 'paid'): ?>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-success d-flex align-items-center mb-0">
|
||||||
|
<i class="bi bi-check-circle-fill fs-4 me-2"></i>
|
||||||
|
<div>
|
||||||
|
<strong>Payment Complete</strong><br>
|
||||||
|
Total Paid: <?= format_currency((float)$shipment['total_price']) ?> (Includes <?= format_currency((float)$shipment['platform_fee']) ?> platform fee)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 d-flex gap-2">
|
||||||
|
<?php if ($isAdmin): ?>
|
||||||
|
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('admin_shipments.php')) ?>">
|
||||||
|
<i class="bi bi-arrow-left me-2"></i>Back to Shipments
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary" href="<?= e(url_with_lang('admin_shipment_edit.php', ['id' => $shipmentId])) ?>">
|
||||||
|
<i class="bi bi-pencil me-2"></i>Edit Shipment
|
||||||
|
</a>
|
||||||
|
<?php elseif ($isShipper): ?>
|
||||||
|
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('shipper_dashboard.php')) ?>"><?= e(t('shipper_dashboard')) ?></a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a class="btn btn-outline-dark" href="<?= e(url_with_lang('truck_owner_dashboard.php')) ?>"><?= e(t('nav_owner')) ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Actions (Offer / Payment) -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<?php if ($isShipper): ?>
|
||||||
|
<!-- Shipper View -->
|
||||||
|
<div class="panel p-4 h-100">
|
||||||
|
<h3 class="section-title">Accept Offer</h3>
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($shipment['status'] === 'offered' && $shipment['offer_price'] > 0): ?>
|
||||||
|
<?php
|
||||||
|
$offerPrice = (float)$shipment['offer_price'];
|
||||||
|
$fee = $offerPrice * $platformFeePercentage;
|
||||||
|
$total = $offerPrice + $fee;
|
||||||
|
?>
|
||||||
|
<div class="card bg-light border-0 mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title fw-bold mb-3">Payment Breakdown</h5>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Truck Offer</span>
|
||||||
|
<span class="fw-medium"><?= format_currency($offerPrice) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-3">
|
||||||
|
<span class="text-muted">Platform Fee (<?= e($platformFeePercentage * 100) ?>%)</span>
|
||||||
|
<span class="fw-medium"><?= format_currency($fee) ?></span>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-bold fs-5">Total</span>
|
||||||
|
<span class="fw-bold fs-4 text-primary"><?= format_currency($total) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<form method="post"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="accept_offer">
|
||||||
|
<button class="btn btn-success w-100 py-3 fw-bold shadow-sm" type="submit">
|
||||||
|
<i class="bi bi-credit-card-2-front me-2"></i>Accept & Pay Now
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" onsubmit="return confirm('Are you sure you want to reject this offer?');"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="reject_offer">
|
||||||
|
<button class="btn btn-outline-danger w-100 py-2 fw-bold" type="submit">
|
||||||
|
<i class="bi bi-x-circle me-2"></i>Reject Offer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted small mt-3 mb-0">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i> Secure payment via <?= e($shipment['payment_method'] === 'bank_transfer' ? 'Bank Transfer' : 'Thawani') ?>
|
||||||
|
</p>
|
||||||
|
<?php elseif ($shipment['status'] === 'posted'): ?>
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-hourglass-split fs-1 text-muted mb-3 d-block"></i>
|
||||||
|
<p class="mb-0 text-muted">Waiting for truck owners to submit offers.</p>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($shipment['status'] === 'pending_approval'): ?>
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-clipboard-check fs-1 text-warning mb-3 d-block"></i>
|
||||||
|
<p class="mb-0 text-muted"><?= t('pending_approval_msg') ?></p>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($shipment['status'] === 'confirmed' || $shipment['status'] === 'in_transit' || $shipment['status'] === 'delivered'): ?>
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-check-circle-fill fs-1 text-success mb-3 d-block"></i>
|
||||||
|
<h4 class="h5">Offer Accepted</h4>
|
||||||
|
<p class="mb-0 text-muted">This shipment has been confirmed and paid for.</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- POD Display for Shipper -->
|
||||||
|
<?php if (!empty($shipment['pod_file'])): ?>
|
||||||
|
<div class="mt-4 pt-4 border-top">
|
||||||
|
<h4 class="h6 text-muted mb-3">Proof of Delivery</h4>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body d-flex align-items-center">
|
||||||
|
<i class="bi bi-file-earmark-text fs-3 text-primary me-3"></i>
|
||||||
|
<div>
|
||||||
|
<a href="<?= e($shipment['pod_file']) ?>" target="_blank" class="fw-bold text-decoration-none stretched-link">View Document</a>
|
||||||
|
<div class="small text-muted">Uploaded by Truck Owner</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Truck Owner / Admin / Other View (Submit Offer & POD) -->
|
||||||
|
<div class="panel p-4">
|
||||||
|
<h3 class="section-title"><?= e(t('submit_offer')) ?></h3>
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($shipment['status'] === 'posted' || $shipment['status'] === 'offered'): ?>
|
||||||
|
<form method="post"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="submit_offer">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('offer_owner')) ?></label>
|
||||||
|
<input class="form-control" name="offer_owner" value="<?= e($shipment['offer_owner'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($pricingModel === 'fixed_price' && !empty($shipment['target_price'])): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('target_price')) ?></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input class="form-control bg-light" value="<?= e($shipment['target_price']) ?>" readonly>
|
||||||
|
<input type="hidden" name="offer_price" value="<?= e($shipment['target_price']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-text text-success fw-bold"><i class="bi bi-check-circle me-1"></i> Fixed Price Offer</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-success w-100 fw-bold shadow-sm" type="submit">
|
||||||
|
Accept Load ($<?= e($shipment['target_price']) ?>)
|
||||||
|
</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label"><?= e(t('offer_price')) ?></label>
|
||||||
|
<input class="form-control" name="offer_price" type="number" step="0.01" min="0.1" value="<?= e($shipment['offer_price'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100" type="submit"><?= e(t('submit_offer')) ?></button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
This shipment is already confirmed/processed or pending approval.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Proof of Delivery Section -->
|
||||||
|
<?php if (in_array($shipment['status'], ['confirmed', 'in_transit', 'delivered'])): ?>
|
||||||
|
<div class="mt-5 pt-4 border-top">
|
||||||
|
<h4 class="section-title mb-3">Proof of Delivery</h4>
|
||||||
|
|
||||||
|
<?php if (!empty($shipment['pod_file'])): ?>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body d-flex align-items-center">
|
||||||
|
<i class="bi bi-check-circle-fill fs-3 text-success me-3"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold text-success">POD Uploaded</div>
|
||||||
|
<a href="<?= e($shipment['pod_file']) ?>" target="_blank" class="small text-decoration-none">View Document</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($isAdmin || ($isTruckOwner && $isAssignedTruckOwner)): ?>
|
||||||
|
<form method="post" enctype="multipart/form-data"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="upload_pod">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">Upload POD Document (Image/PDF)</label>
|
||||||
|
<input type="file" name="pod_file" class="form-control" accept="image/*,.pdf" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-dark w-100">
|
||||||
|
<i class="bi bi-upload me-2"></i>Upload POD
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
400
shipper_dashboard.php
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('shipper');
|
||||||
|
require_once __DIR__ . '/includes/NotificationService.php';
|
||||||
|
|
||||||
|
ensure_schema();
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$settings = get_settings();
|
||||||
|
$pricingModel = $settings['pricing_model'] ?? 'percentage';
|
||||||
|
|
||||||
|
// Try to pre-fill from profile if logged in
|
||||||
|
$prefillName = $_SESSION['shipper_name'] ?? '';
|
||||||
|
$prefillCompany = '';
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("SELECT u.full_name, p.company_name FROM users u LEFT JOIN shipper_profiles p ON u.id = p.user_id WHERE u.id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
$profile = $stmt->fetch();
|
||||||
|
if ($profile) {
|
||||||
|
$prefillName = $profile['full_name'];
|
||||||
|
$prefillCompany = $profile['company_name'] ?? '';
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_shipment') {
|
||||||
|
$shipperName = trim($_POST['shipper_name'] ?? '');
|
||||||
|
$shipperCompany = trim($_POST['shipper_company'] ?? '');
|
||||||
|
$origin = trim($_POST['origin_city'] ?? '');
|
||||||
|
$destination = trim($_POST['destination_city'] ?? '');
|
||||||
|
$shipmentType = trim($_POST['shipment_type'] ?? 'Dry');
|
||||||
|
$cargo = trim($_POST['cargo_description'] ?? '');
|
||||||
|
$weight = trim($_POST['weight_tons'] ?? '');
|
||||||
|
$pickupDate = trim($_POST['pickup_date'] ?? '');
|
||||||
|
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||||
|
$payment = $_POST['payment_method'] ?? 'thawani';
|
||||||
|
|
||||||
|
$targetPrice = ($pricingModel === 'fixed_price') ? trim($_POST['target_price'] ?? '') : null;
|
||||||
|
$initialStatus = ($pricingModel === 'fixed_price') ? 'pending_approval' : 'posted';
|
||||||
|
|
||||||
|
if ($shipperName === '' || $shipperCompany === '' || $origin === '' || $destination === '' || $cargo === '' || $weight === '' || $pickupDate === '' || $deliveryDate === '') {
|
||||||
|
$errors[] = t('error_required');
|
||||||
|
} elseif (!is_numeric($weight)) {
|
||||||
|
$errors[] = t('error_invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pricingModel === 'fixed_price' && (!is_numeric($targetPrice) || $targetPrice <= 0)) {
|
||||||
|
$errors[] = t('error_invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$shipperId = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||||
|
|
||||||
|
$stmt = db()->prepare(
|
||||||
|
"INSERT INTO shipments (shipper_id, shipper_name, shipper_company, origin_city, destination_city, shipment_type, cargo_description, weight_tons, pickup_date, delivery_date, payment_method, status, target_price)
|
||||||
|
VALUES (:shipper_id, :shipper_name, :shipper_company, :origin_city, :destination_city, :shipment_type, :cargo_description, :weight_tons, :pickup_date, :delivery_date, :payment_method, :status, :target_price)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':shipper_id' => $shipperId,
|
||||||
|
':shipper_name' => $shipperName,
|
||||||
|
':shipper_company' => $shipperCompany,
|
||||||
|
':origin_city' => $origin,
|
||||||
|
':destination_city' => $destination,
|
||||||
|
':shipment_type' => $shipmentType,
|
||||||
|
':cargo_description' => $cargo,
|
||||||
|
':weight_tons' => $weight,
|
||||||
|
':pickup_date' => $pickupDate,
|
||||||
|
':delivery_date' => $deliveryDate,
|
||||||
|
':payment_method' => $payment,
|
||||||
|
':status' => $initialStatus,
|
||||||
|
':target_price' => $targetPrice,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newShipmentId = db()->lastInsertId();
|
||||||
|
$_SESSION['shipper_name'] = $shipperName;
|
||||||
|
$_SESSION['shipper_company_session'] = $shipperCompany; // for rudimentary filtering
|
||||||
|
|
||||||
|
// Notify Shipper (Confirmation)
|
||||||
|
if ($shipperId) {
|
||||||
|
$user = db()->query("SELECT * FROM users WHERE id = $shipperId")->fetch();
|
||||||
|
if ($user) {
|
||||||
|
NotificationService::send('shipment_created', $user, [
|
||||||
|
'shipment_id' => $newShipmentId,
|
||||||
|
'origin' => $origin,
|
||||||
|
'destination' => $destination
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = ($initialStatus === 'pending_approval') ? t('pending_approval_msg') : t('success_shipment');
|
||||||
|
set_flash('success', $msg);
|
||||||
|
header('Location: ' . url_with_lang('shipper_dashboard.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipments = [];
|
||||||
|
$filterCompany = $_SESSION['shipper_company_session'] ?? $prefillCompany;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($filterCompany) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM shipments WHERE shipper_company = ? ORDER BY created_at DESC LIMIT 20");
|
||||||
|
$stmt->execute([$filterCompany]);
|
||||||
|
} else {
|
||||||
|
$stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 20");
|
||||||
|
}
|
||||||
|
$shipments = $stmt->fetchAll();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$shipments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total' => 0,
|
||||||
|
'active' => 0,
|
||||||
|
'delivered' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($filterCompany) {
|
||||||
|
$stmt = db()->prepare("SELECT COUNT(*) FROM shipments WHERE shipper_company = ?");
|
||||||
|
$stmt->execute([$filterCompany]);
|
||||||
|
$stats['total'] = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = db()->prepare("SELECT COUNT(*) FROM shipments WHERE shipper_company = ? AND status != 'delivered'");
|
||||||
|
$stmt->execute([$filterCompany]);
|
||||||
|
$stats['active'] = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = db()->prepare("SELECT COUNT(*) FROM shipments WHERE shipper_company = ? AND status = 'delivered'");
|
||||||
|
$stmt->execute([$filterCompany]);
|
||||||
|
$stats['delivered'] = (int)$stmt->fetchColumn();
|
||||||
|
} else {
|
||||||
|
$stats['total'] = (int)db()->query("SELECT COUNT(*) FROM shipments")->fetchColumn();
|
||||||
|
$stats['active'] = (int)db()->query("SELECT COUNT(*) FROM shipments WHERE status != 'delivered'")->fetchColumn();
|
||||||
|
$stats['delivered'] = (int)db()->query("SELECT COUNT(*) FROM shipments WHERE status = 'delivered'")->fetchColumn();
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
// Fetch countries for dropdowns
|
||||||
|
$countries = [];
|
||||||
|
try {
|
||||||
|
$countries = db()->query("SELECT * FROM countries ORDER BY name_en ASC")->fetchAll();
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
render_header(t('shipper_dashboard'), 'shipper');
|
||||||
|
$flash = get_flash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="section-title mb-1"><?= e(t('shipper_dashboard')) ?></h1>
|
||||||
|
<p class="muted mb-0"><?= e(t('welcome_back') ?? 'Welcome to your dashboard. Manage your cargo shipments here.') ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-box-seam" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-primary mb-2 position-relative"><i class="bi bi-box-seam fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['total'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('total_shipments_posted')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-truck" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-warning mb-2 position-relative"><i class="bi bi-truck fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['active'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('active_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel p-4 text-center h-100 shadow-sm border-0 rounded-4 position-relative overflow-hidden" style="background: linear-gradient(135deg, #ffffff, #f8f9fa);">
|
||||||
|
<div class="position-absolute opacity-10" style="inset-inline-end: 10px; top: 15px;"><i class="bi bi-check-circle" style="font-size: 3.5rem;"></i></div>
|
||||||
|
<div class="text-success mb-2 position-relative"><i class="bi bi-check-circle fs-2"></i></div>
|
||||||
|
<h3 class="h2 mb-0 fw-bold position-relative"><?= $stats['delivered'] ?></h3>
|
||||||
|
<p class="text-muted small text-uppercase mb-0 fw-bold position-relative" style="letter-spacing: 0.5px;"><?= e(t('delivered_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="panel p-4 shadow-sm border-0 rounded-4 h-100">
|
||||||
|
<h2 class="h5 fw-bold mb-4"><i class="bi bi-plus-circle-fill text-primary me-2"></i><?= e(t('new_shipment')) ?></h2>
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($errors): ?>
|
||||||
|
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="create_shipment">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('shipper_name')) ?></label>
|
||||||
|
<input class="form-control" name="shipper_name" value="<?= e($prefillName) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('shipper_company')) ?></label>
|
||||||
|
<input class="form-control" name="shipper_company" value="<?= e($prefillCompany) ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold">Origin Country</label>
|
||||||
|
<select class="form-select" id="origin_country" required>
|
||||||
|
<option value="">Select Country</option>
|
||||||
|
<?php foreach ($countries as $country): ?>
|
||||||
|
<option value="<?= e($country['id']) ?>"><?= e($country['name_en']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('origin')) ?></label>
|
||||||
|
<select class="form-select" name="origin_city" id="origin_city" disabled required>
|
||||||
|
<option value="">Select City</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold">Destination Country</label>
|
||||||
|
<select class="form-select" id="destination_country" required>
|
||||||
|
<option value="">Select Country</option>
|
||||||
|
<?php foreach ($countries as $country): ?>
|
||||||
|
<option value="<?= e($country['id']) ?>"><?= e($country['name_en']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('destination')) ?></label>
|
||||||
|
<select class="form-select" name="destination_city" id="destination_city" disabled required>
|
||||||
|
<option value="">Select City</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('shipment_type')) ?></label>
|
||||||
|
<select class="form-select" name="shipment_type">
|
||||||
|
<option value="Dry"><?= t('type_dry') ?></option>
|
||||||
|
<option value="Cold"><?= t('type_cold') ?></option>
|
||||||
|
<option value="Frozen"><?= t('type_frozen') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('cargo')) ?></label>
|
||||||
|
<input class="form-control" name="cargo_description" placeholder="<?= e(t('cargo_placeholder') ?? 'e.g. 20 Pallets of Electronics') ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($pricingModel === 'fixed_price'): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('target_price')) ?></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input class="form-control" name="target_price" type="number" step="0.01" min="0.1" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-text small text-info"><i class="bi bi-info-circle me-1"></i><?= e(t('pending_approval_msg')) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('weight')) ?></label>
|
||||||
|
<input class="form-control" name="weight_tons" type="number" step="0.01" min="0.1" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('payment_method')) ?></label>
|
||||||
|
<select class="form-select" name="payment_method">
|
||||||
|
<option value="thawani"><?= e(t('payment_thawani')) ?></option>
|
||||||
|
<option value="bank_transfer"><?= e(t('payment_bank')) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('pickup_date')) ?></label>
|
||||||
|
<input class="form-control" name="pickup_date" type="date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label text-muted small fw-bold"><?= e(t('delivery_date')) ?></label>
|
||||||
|
<input class="form-control" name="delivery_date" type="date" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100 py-2 fw-bold rounded-pill shadow-sm" type="submit"><?= e(t('submit_shipment')) ?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="panel p-4 shadow-sm border-0 rounded-4 h-100 d-flex flex-column">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="h5 mb-0 fw-bold"><i class="bi bi-list-ul text-muted me-2"></i><?= e(t('shipments_list')) ?></h2>
|
||||||
|
<span class="badge bg-light text-dark border"><?= e(count($shipments)) ?> <?= e(t('total_label') ?? 'total') ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$shipments): ?>
|
||||||
|
<div class="text-center p-5 text-muted flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
<i class="bi bi-inbox fs-1 mb-3 d-block opacity-50"></i>
|
||||||
|
<p class="mb-0"><?= e(t('no_shipments')) ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive flex-grow-1">
|
||||||
|
<table class="table align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0"><?= e(t('route_label') ?? 'Route') ?></th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0"><?= e(t('status')) ?></th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0"><?= e(t('offer')) ?></th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0 text-end"><?= e(t('actions')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($shipments as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="fw-medium"><?= e($row['origin_city']) ?></span>
|
||||||
|
<i class="bi bi-arrow-right text-muted small"></i>
|
||||||
|
<span class="fw-medium"><?= e($row['destination_city']) ?></span>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted"><?= e(date('M d', strtotime($row['pickup_date']))) ?></small>
|
||||||
|
<?php if (!empty($row['shipment_type'])): ?>
|
||||||
|
<div class="text-xs text-muted mt-1">
|
||||||
|
<span class="badge bg-light text-secondary border"><?= e(t('type_' . strtolower($row['shipment_type']))) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($row['target_price'])): ?>
|
||||||
|
<div class="text-xs text-info mt-1 fw-bold">
|
||||||
|
<?= e(t('target_price')) ?>: $<?= e($row['target_price']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge <?= e($row['status'] === 'pending_approval' ? 'bg-warning text-dark' : $row['status']) ?> rounded-pill px-3 py-2"><?= e(status_label($row['status'])) ?></span></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($row['offer_price']): ?>
|
||||||
|
<div class="fw-bold text-success">$<?= e($row['offer_price']) ?></div>
|
||||||
|
<small class="text-muted"><?= e($row['offer_owner']) ?></small>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted small"><?= e(t('no_offers')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-sm p-1 border-0 bg-transparent text-primary" href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>" title="<?= e(t('view')) ?>">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
function setupCityLoader(countrySelectId, citySelectId) {
|
||||||
|
const countrySelect = document.getElementById(countrySelectId);
|
||||||
|
const citySelect = document.getElementById(citySelectId);
|
||||||
|
|
||||||
|
countrySelect.addEventListener('change', function() {
|
||||||
|
const countryId = this.value;
|
||||||
|
citySelect.innerHTML = '<option value="">Loading...</option>';
|
||||||
|
citySelect.disabled = true;
|
||||||
|
|
||||||
|
if (countryId) {
|
||||||
|
fetch('api/get_cities.php?country_id=' + countryId)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
citySelect.innerHTML = '<option value="">Select City</option>';
|
||||||
|
data.forEach(city => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = city.name_en; // Using name as value for DB compatibility
|
||||||
|
option.textContent = city.name_en;
|
||||||
|
citySelect.appendChild(option);
|
||||||
|
});
|
||||||
|
citySelect.disabled = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching cities:', error);
|
||||||
|
citySelect.innerHTML = '<option value="">Error loading cities</option>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
citySelect.innerHTML = '<option value="">Select City</option>';
|
||||||
|
citySelect.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCityLoader('origin_country', 'origin_city');
|
||||||
|
setupCityLoader('destination_country', 'destination_city');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
36
terms.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php';
|
||||||
|
|
||||||
|
$settings = get_settings();
|
||||||
|
global $lang;
|
||||||
|
|
||||||
|
$title = $lang === 'ar' ? 'شروط الخدمة' : 'Terms of Service';
|
||||||
|
$content = $lang === 'ar' ? ($settings['terms_ar'] ?? '') : ($settings['terms_en'] ?? '');
|
||||||
|
|
||||||
|
render_header($title, 'home');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container py-5 my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<h1 class="display-5 fw-bold mb-4 <?= $lang === 'ar' ? 'text-end' : '' ?>"><?= e($title) ?></h1>
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4 p-md-5 <?= $lang === 'ar' ? 'text-end' : '' ?>" <?= $lang === 'ar' ? 'dir="rtl"' : '' ?>>
|
||||||
|
<?php if (trim($content) === ''): ?>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<?= $lang === 'ar' ? 'لم يتم تقديم شروط الخدمة بعد.' : 'Terms of service have not been provided yet.' ?>
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="policy-content">
|
||||||
|
<?= nl2br(e($content)) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
177
truck_owner_dashboard.php
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout.php'; require_role('truck_owner');
|
||||||
|
|
||||||
|
$userId = $_SESSION['user_id'];
|
||||||
|
$settings = get_settings();
|
||||||
|
$pricingModel = $settings['pricing_model'] ?? 'percentage';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_truck') {
|
||||||
|
$truckType = trim($_POST['truck_type'] ?? '');
|
||||||
|
$loadCapacity = trim($_POST['load_capacity'] ?? '');
|
||||||
|
$plateNo = trim($_POST['plate_no'] ?? '');
|
||||||
|
$regExpiry = trim($_POST['registration_expiry_date'] ?? '');
|
||||||
|
$insExpiry = trim($_POST['insurance_expiry_date'] ?? '');
|
||||||
|
|
||||||
|
if ($truckType === '' || $loadCapacity === '' || $plateNo === '' || $regExpiry === '' || $insExpiry === '') {
|
||||||
|
set_flash('error', 'All truck details including expiry dates are required.');
|
||||||
|
} elseif (!is_numeric($loadCapacity) || (float)$loadCapacity <= 0) {
|
||||||
|
set_flash('error', 'Load capacity must be numeric and greater than zero.');
|
||||||
|
} else {
|
||||||
|
$stmt = db()->prepare("INSERT INTO trucks (user_id, truck_type, load_capacity, plate_no, registration_expiry_date, insurance_expiry_date, is_approved) VALUES (?, ?, ?, ?, ?, ?, 0)");
|
||||||
|
$stmt->execute([$userId, $truckType, $loadCapacity, $plateNo, $regExpiry, $insExpiry]);
|
||||||
|
set_flash('success', 'Truck added successfully, pending admin approval.');
|
||||||
|
}
|
||||||
|
header('Location: ' . url_with_lang('truck_owner_dashboard.php'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$trucks = db()->prepare("SELECT * FROM trucks WHERE user_id = ?");
|
||||||
|
$trucks->execute([$userId]);
|
||||||
|
$myTrucks = $trucks->fetchAll();
|
||||||
|
|
||||||
|
// Fetch Available Shipments
|
||||||
|
$availShipments = [];
|
||||||
|
try {
|
||||||
|
$stmt = db()->query("SELECT * FROM shipments WHERE status = 'posted' ORDER BY created_at DESC");
|
||||||
|
$availShipments = $stmt->fetchAll();
|
||||||
|
} catch (Throwable $e) {}
|
||||||
|
|
||||||
|
render_header('Truck Owner Dashboard', 'owner');
|
||||||
|
$flash = get_flash();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="page-intro mb-4">
|
||||||
|
<h1 class="section-title mb-1">Truck Owner Dashboard</h1>
|
||||||
|
<p class="muted mb-0">Manage your trucks and view available shipments.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= $flash['type'] ?>" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="panel p-4 mb-4">
|
||||||
|
<h5 class="mb-3">Add New Truck</h5>
|
||||||
|
<form method="post" class="row g-3"> <?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="action" value="add_truck">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label small text-muted">Truck Type</label>
|
||||||
|
<input type="text" name="truck_type" class="form-control" placeholder="e.g. Refrigerated" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small text-muted">Capacity (T)</label>
|
||||||
|
<input type="number" name="load_capacity" class="form-control" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small text-muted">Plate No</label>
|
||||||
|
<input type="text" name="plate_no" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small text-muted">Reg. Expiry</label>
|
||||||
|
<input type="date" name="registration_expiry_date" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label small text-muted">Ins. Expiry</label>
|
||||||
|
<input type="date" name="insurance_expiry_date" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 pt-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Add Truck</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel p-4">
|
||||||
|
<h5 class="mb-3">My Trucks</h5>
|
||||||
|
<?php if (!$myTrucks): ?>
|
||||||
|
<p class="text-muted small">No trucks registered yet.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<?php foreach ($myTrucks as $truck): ?>
|
||||||
|
<?php
|
||||||
|
$isExpired = (strtotime($truck['registration_expiry_date']) < time()) || (strtotime($truck['insurance_expiry_date']) < time());
|
||||||
|
?>
|
||||||
|
<div class="list-group-item px-0 py-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<strong class="text-dark"><?= e($truck['plate_no']) ?></strong>
|
||||||
|
<?php if ($isExpired): ?>
|
||||||
|
<span class="badge bg-danger">Expired</span>
|
||||||
|
<?php elseif ($truck['is_approved']): ?>
|
||||||
|
<span class="badge bg-success">Approved</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-warning text-dark">Pending</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted"><?= e($truck['truck_type']) ?> • <?= e($truck['load_capacity']) ?> Tons</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="panel p-4 h-100">
|
||||||
|
<h5 class="mb-3">Available Shipments</h5>
|
||||||
|
<?php if (!$availShipments): ?>
|
||||||
|
<div class="text-center p-5 text-muted">
|
||||||
|
<i class="bi bi-inbox fs-1 mb-3 d-block opacity-50"></i>
|
||||||
|
<p class="mb-0">No shipments available at the moment.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0">Route</th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0">Details</th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0">Dates</th>
|
||||||
|
<th class="text-uppercase small text-muted border-top-0 text-end">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($availShipments as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="fw-medium"><?= e($row['origin_city']) ?></span>
|
||||||
|
<i class="bi bi-arrow-right text-muted small"></i>
|
||||||
|
<span class="fw-medium"><?= e($row['destination_city']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($row['shipment_type'])): ?>
|
||||||
|
<div class="text-xs text-muted mt-1">
|
||||||
|
<span class="badge bg-light text-secondary border"><?= e(t('type_' . strtolower($row['shipment_type']))) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($row['target_price'])): ?>
|
||||||
|
<div class="text-success small fw-bold mt-1">
|
||||||
|
Price: $<?= e($row['target_price']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="small text-dark fw-bold"><?= e($row['cargo_description']) ?></div>
|
||||||
|
<div class="small text-muted"><?= e($row['weight_tons']) ?> Tons • <?= e($row['payment_method']) ?></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="small"><span class="text-muted">Pick:</span> <?= e(date('M d', strtotime($row['pickup_date']))) ?></div>
|
||||||
|
<div class="small"><span class="text-muted">Drop:</span> <?= e(date('M d', strtotime($row['delivery_date']))) ?></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php render_footer(); ?>
|
||||||
BIN
uploads/pages/img_69ad2b68c8965.jfif
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
uploads/pages/img_69ad2b9960534.jfif
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
uploads/pages/img_69ad2bff407d4.jpg
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
uploads/profiles/profile_7_1772962468.png
Normal file
|
After Width: | Height: | Size: 542 B |
1
uploads/trucks/reg_69c222f680ca5.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
uploads/trucks/reg_69c222f68d477.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
uploads/trucks/reg_69c222f68f3ca.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
uploads/trucks/reg_69c222f690c1c.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
uploads/trucks/reg_69c222f691903.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
uploads/trucks/reg_69c222f69343f.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
uploads/trucks/reg_69c222f697e5d.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
uploads/trucks/reg_69c222f6996d8.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
uploads/trucks/reg_69c222f69a0ab.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
uploads/trucks/reg_69c222f69b3a3.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
uploads/trucks/truck_69c222f680ca4.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
uploads/trucks/truck_69c222f68d475.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
uploads/trucks/truck_69c222f68f3c8.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
uploads/trucks/truck_69c222f690c1b.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
uploads/trucks/truck_69c222f691901.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
uploads/trucks/truck_69c222f69343d.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
uploads/trucks/truck_69c222f697e5b.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
uploads/trucks/truck_69c222f6996d7.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
uploads/trucks/truck_69c222f69a0a9.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
uploads/trucks/truck_69c222f69b3a2.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 |