updating landing page

This commit is contained in:
Flatlogic Bot 2026-03-08 10:11:07 +00:00
parent 022932f8fe
commit d424fc2360
30 changed files with 1055 additions and 263 deletions

View File

@ -107,14 +107,14 @@ if ($editCityId > 0) {
}
}
render_header('Manage Cities', 'admin');
render_header('Manage Cities', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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>

View File

@ -87,14 +87,14 @@ $currentTermsAr = $settings['terms_ar'] ?? '';
$currentPrivacyEn = $settings['privacy_en'] ?? '';
$currentPrivacyAr = $settings['privacy_ar'] ?? '';
render_header('Company Profile', 'admin');
render_header('Company Profile', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<div class="col-md-10 p-4">
<div class="page-intro mb-4">
<h1 class="section-title mb-1">Company Profile</h1>
<p class="muted mb-0">Update your app name, logo, favicon, contact details, platform charge, and legal policies.</p>
@ -109,7 +109,7 @@ render_header('Company Profile', 'admin');
<div class="panel p-4">
<form method="post" enctype="multipart/form-data">
<div class="row g-4">
<div class="row g-0">
<div class="col-md-6">
<label class="form-label fw-bold">Company / App Name</label>
<input type="text" name="company_name" class="form-control" value="<?= e($currentName) ?>" required>

View File

@ -85,14 +85,14 @@ if ($editCountryId > 0) {
}
}
render_header('Manage Countries', 'admin');
render_header('Manage Countries', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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>

View File

@ -33,14 +33,14 @@ try {
$flash = get_flash();
render_header(t('admin_dashboard'), 'admin');
render_header(t('admin_dashboard'), 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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">Overview of your platform's performance and recent activity.</p>
@ -82,7 +82,7 @@ render_header(t('admin_dashboard'), 'admin');
</div>
</div>
<div class="row g-4">
<div class="row g-0">
<!-- Main Content: Shipments -->
<div class="col-lg-8">
<div class="panel p-4 shadow-sm border-0 h-100 rounded-4 d-flex flex-column">

View File

@ -101,14 +101,14 @@ if ($editFaqId > 0) {
}
}
render_header('Manage FAQs', 'admin');
render_header('Manage FAQs', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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>

View File

@ -50,14 +50,14 @@ $smtpPass = $settings['smtp_pass'] ?? '';
$mailFrom = $settings['mail_from'] ?? '';
$mailFromName = $settings['mail_from_name'] ?? '';
render_header('Integrations', 'admin');
render_header('Integrations', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<div class="col-md-10 p-4">
<div class="page-intro mb-4">
<h1 class="section-title mb-1">Integrations</h1>
<p class="muted mb-0">Manage your payment gateway and communication APIs.</p>
@ -82,7 +82,7 @@ render_header('Integrations', 'admin');
</h3>
<p class="text-muted small mb-4">Configure your Oman-based Thawani Pay integration to process shipment payments.</p>
<div class="row g-4">
<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">
@ -114,7 +114,7 @@ render_header('Integrations', 'admin');
</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-4">
<div class="row g-0">
<div class="col-md-6">
<label class="form-label fw-bold">Wablas Server Domain</label>
<input type="text" name="wablas_domain" class="form-control" value="<?= e($wablasDomain) ?>" placeholder="e.g. https://solo.wablas.com">
@ -144,7 +144,7 @@ render_header('Integrations', 'admin');
</h3>
<p class="text-muted small mb-4">Configure your SMTP server to send emails and system notifications.</p>
<div class="row g-4">
<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">

View File

@ -18,10 +18,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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;
@ -42,12 +46,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
if ($action === 'create') {
$stmt = $pdo->prepare("INSERT INTO landing_sections (title, subtitle, content, image_path, layout, button_text, button_link, section_order, is_active, section_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'custom')");
$stmt->execute([$title, $subtitle, $content, $image_path, $layout, $button_text, $button_link, $section_order, $is_active]);
$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=?, subtitle=?, content=?, image_path=?, layout=?, button_text=?, button_link=?, section_order=?, is_active=? WHERE id=?");
$stmt->execute([$title, $subtitle, $content, $image_path, $layout, $button_text, $button_link, $section_order, $is_active, $id]);
$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'));
@ -82,14 +86,14 @@ if ($editId) {
$editSection = $stmt->fetch();
}
render_header(t('app_name') . ' - Landing Pages', 'admin');
render_header(t('app_name') . ' - Landing Pages', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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>
@ -103,7 +107,7 @@ render_header(t('app_name') . ' - Landing Pages', 'admin');
</div>
<?php endif; ?>
<div class="row g-4">
<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>
@ -115,21 +119,36 @@ render_header(t('app_name') . ' - Landing Pages', 'admin');
<?php endif; ?>
<div class="mb-3">
<label class="form-label">Title <span class="text-danger">*</span></label>
<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 (Optional)</label>
<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">Content (HTML allowed)</label>
<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>
@ -147,14 +166,19 @@ render_header(t('app_name') . ' - Landing Pages', 'admin');
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Button Text</label>
<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 Link (e.g. login.php)</label>
<input type="text" name="button_link" class="form-control" value="<?= e($editSection['button_link'] ?? '') ?>">
<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>
@ -193,6 +217,9 @@ render_header(t('app_name') . ' - Landing Pages', 'admin');
<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 btn-outline-primary">Edit</a>

View File

@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
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', 'Template not found.');
header('Location: ' . url_with_lang('admin_notification_templates.php'));
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$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[] = 'English subject and body are 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', 'Template updated successfully.');
header('Location: ' . url_with_lang('admin_notification_templates.php'));
exit;
}
}
render_header('Edit Notification 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">Edit Template: <?= e($template['event_name']) ?></h2>
<a href="<?= e(url_with_lang('admin_notification_templates.php')) ?>" class="btn btn-outline-secondary">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">
<div class="row g-4">
<div class="col-md-6">
<h5 class="mb-3 border-bottom pb-2">English</h5>
<div class="mb-3">
<label class="form-label">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">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">Use placeholders like {shipment_id}, {user_name}, {offer_price}.</div>
</div>
<div class="mb-3">
<label class="form-label">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">Arabic</h5>
<div class="mb-3" dir="rtl">
<label class="form-label">موضوع البريد الإلكتروني</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">نص البريد الإلكتروني</label>
<textarea class="form-control" name="email_body_ar" rows="6"><?= e($template['email_body_ar']) ?></textarea>
</div>
<div class="mb-3" dir="rtl">
<label class="form-label">نص الواتساب</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">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php
render_footer();
exit;
}
// List View
$stmt = db()->query("SELECT * FROM notification_templates ORDER BY event_name ASC");
$templates = $stmt->fetchAll();
render_header('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">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">Event Name</th>
<th>Subject (EN)</th>
<th>Subject (AR)</th>
<th class="text-end pe-4">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 btn-outline-primary">
Edit
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php render_footer(); ?>

View File

@ -109,14 +109,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
render_header('Edit Shipment', 'admin');
render_header('Edit Shipment', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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="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">

View File

@ -66,14 +66,14 @@ $stmt = db()->prepare($sql);
$stmt->execute($params);
$shipments = $stmt->fetchAll();
render_header('Manage Shipments', 'admin');
render_header('Manage Shipments', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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">Shipments</h1>

View File

@ -101,14 +101,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
render_header('Edit Shipper', 'admin');
render_header('Edit Shipper', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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">&larr; Back to Shippers</a>

View File

@ -75,14 +75,14 @@ $stmt = db()->prepare($sql);
$stmt->execute($params);
$shippers = $stmt->fetchAll();
render_header('Manage Shippers', 'admin');
render_header('Manage Shippers', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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">Shippers</h1>

View File

@ -126,14 +126,14 @@ $idCards = json_decode($owner['id_card_path'] ?? '[]', true) ?: [];
$regs = json_decode($owner['registration_path'] ?? '[]', true) ?: [];
$pic = $owner['truck_pic_path'];
render_header('Edit Truck Owner', 'admin');
render_header('Edit Truck Owner', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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_truck_owners.php" class="text-decoration-none small text-muted mb-2 d-inline-block">&larr; Back to Truck Owners</a>

View File

@ -75,14 +75,14 @@ $stmt = db()->prepare($sql);
$stmt->execute($params);
$owners = $stmt->fetchAll();
render_header('Manage Truck Owners', 'admin');
render_header('Manage Truck Owners', 'admin', true);
?>
<div class="row g-4">
<div class="col-lg-3">
<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-lg-9">
<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">Truck Owners</h1>

View File

@ -238,8 +238,10 @@ body.app-body {
.admin-sidebar {
position: sticky;
top: 88px;
border-radius: 16px;
top: 76px;
height: calc(100vh - 76px);
overflow-y: auto;
border-radius: 0;
}
.admin-nav-link {
@ -289,5 +291,7 @@ body.app-body {
.admin-sidebar {
position: static;
height: auto;
overflow-y: visible;
}
}

View 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"; }

View 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();
}

View 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();
}

View File

@ -0,0 +1,73 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../mail/MailService.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 'email' and 'full_name')
* @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);
}
// Log WhatsApp (Mock)
// In a real app, this would call Twilio/Meta API
error_log("WHATSAPP Notification to {$user['email']} (Phone N/A): $whatsapp");
}
}

View File

@ -13,142 +13,366 @@ $_SESSION['lang'] = $lang;
$dir = $lang === 'ar' ? 'rtl' : 'ltr';
$translations = [
'en' => [
'app_name' => 'CargoLink',
'nav_home' => 'Overview',
'nav_shipper' => 'Shipper Desk',
'nav_owner' => 'Truck Owner Desk',
'nav_admin' => 'Admin Panel',
'hero_title' => 'Move cargo faster with verified trucks.',
'hero_subtitle' => 'Post shipments, collect offers, and pay via Thawani or bank transfer. Built for local and nearby cross-border moves.',
'hero_tagline' => 'Multilingual Logistics Marketplace',
'register_shipper' => 'Register as Shipper',
'register_owner' => 'Register as Truck Owner',
'cta_shipper' => 'Post a shipment',
'cta_owner' => 'Find loads',
'cta_admin' => 'Open admin',
'stats_shipments' => 'Shipments posted',
'stats_offers' => 'Active offers',
'stats_confirmed' => 'Confirmed trips',
'section_workflow' => 'How it works',
'recent_shipments' => 'Recent shipments',
'step_post' => 'Shipper posts cargo details and preferred payment.',
'step_offer' => 'Truck owners respond with their best rate.',
'step_confirm' => 'Admin confirms booking and status.',
'shipper_dashboard' => 'Shipper Dashboard',
'new_shipment' => 'Create shipment',
'shipper_name' => 'Shipper name',
'shipper_company' => 'Company',
'origin' => 'Origin city',
'destination' => 'Destination city',
'cargo' => 'Cargo description',
'weight' => 'Weight (tons)',
'pickup_date' => 'Pickup date',
'delivery_date' => 'Delivery date',
'payment_method' => 'Payment method',
'payment_thawani' => 'Thawani online payment',
'payment_bank' => 'Bank transfer',
'submit_shipment' => 'Submit shipment',
'shipments_list' => 'Your latest shipments',
'status' => 'Status',
'offer' => 'Best offer',
'actions' => 'Actions',
'view' => 'View',
'owner_dashboard' => 'Truck Owner Dashboard',
'available_shipments' => 'Available shipments',
'offer_price' => 'Offer price',
'offer_owner' => 'Truck owner name',
'submit_offer' => 'Send offer',
'admin_dashboard' => 'Admin Dashboard',
'update_status' => 'Update status',
'save' => 'Save',
'shipment_detail' => 'Shipment detail',
'created_at' => 'Created',
'best_offer' => 'Best offer',
'assign_owner' => 'Assigned owner',
'no_shipments' => 'No shipments yet. Create the first one to get started.',
'no_offers' => 'No offers yet.',
'success_shipment' => 'Shipment posted successfully.',
'success_offer' => 'Offer submitted to the shipper.',
'success_status' => 'Status updated.',
'error_required' => 'Please fill in all required fields.',
'error_invalid' => 'Please enter valid values.',
'status_posted' => 'Posted',
'status_offered' => 'Offered',
'status_confirmed' => 'Confirmed',
'status_in_transit' => 'In transit',
'status_delivered' => 'Delivered',
'footer_note' => 'This is the initial MVP slice. Payments are not yet connected.',
'marketing_title_1' => 'For Shippers', 'marketing_desc_1' => 'Find the right truck for your cargo quickly and securely. Post your load and get offers instantly.', 'marketing_title_2' => 'For Truck Owners', 'marketing_desc_2' => 'Maximize your earnings and eliminate empty miles. Browse available shipments and offer your rate.', 'motivation_phrase' => 'Empowering the logistics of tomorrow.', 'why_choose_us' => 'لماذا تختار كارجو لينك؟', 'feature_1_title' => 'Fast Matching', 'feature_1_desc' => 'Connect with available trucks or shipments in minutes.', 'feature_2_title' => 'Secure Payments', 'feature_2_desc' => 'Your transactions are protected with security.', 'feature_3_title' => 'Verified Users', 'feature_3_desc' => 'We verify all truck owners to ensure peace of mind.',
],
'ar' => [
'app_name' => 'CargoLink',
'nav_home' => 'نظرة عامة',
'nav_shipper' => 'لوحة الشاحن',
'nav_owner' => 'لوحة مالك الشاحنة',
'nav_admin' => 'لوحة الإدارة',
'hero_title' => 'انقل شحنتك بسرعة مع شاحنات موثوقة.',
'hero_subtitle' => 'أنشئ شحنة، استلم عروضاً، وادفع عبر ثواني أو التحويل البنكي.',
'hero_tagline' => 'منصة لوجستية متعددة اللغات',
'register_shipper' => 'التسجيل كشاحن',
'register_owner' => 'التسجيل كمالك شاحنة',
'cta_shipper' => 'إنشاء شحنة',
'cta_owner' => 'البحث عن الشحنات',
'cta_admin' => 'الدخول للإدارة',
'stats_shipments' => 'الشحنات المنشورة',
'stats_offers' => 'العروض الحالية',
'stats_confirmed' => 'الرحلات المؤكدة',
'section_workflow' => 'طريقة العمل',
'recent_shipments' => 'أحدث الشحنات',
'step_post' => 'يقوم الشاحن بإدخال تفاصيل الشحنة وطريقة الدفع.',
'step_offer' => 'يرسل أصحاب الشاحنات أفضل عروضهم.',
'step_confirm' => 'تؤكد الإدارة الحجز وتحدث الحالة.',
'shipper_dashboard' => 'لوحة الشاحن',
'new_shipment' => 'إنشاء شحنة',
'shipper_name' => 'اسم الشاحن',
'shipper_company' => 'الشركة',
'origin' => 'مدينة الانطلاق',
'destination' => 'مدينة الوصول',
'cargo' => 'وصف الحمولة',
'weight' => 'الوزن (طن)',
'pickup_date' => 'تاريخ الاستلام',
'delivery_date' => 'تاريخ التسليم',
'payment_method' => 'طريقة الدفع',
'payment_thawani' => 'الدفع الإلكتروني عبر ثواني',
'payment_bank' => 'تحويل بنكي',
'submit_shipment' => 'إرسال الشحنة',
'shipments_list' => 'أحدث الشحنات',
'status' => 'الحالة',
'offer' => 'أفضل عرض',
'actions' => 'إجراءات',
'view' => 'عرض',
'owner_dashboard' => 'لوحة مالك الشاحنة',
'available_shipments' => 'الشحنات المتاحة',
'offer_price' => 'سعر العرض',
'offer_owner' => 'اسم مالك الشاحنة',
'submit_offer' => 'إرسال العرض',
'admin_dashboard' => 'لوحة الإدارة',
'update_status' => 'تحديث الحالة',
'save' => 'حفظ',
'shipment_detail' => 'تفاصيل الشحنة',
'created_at' => 'تم الإنشاء',
'best_offer' => 'أفضل عرض',
'assign_owner' => 'المالك المعتمد',
'no_shipments' => 'لا توجد شحنات بعد. ابدأ بإنشاء أول شحنة.',
'no_offers' => 'لا توجد عروض بعد.',
'success_shipment' => 'تم نشر الشحنة بنجاح.',
'success_offer' => 'تم إرسال العرض إلى الشاحن.',
'success_status' => 'تم تحديث الحالة.',
'error_required' => 'يرجى تعبئة جميع الحقول المطلوبة.',
'error_invalid' => 'يرجى إدخال قيم صحيحة.',
'status_posted' => 'منشورة',
'status_offered' => 'بعرض',
'status_confirmed' => 'مؤكدة',
'status_in_transit' => 'قيد النقل',
'status_delivered' => 'تم التسليم',
'footer_note' => 'هذه هي النسخة الأولية. الدفع غير متصل بعد.',
'marketing_title_1' => 'للشاحنين', 'marketing_desc_1' => 'ابحث عن الشاحنة المناسبة لحمولتك بسرعة وأمان.', 'marketing_title_2' => 'لأصحاب الشاحنات', 'marketing_desc_2' => 'عظّم أرباحك وتجنب العودة فارغاً.', 'motivation_phrase' => 'تمكين الخدمات اللوجستية للمستقبل.', 'why_choose_us' => 'لماذا تختار كارجو لينك؟', 'feature_1_title' => 'مطابقة سريعة', 'feature_1_desc' => 'تواصل مع الشاحنات المتاحة في دقائق.', 'feature_2_title' => 'مدفوعات آمنة', 'feature_2_desc' => 'معاملاتك محمية بأعلى معايير الأمان.', 'feature_3_title' => 'مستخدمون موثوقون', 'feature_3_desc' => 'نقوم بالتحقق من جميع أصحاب الشاحنات لضمان راحتك.',
],
"en" => array (
'app_name' => 'CargoLink',
'nav_home' => 'Overview',
'nav_shipper' => 'Shipper Desk',
'nav_owner' => 'Truck Owner Desk',
'nav_admin' => 'Admin Panel',
'hero_title' => 'Move cargo faster with verified trucks.',
'hero_subtitle' => 'Post shipments, collect offers, and pay via Thawani or bank transfer. Built for local and nearby cross-border moves.',
'hero_tagline' => 'Multilingual Logistics Marketplace',
'register_shipper' => 'Register as Shipper',
'register_owner' => 'Register as Truck Owner',
'cta_shipper' => 'Post a shipment',
'cta_owner' => 'Find loads',
'cta_admin' => 'Open admin',
'stats_shipments' => 'Shipments posted',
'stats_offers' => 'Active offers',
'stats_confirmed' => 'Confirmed trips',
'section_workflow' => 'How it works',
'recent_shipments' => 'Recent shipments',
'step_post' => 'Shipper posts cargo details and preferred payment.',
'step_offer' => 'Truck owners respond with their best rate.',
'step_confirm' => 'Admin confirms booking and status.',
'step_confirm_desc' => 'Secure booking and track the delivery until completion.',
'shipper_dashboard' => 'Shipper Dashboard',
'new_shipment' => 'Create shipment',
'shipper_name' => 'Shipper name',
'shipper_company' => 'Company',
'origin' => 'Origin city',
'destination' => 'Destination city',
'cargo' => 'Cargo description',
'cargo_placeholder' => 'e.g. 20 Pallets of Electronics',
'weight' => 'Weight (tons)',
'pickup_date' => 'Pickup date',
'delivery_date' => 'Delivery date',
'payment_method' => 'Payment method',
'payment_thawani' => 'Thawani online payment',
'payment_bank' => 'Bank transfer',
'submit_shipment' => 'Submit shipment',
'shipments_list' => 'Your latest shipments',
'status' => 'Status',
'offer' => 'Best offer',
'actions' => 'Actions',
'view' => 'View',
'owner_dashboard' => 'Truck Owner Dashboard',
'available_shipments' => 'Available shipments',
'offer_price' => 'Offer price',
'offer_owner' => 'Truck owner name',
'submit_offer' => 'Send offer',
'admin_dashboard' => 'Admin Dashboard',
'update_status' => 'Update status',
'save' => 'Save',
'shipment_detail' => 'Shipment detail',
'created_at' => 'Created',
'best_offer' => 'Best offer',
'assign_owner' => 'Assigned owner',
'no_shipments' => 'No shipments yet. Create the first one to get started.',
'no_offers' => 'No offers yet.',
'success_shipment' => 'Shipment posted successfully.',
'success_offer' => 'Offer submitted to the shipper.',
'success_status' => 'Status updated.',
'error_required' => 'Please fill in all required fields.',
'error_invalid' => 'Please enter valid values.',
'status_posted' => 'Posted',
'status_offered' => 'Offered',
'status_confirmed' => 'Confirmed',
'status_in_transit' => 'In transit',
'status_delivered' => 'Delivered',
'footer_note' => 'This is the initial MVP slice. Payments are not yet connected.',
'marketing_title_1' => 'For Shippers',
'marketing_desc_1' => 'Find the right truck for your cargo quickly and securely. Post your load and get offers instantly.',
'marketing_title_2' => 'For Truck Owners',
'marketing_desc_2' => 'Maximize your earnings and eliminate empty miles. Browse available shipments and offer your rate.',
'motivation_phrase' => 'Empowering the logistics of tomorrow.',
'why_choose_us' => 'Why Choose CargoLink?',
'feature_1_title' => 'Fast Matching',
'feature_1_desc' => 'Connect with available trucks or shipments in minutes.',
'feature_2_title' => 'Secure Payments',
'feature_2_desc' => 'Your transactions are protected with security.',
'feature_3_title' => 'Verified Users',
'feature_3_desc' => 'We verify all truck owners to ensure peace of mind.',
'view_faq' => 'View FAQ',
'faq_title' => 'Have Questions?',
'faq_subtitle' => 'Check out our Frequently Asked Questions to learn more about how our platform works.',
'motivation_title' => 'Ready to transform your logistics?',
'motivation_subtitle' => 'Join our platform today to find reliable trucks or secure the best shipments in the market.',
'company' => 'Company',
'about_us' => 'About Us',
'careers' => 'Careers',
'contact' => 'Contact',
'resources' => 'Resources',
'help_center' => 'Help Center / FAQ',
'terms_of_service' => 'Terms of Service',
'privacy_policy' => 'Privacy Policy',
'language' => 'Language',
'all_rights_reserved' => 'All rights reserved.',
'dashboard' => 'Dashboard',
'settings' => 'Settings',
'company_setting' => 'Company Setting',
'integrations' => 'Integrations',
'locations' => 'Locations',
'countries' => 'Countries',
'cities' => 'Cities',
'users' => 'Users',
'shippers' => 'Shippers',
'truck_owners' => 'Truck Owners',
'user_registration' => 'User Registration',
'pages' => 'Pages',
'faqs' => 'FAQs',
'landing_pages' => 'Landing Pages',
'login_title' => 'Welcome Back',
'login_subtitle' => 'Sign in to your account to continue',
'email_address' => 'Email address',
'email_placeholder' => 'name@example.com',
'password' => 'Password',
'forgot_password' => 'Forgot password?',
'password_placeholder' => 'Enter your password',
'change_password' => 'Change Password',
'new_password' => 'New Password',
'confirm_password' => 'Confirm Password',
'passwords_do_not_match' => 'Passwords do not match.',
'password_too_short' => 'Password must be at least 6 characters.',
'password_updated' => 'Password updated successfully.',
'sign_in' => 'Sign In',
'dont_have_account' => 'Don\'t have an account?',
'register_now' => 'Register now',
'reset_password_title' => 'Reset Password',
'reset_password_subtitle' => 'Enter your email and we\'ll send you a link to reset your password',
'send_reset_link' => 'Send Reset Link',
'back_to_login' => 'Back to login',
'invalid_image' => 'Invalid image format. Please upload JPG, PNG, GIF, or WEBP.',
'upload_failed' => 'Failed to save uploaded file.',
'profile_updated' => 'Profile updated successfully.',
'my_profile' => 'My Profile',
'profile_picture' => 'Profile Picture',
'change_picture' => 'Change Picture',
'picture_hint' => 'JPG, PNG, or GIF up to 5MB',
'full_name' => 'Full Name',
'email_hint' => 'Email address cannot be changed.',
'account_role' => 'Account Role',
'save_changes' => 'Save Changes',
'reg_title' => 'Create your logistics account',
'reg_subtitle' => 'Shippers and truck owners can self-register with full profile details.',
'reg_success_pending' => 'Registration completed successfully. Your account is pending admin approval.',
'reg_success' => 'Registration completed successfully.',
'role' => 'Role',
'shipper' => 'Shipper',
'truck_owner' => 'Truck Owner',
'email' => 'Email',
'phone' => 'Phone',
'country' => 'Country',
'select_country' => 'Select country',
'city' => 'City',
'select_city' => 'Select city',
'address' => 'Address',
'shipper_details' => 'Shipper details',
'company_name' => 'Company name',
'truck_details' => 'Truck owner details',
'truck_type' => 'Truck type',
'load_capacity' => 'Load capacity (tons)',
'plate_no' => 'Plate number',
'bank_account' => 'Bank Account / IBAN',
'bank_name' => 'Bank Name',
'bank_branch' => 'Bank Branch',
'id_card_front' => 'ID card (Front Face)',
'id_card_back' => 'ID card (Back Face)',
'truck_reg_front' => 'Truck Registration (Front Face)',
'truck_reg_back' => 'Truck Registration (Back Face)',
'truck_picture' => 'Clear Truck Photo (showing plate number)',
'create_account' => 'Create account',
'back_to_admin' => 'Back to admin',
'welcome_back' => 'Welcome to your dashboard. Manage your cargo shipments here.',
'total_shipments_posted' => 'Total Shipments',
'active_shipments' => 'Active Shipments',
'delivered_shipments' => 'Delivered Shipments',
'route_label' => 'Route',
'total_label' => 'total',
'welcome_back_owner' => 'Find loads and submit your best rate.',
'total_offers' => 'Total Offers',
'won_shipments' => 'Won Shipments',
),
"ar" => array (
'app_name' => 'CargoLink',
'nav_home' => 'نظرة عامة',
'nav_shipper' => 'لوحة الشاحن',
'nav_owner' => 'لوحة مالك الشاحنة',
'nav_admin' => 'لوحة الإدارة',
'hero_title' => 'انقل شحنتك بسرعة مع شاحنات موثوقة.',
'hero_subtitle' => 'أنشئ شحنة، استلم عروضاً، وادفع عبر ثواني أو التحويل البنكي.',
'hero_tagline' => 'منصة لوجستية متعددة اللغات',
'register_shipper' => 'التسجيل كشاحن',
'register_owner' => 'التسجيل كمالك شاحنة',
'cta_shipper' => 'إنشاء شحنة',
'cta_owner' => 'البحث عن الشحنات',
'cta_admin' => 'الدخول للإدارة',
'stats_shipments' => 'الشحنات المنشورة',
'stats_offers' => 'العروض الحالية',
'stats_confirmed' => 'الرحلات المؤكدة',
'section_workflow' => 'طريقة العمل',
'recent_shipments' => 'أحدث الشحنات',
'step_post' => 'يقوم الشاحن بإدخال تفاصيل الشحنة وطريقة الدفع.',
'step_offer' => 'يرسل أصحاب الشاحنات أفضل عروضهم.',
'step_confirm' => 'تؤكد الإدارة الحجز وتحدث الحالة.',
'step_confirm_desc' => 'حجز آمن وتتبع التسليم حتى الانتهاء.',
'shipper_dashboard' => 'لوحة الشاحن',
'new_shipment' => 'إنشاء شحنة',
'shipper_name' => 'اسم الشاحن',
'shipper_company' => 'الشركة',
'origin' => 'مدينة الانطلاق',
'destination' => 'مدينة الوصول',
'cargo' => 'وصف الحمولة',
'cargo_placeholder' => 'مثال: 20 منصة إلكترونيات',
'weight' => 'الوزن (طن)',
'pickup_date' => 'تاريخ الاستلام',
'delivery_date' => 'تاريخ التسليم',
'payment_method' => 'طريقة الدفع',
'payment_thawani' => 'الدفع الإلكتروني عبر ثواني',
'payment_bank' => 'تحويل بنكي',
'submit_shipment' => 'إرسال الشحنة',
'shipments_list' => 'أحدث الشحنات',
'status' => 'الحالة',
'offer' => 'أفضل عرض',
'actions' => 'إجراءات',
'view' => 'عرض',
'owner_dashboard' => 'لوحة مالك الشاحنة',
'available_shipments' => 'الشحنات المتاحة',
'offer_price' => 'سعر العرض',
'offer_owner' => 'اسم مالك الشاحنة',
'submit_offer' => 'إرسال العرض',
'admin_dashboard' => 'لوحة الإدارة',
'update_status' => 'تحديث الحالة',
'save' => 'حفظ',
'shipment_detail' => 'تفاصيل الشحنة',
'created_at' => 'تم الإنشاء',
'best_offer' => 'أفضل عرض',
'assign_owner' => 'المالك المعتمد',
'no_shipments' => 'لا توجد شحنات بعد. ابدأ بإنشاء أول شحنة.',
'no_offers' => 'لا توجد عروض بعد.',
'success_shipment' => 'تم نشر الشحنة بنجاح.',
'success_offer' => 'تم إرسال العرض إلى الشاحن.',
'success_status' => 'تم تحديث الحالة.',
'error_required' => 'يرجى تعبئة جميع الحقول المطلوبة.',
'error_invalid' => 'يرجى إدخال قيم صحيحة.',
'status_posted' => 'منشورة',
'status_offered' => 'بعرض',
'status_confirmed' => 'مؤكدة',
'status_in_transit' => 'قيد النقل',
'status_delivered' => 'تم التسليم',
'footer_note' => 'هذه هي النسخة الأولية. الدفع غير متصل بعد.',
'marketing_title_1' => 'للشاحنين',
'marketing_desc_1' => 'ابحث عن الشاحنة المناسبة لحمولتك بسرعة وأمان.',
'marketing_title_2' => 'لأصحاب الشاحنات',
'marketing_desc_2' => 'عظّم أرباحك وتجنب العودة فارغاً.',
'motivation_phrase' => 'تمكين الخدمات اللوجستية للمستقبل.',
'why_choose_us' => 'لماذا تختار كارجو لينك؟',
'feature_1_title' => 'مطابقة سريعة',
'feature_1_desc' => 'تواصل مع الشاحنات المتاحة في دقائق.',
'feature_2_title' => 'مدفوعات آمنة',
'feature_2_desc' => 'معاملاتك محمية بأعلى معايير الأمان.',
'feature_3_title' => 'مستخدمون موثوقون',
'feature_3_desc' => 'نقوم بالتحقق من جميع أصحاب الشاحنات لضمان راحتك.',
'view_faq' => 'عرض الأسئلة الشائعة',
'faq_title' => 'لديك أسئلة؟',
'faq_subtitle' => 'اطلع على الأسئلة الشائعة لمعرفة المزيد حول كيفية عمل منصتنا.',
'motivation_title' => 'هل أنت مستعد لتحويل خدماتك اللوجستية؟',
'motivation_subtitle' => 'انضم إلى منصتنا اليوم للعثور على شاحنات موثوقة أو تأمين أفضل الشحنات في السوق.',
'company' => 'الشركة',
'about_us' => 'معلومات عنا',
'careers' => 'الوظائف',
'contact' => 'اتصل بنا',
'resources' => 'الموارد',
'help_center' => 'مركز المساعدة / الأسئلة الشائعة',
'terms_of_service' => 'شروط الخدمة',
'privacy_policy' => 'سياسة الخصوصية',
'language' => 'اللغة',
'all_rights_reserved' => 'جميع الحقوق محفوظة.',
'dashboard' => 'لوحة القيادة',
'settings' => 'الإعدادات',
'company_setting' => 'إعدادات الشركة',
'integrations' => 'التكاملات',
'locations' => 'المواقع',
'countries' => 'البلدان',
'cities' => 'المدن',
'users' => 'المستخدمون',
'shippers' => 'الشاحنون',
'truck_owners' => 'أصحاب الشاحنات',
'user_registration' => 'تسجيل المستخدم',
'pages' => 'الصفحات',
'faqs' => 'الأسئلة الشائعة',
'landing_pages' => 'صفحات الهبوط',
'login_title' => 'مرحبًا بعودتك',
'login_subtitle' => 'قم بتسجيل الدخول إلى حسابك للمتابعة',
'email_address' => 'البريد الإلكتروني',
'email_placeholder' => 'name@example.com',
'password' => 'كلمة المرور',
'forgot_password' => 'هل نسيت كلمة المرور؟',
'password_placeholder' => 'أدخل كلمة المرور',
'change_password' => 'تغيير كلمة المرور',
'new_password' => 'كلمة المرور الجديدة',
'confirm_password' => 'تأكيد كلمة المرور',
'passwords_do_not_match' => 'كلمات المرور غير متطابقة.',
'password_too_short' => 'يجب أن تتكون كلمة المرور من 6 أحرف على الأقل.',
'password_updated' => 'تم تحديث كلمة المرور بنجاح.',
'sign_in' => 'تسجيل الدخول',
'dont_have_account' => 'ليس لديك حساب؟',
'register_now' => 'سجل الآن',
'reset_password_title' => 'إعادة تعيين كلمة المرور',
'reset_password_subtitle' => 'أدخل بريدك الإلكتروني وسنرسل لك رابطًا لإعادة تعيين كلمة المرور',
'send_reset_link' => 'إرسال رابط إعادة التعيين',
'back_to_login' => 'العودة لتسجيل الدخول',
'invalid_image' => 'صيغة صورة غير صالحة. يرجى تحميل JPG أو PNG أو GIF أو WEBP.',
'upload_failed' => 'فشل في حفظ الملف المحمل.',
'profile_updated' => 'تم تحديث الملف الشخصي بنجاح.',
'my_profile' => 'ملفي الشخصي',
'profile_picture' => 'صورة الملف الشخصي',
'change_picture' => 'تغيير الصورة',
'picture_hint' => 'JPG، PNG، أو GIF حتى 5 ميغابايت',
'full_name' => 'الاسم الكامل',
'email_hint' => 'لا يمكن تغيير عنوان البريد الإلكتروني.',
'account_role' => 'دور الحساب',
'save_changes' => 'حفظ التغييرات',
'reg_title' => 'أنشئ حسابك اللوجستي',
'reg_subtitle' => 'يمكن للشاحنين وأصحاب الشاحنات التسجيل الذاتي ببيانات الملف الشخصي الكاملة.',
'reg_success_pending' => 'اكتمل التسجيل بنجاح. حسابك في انتظار موافقة الإدارة.',
'reg_success' => 'اكتمل التسجيل بنجاح.',
'role' => 'الدور',
'shipper' => 'شاحن',
'truck_owner' => 'مالك شاحنة',
'email' => 'البريد الإلكتروني',
'phone' => 'الهاتف',
'country' => 'البلد',
'select_country' => 'اختر البلد',
'city' => 'المدينة',
'select_city' => 'اختر المدينة',
'address' => 'العنوان',
'shipper_details' => 'تفاصيل الشاحن',
'company_name' => 'اسم الشركة',
'truck_details' => 'تفاصيل مالك الشاحنة',
'truck_type' => 'نوع الشاحنة',
'load_capacity' => 'سعة الحمولة (طن)',
'plate_no' => 'رقم اللوحة',
'bank_account' => 'الحساب البنكي / الآيبان',
'bank_name' => 'اسم البنك',
'bank_branch' => 'فرع البنك',
'id_card_front' => 'البطاقة الشخصية (الوجه الأمامي)',
'id_card_back' => 'البطاقة الشخصية (الوجه الخلفي)',
'truck_reg_front' => 'تسجيل الشاحنة (الوجه الأمامي)',
'truck_reg_back' => 'تسجيل الشاحنة (الوجه الخلفي)',
'truck_picture' => 'صورة واضحة للشاحنة (تظهر رقم اللوحة)',
'create_account' => 'إنشاء حساب',
'back_to_admin' => 'العودة للإدارة',
'welcome_back' => 'مرحبًا بك في لوحة القيادة الخاصة بك. قم بإدارة شحناتك هنا.',
'total_shipments_posted' => 'إجمالي الشحنات',
'active_shipments' => 'الشحنات النشطة',
'delivered_shipments' => 'الشحنات المسلمة',
'route_label' => 'المسار',
'total_label' => 'المجموع',
'welcome_back_owner' => 'ابحث عن الأحمال وقدم أفضل سعر لديك.',
'total_offers' => 'إجمالي العروض',
'won_shipments' => 'الشحنات الفائزة',
)
];
function t(string $key): string
@ -164,8 +388,7 @@ function e($value): string
function ensure_schema(): void
{
db()->exec("
CREATE TABLE IF NOT EXISTS landing_sections (
db()->exec("\n CREATE TABLE IF NOT EXISTS landing_sections (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
subtitle TEXT NULL,
@ -179,6 +402,11 @@ function ensure_schema(): void
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
try { db()->exec("ALTER TABLE landing_sections ADD COLUMN title_ar VARCHAR(255) NULL AFTER title"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE landing_sections ADD COLUMN subtitle_ar TEXT NULL AFTER subtitle"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE landing_sections ADD COLUMN content_ar TEXT NULL AFTER content"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE landing_sections ADD COLUMN button_text_ar VARCHAR(100) NULL AFTER button_text"); } catch (Exception $e) {}
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS shipments (
@ -199,8 +427,8 @@ CREATE TABLE IF NOT EXISTS shipments (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL;
db()->exec($sql);
try { db()->exec("ALTER TABLE users ADD COLUMN status ENUM('pending','active','rejected') NOT NULL DEFAULT 'active'"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE users ADD COLUMN profile_picture VARCHAR(255) NULL AFTER full_name"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE users ADD COLUMN status ENUM('pending','active','rejected') NOT NULL DEFAULT 'active'"); } catch (Exception $e) {}
try { db()->exec("ALTER TABLE users ADD COLUMN profile_picture VARCHAR(255) NULL AFTER full_name"); } catch (Exception $e) {}
// Payment fields for shipments
try { db()->exec("ALTER TABLE shipments ADD COLUMN platform_fee DECIMAL(10,2) DEFAULT 0.00 AFTER offer_price"); } catch (Exception $e) {}
@ -320,4 +548,4 @@ function get_setting(string $key, $default = ''): string
{
$settings = get_settings();
return $settings[$key] ?? $default;
}
}

View File

@ -1,7 +1,7 @@
<?php
require_once __DIR__ . '/app.php';
function render_header(string $title, string $active = ''): void
function render_header(string $title, string $active = '', bool $isFluid = false): void
{
global $lang, $dir;
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
@ -40,8 +40,8 @@ function render_header(string $title, string $active = ''): void
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
</head>
<body class="app-body">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom sticky-top shadow-sm py-3">
<div class="container">
<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">
@ -126,7 +126,7 @@ function render_header(string $title, string $active = ''): void
</div>
</div>
</nav>
<main class="container py-5">
<main class="<?= $isFluid ? 'container-fluid p-0' : 'container py-5' ?>">
<?php
}
@ -140,7 +140,7 @@ function render_footer(): void
$companyAddress = get_setting('company_address', '');
?>
</main>
<footer class="bg-white border-top py-5 mt-5">
<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">
@ -212,14 +212,14 @@ function render_footer(): void
function render_admin_sidebar(string $active = 'dashboard'): void
{
$settingsActive = in_array($active, ['company_profile', 'integrations']);
$settingsActive = in_array($active, ['company_profile', 'integrations', 'notification_templates']);
$locationsActive = in_array($active, ['countries', 'cities']);
$usersActive = in_array($active, ['shippers', 'truck_owners', 'register']);
$pagesActive = in_array($active, ['faqs', 'landing_pages']);
?>
<aside class="admin-sidebar panel p-4 shadow-sm border-0">
<h2 class="h5 fw-bold mb-4"><i class="bi bi-shield-lock me-2 text-primary"></i><?= e(t('nav_admin')) ?></h2>
<nav class="nav flex-column gap-2">
<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 === 'shipments' ? 'active' : '' ?>" href="<?= e(url_with_lang('admin_shipments.php')) ?>">
<i class="bi bi-box2-fill me-2"></i><?= e(t('shipments') ?: 'Shipments') ?>
</a>
@ -239,6 +239,9 @@ function render_admin_sidebar(string $active = 'dashboard'): void
<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') ?: 'Notifications') ?>
</a>
</div>
</div>
@ -291,6 +294,12 @@ function render_admin_sidebar(string $active = 'dashboard'): void
</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
}

View File

@ -34,6 +34,13 @@ 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'):
?>
<div class="hero-section mb-5">
@ -44,10 +51,10 @@ try {
<?= e(t('hero_tagline')) ?>
</span>
<h1 class="display-5 fw-bold mb-4" style="line-height: 1.2;">
<?= e($sec['title'] ?: t('hero_title')) ?>
<?= e($title ?: t('hero_title')) ?>
</h1>
<p class="fs-5 text-muted mb-5">
<?= e($sec['subtitle'] ?: t('hero_subtitle')) ?>
<?= e($subtitle ?: t('hero_subtitle')) ?>
</p>
<div class="d-flex flex-column flex-sm-row gap-3">
<a class="btn btn-primary btn-lg shadow-sm" href="<?= e(url_with_lang('register.php', ['role' => 'shipper'])) ?>">
@ -72,8 +79,8 @@ try {
</div>
<?php elseif ($sec['section_type'] === 'stats'): ?>
<div class="row mb-5 g-4">
<?php if ($sec['title']): ?>
<div class="col-12 text-center mb-3"><h2 class="display-6 fw-bold"><?= e($sec['title']) ?></h2></div>
<?php if ($title): ?>
<div class="col-12 text-center mb-3"><h2 class="display-6 fw-bold"><?= e($title) ?></h2></div>
<?php endif; ?>
<div class="col-md-4">
<div class="stat-card">
@ -97,8 +104,8 @@ try {
<?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($sec['title'] ?: t('why_choose_us')) ?></h2>
<p class="text-muted fs-5 mx-auto" style="max-width: 600px;"><?= e($sec['subtitle'] ?: t('motivation_phrase')) ?></p>
<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">
@ -139,7 +146,7 @@ try {
</div>
<div class="col-md-6 order-md-1">
<div class="p-lg-4">
<h2 class="display-6 fw-bold mb-4"><?= e($sec['title'] ?: t('section_workflow')) ?></h2>
<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">
@ -178,7 +185,7 @@ try {
<?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($sec['title'] ?: t('recent_shipments')) ?></h2>
<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>
@ -219,8 +226,8 @@ try {
<?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($sec['title'] ?: t('faq_title')) ?></h2>
<p class="text-muted fs-5 mb-4 mx-auto" style="max-width: 600px;"><?= e($sec['subtitle'] ?: t('faq_subtitle')) ?></p>
<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>
@ -228,8 +235,8 @@ try {
</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($sec['title'] ?: t('motivation_title')) ?></h3>
<p class="fs-5 text-white-50 mb-4 mx-auto" style="max-width: 600px;"><?= e($sec['subtitle'] ?: t('motivation_subtitle')) ?></p>
<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>
@ -242,15 +249,15 @@ try {
<?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($sec['title']) ?></h2>
<?php if ($sec['subtitle']): ?>
<p class="text-muted fs-5 mb-4 mx-auto" style="max-width: 600px;"><?= e($sec['subtitle']) ?></p>
<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 ($sec['content']): ?>
<div class="mb-4 text-start mx-auto" style="max-width: 800px;"><?= $sec['content'] ?></div>
<?php if ($content): ?>
<div class="mb-4 text-start mx-auto" style="max-width: 800px;"><?= $content ?></div>
<?php endif; ?>
<?php if ($sec['button_text']): ?>
<a href="<?= e(url_with_lang($sec['button_link'] ?: '#')) ?>" class="btn btn-primary btn-lg px-4 rounded-pill shadow-sm"><?= e($sec['button_text']) ?></a>
<?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: ?>
@ -262,15 +269,15 @@ try {
</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($sec['title']) ?></h2>
<?php if ($sec['subtitle']): ?>
<h5 class="fw-bold mb-3 text-muted"><?= e($sec['subtitle']) ?></h5>
<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 ($sec['content']): ?>
<div class="mb-4"><?= $sec['content'] ?></div>
<?php if ($content): ?>
<div class="mb-4"><?= $content ?></div>
<?php endif; ?>
<?php if ($sec['button_text']): ?>
<a href="<?= e(url_with_lang($sec['button_link'] ?: '#')) ?>" class="btn btn-primary btn-lg px-4 rounded-pill shadow-sm"><?= e($sec['button_text']) ?></a>
<?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>

View File

@ -117,6 +117,12 @@ $translations_en = [
'password' => 'Password',
'forgot_password' => 'Forgot password?',
'password_placeholder' => 'Enter your password',
'change_password' => 'Change Password',
'new_password' => 'New Password',
'confirm_password' => 'Confirm Password',
'passwords_do_not_match' => 'Passwords do not match.',
'password_too_short' => 'Password must be at least 6 characters.',
'password_updated' => 'Password updated successfully.',
'sign_in' => 'Sign In',
'dont_have_account' => 'Don\'t have an account?',
'register_now' => 'Register now',
@ -292,6 +298,12 @@ $translations_ar = [
'password' => 'كلمة المرور',
'forgot_password' => 'هل نسيت كلمة المرور؟',
'password_placeholder' => 'أدخل كلمة المرور',
'change_password' => 'تغيير كلمة المرور',
'new_password' => 'كلمة المرور الجديدة',
'confirm_password' => 'تأكيد كلمة المرور',
'passwords_do_not_match' => 'كلمات المرور غير متطابقة.',
'password_too_short' => 'يجب أن تتكون كلمة المرور من 6 أحرف على الأقل.',
'password_updated' => 'تم تحديث كلمة المرور بنجاح.',
'sign_in' => 'تسجيل الدخول',
'dont_have_account' => 'ليس لديك حساب؟',
'register_now' => 'سجل الآن',

View File

@ -58,15 +58,43 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'updat
}
}
// 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 {
$updateStmt = db()->prepare("UPDATE users SET full_name = :name, profile_picture = :pic WHERE id = :id");
$updateStmt->execute([
$sql = "UPDATE users SET full_name = :name, profile_picture = :pic";
$params = [
':name' => $fullName,
':pic' => $profilePicPath,
':id' => $userId
]);
set_flash('success', t('profile_updated'));
];
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) {
@ -140,6 +168,20 @@ $flash = get_flash();
<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>
@ -173,4 +215,4 @@ function previewImage(input) {
}
</script>
<?php render_footer(); ?>
<?php render_footer(); ?>

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema();
@ -37,15 +38,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} 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'
"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;
@ -70,10 +87,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':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;
}
}
}
@ -209,15 +262,23 @@ render_header(t('shipment_detail'));
</div>
</div>
<form method="post">
<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>
<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>
</form>
<div class="d-grid gap-2">
<form method="post">
<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?');">
<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>

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema();
@ -40,11 +41,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'creat
}
if (!$errors) {
$shipperId = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
$stmt = db()->prepare(
"INSERT INTO shipments (shipper_name, shipper_company, origin_city, destination_city, cargo_description, weight_tons, pickup_date, delivery_date, payment_method)
VALUES (:shipper_name, :shipper_company, :origin_city, :destination_city, :cargo_description, :weight_tons, :pickup_date, :delivery_date, :payment_method)"
"INSERT INTO shipments (shipper_id, shipper_name, shipper_company, origin_city, destination_city, cargo_description, weight_tons, pickup_date, delivery_date, payment_method)
VALUES (:shipper_id, :shipper_name, :shipper_company, :origin_city, :destination_city, :cargo_description, :weight_tons, :pickup_date, :delivery_date, :payment_method)"
);
$stmt->execute([
':shipper_id' => $shipperId,
':shipper_name' => $shipperName,
':shipper_company' => $shipperCompany,
':origin_city' => $origin,
@ -55,8 +59,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'creat
':delivery_date' => $deliveryDate,
':payment_method' => $payment,
]);
$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
]);
}
}
set_flash('success', t('success_shipment'));
header('Location: ' . url_with_lang('shipper_dashboard.php'));
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B