update dashboard

This commit is contained in:
Flatlogic Bot 2026-03-14 13:13:28 +00:00
parent 414499c9c9
commit 56f2aad3d0
27 changed files with 410 additions and 103 deletions

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS cities (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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'] ?? '');
@ -129,7 +129,7 @@ render_header('Manage Cities', 'admin', true);
<div class="panel p-4">
<h2 class="h5 mb-3">Add city</h2>
<form method="post" class="row g-3">
<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>
@ -158,7 +158,7 @@ render_header('Manage Cities', 'admin', true);
<h2 class="h5 mb-2">Cities list</h2>
<?php if ($editingCity): ?>
<form method="post" class="row g-2 align-items-end mb-3">
<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>
@ -210,7 +210,7 @@ render_header('Manage Cities', 'admin', true);
<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?');">
<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>

View File

@ -1,14 +1,14 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
ensure_schema();
$errors = [];
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$companyName = trim($_POST['company_name'] ?? '');
$companyEmail = trim($_POST['company_email'] ?? '');
$companyPhone = trim($_POST['company_phone'] ?? '');
@ -111,7 +111,7 @@ render_header('Company Profile', 'admin', true);
<?php endif; ?>
<div class="panel p-4">
<form method="post" enctype="multipart/form-data">
<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">

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS cities (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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'] ?? '');
@ -107,7 +107,7 @@ render_header('Manage Countries', 'admin', true);
<div class="panel p-4">
<h2 class="h5 mb-3">Add country</h2>
<form method="post" class="row g-3">
<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>
@ -127,7 +127,7 @@ render_header('Manage Countries', 'admin', true);
<h2 class="h5 mb-2">Countries list</h2>
<?php if ($editingCountry): ?>
<form method="post" class="row g-2 align-items-end mb-3">
<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>
@ -167,7 +167,7 @@ render_header('Manage Countries', 'admin', true);
<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?');">
<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>

View File

@ -1,17 +1,15 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
ensure_schema();
$errors = [];
// Removed inline status update logic as per UI cleanup
$shipments = [];
try {
$stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 30");
$stmt = db()->query("SELECT * FROM shipments ORDER BY created_at DESC LIMIT 10");
$shipments = $stmt->fetchAll();
} catch (Throwable $e) {
$shipments = [];
@ -22,14 +20,48 @@ $stats = [
'active_shipments' => 0,
'total_shippers' => 0,
'total_truck_owners' => 0,
'total_revenue' => 0.0,
];
$chartData = [
'labels' => [],
'data' => []
];
try {
$stats['total_shipments'] = (int)db()->query("SELECT COUNT(*) FROM shipments")->fetchColumn();
$stats['active_shipments'] = (int)db()->query("SELECT COUNT(*) FROM shipments WHERE status != 'delivered'")->fetchColumn();
$stats['total_shippers'] = (int)db()->query("SELECT COUNT(*) FROM users WHERE role = 'shipper'")->fetchColumn();
$stats['total_truck_owners'] = (int)db()->query("SELECT COUNT(*) FROM users WHERE role = 'truck_owner'")->fetchColumn();
} catch (Throwable $e) {}
$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();
@ -48,7 +80,7 @@ render_header(t('admin_dashboard'), 'admin', true);
<!-- Stats Row -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<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>
@ -56,7 +88,7 @@ render_header(t('admin_dashboard'), 'admin', true);
<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-3">
<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>
@ -64,7 +96,7 @@ render_header(t('admin_dashboard'), 'admin', true);
<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-3">
<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>
@ -72,7 +104,7 @@ render_header(t('admin_dashboard'), 'admin', true);
<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-3">
<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>
@ -80,22 +112,40 @@ render_header(t('admin_dashboard'), 'admin', true);
<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-0">
<!-- Main Content: Shipments -->
<div class="row g-4">
<!-- Main Content: Shipments Chart -->
<div class="col-lg-8">
<div class="panel shadow-sm border-0 h-100 rounded-4 d-flex flex-column">
<!-- 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>
<span class="badge bg-white text-dark"><?= e(count($shipments)) ?> <?= e(t('shown')) ?></span>
<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-4 flex-grow-1 d-flex flex-column">
<div class="p-0 flex-grow-1 d-flex flex-column">
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash['message']) ?></div>
<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"><?= e(implode(' ', $errors)) ?></div>
<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">
@ -104,32 +154,32 @@ render_header(t('admin_dashboard'), 'admin', true);
</div>
<?php else: ?>
<div class="table-responsive flex-grow-1">
<table class="table align-middle mb-0">
<thead>
<table class="table align-middle mb-0 table-hover">
<thead class="bg-light">
<tr>
<th class="text-uppercase small text-muted border-top-0"><?= e(t('shipment')) ?></th>
<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="text-uppercase small text-muted border-top-0 text-end"><?= e(t('action')) ?></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>
<div class="fw-bold"><?= e($row['shipper_company']) ?></div>
<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"><?= e($row['origin_city']) ?></span>
<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"><?= e($row['destination_city']) ?></span>
<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">
<a href="<?= e(url_with_lang('shipment_detail.php', ['id' => $row['id']])) ?>" class="btn btn-sm p-1 border-0 bg-transparent text-primary" title="<?= e(t('view_details')) ?>">
<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>
@ -183,11 +233,11 @@ render_header(t('admin_dashboard'), 'admin', true);
</div>
<i class="bi bi-chevron-right ms-auto text-muted small"></i>
</a>
<a href="<?= e(url_with_lang('admin_landing_pages.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-info d-flex align-items-center justify-content-center" style="width: 48px; height: 48px;"><i class="bi bi-layout-text-window-reverse fs-5"></i></div>
<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('landing_pages')) ?></h6>
<small class="text-muted d-block line-height-sm"><?= e(t('edit_homepage')) ?></small>
<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>
@ -199,4 +249,79 @@ render_header(t('admin_dashboard'), 'admin', true);
</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(); ?>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS faqs (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['add_faq'])) {
$questionEn = trim($_POST['question_en'] ?? '');
$answerEn = trim($_POST['answer_en'] ?? '');
@ -133,7 +133,7 @@ render_header('Manage FAQs', 'admin', true);
<h5 class="mb-0">Edit FAQ</h5>
</div>
<div class="card-body">
<form method="post" class="row g-3">
<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">
@ -172,7 +172,7 @@ render_header('Manage FAQs', 'admin', true);
<h5 class="mb-0">Add New FAQ</h5>
</div>
<div class="card-body">
<form method="post" class="row g-3">
<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>
@ -235,7 +235,7 @@ render_header('Manage FAQs', 'admin', true);
<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?');">
<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>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema();
@ -11,7 +11,7 @@ $success = '';
$testSuccess = '';
$testError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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'] ?? '');
@ -106,7 +106,7 @@ render_header('Integrations', 'admin', true);
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
<?php endif; ?>
<form method="post" id="settingsForm">
<form method="post" id="settingsForm"> <?= csrf_field() ?>
<ul class="nav nav-tabs mb-4" id="integrationsTab" role="tablist">
<li class="nav-item" role="presentation">
@ -285,7 +285,7 @@ render_header('Integrations', 'admin', true);
</form>
<!-- Hidden Test Form -->
<form method="post" id="testForm" style="display:none;">
<form method="post" id="testForm" style="display:none;"> <?= csrf_field() ?>
<input type="hidden" name="action" value="test_whatsapp">
</form>
</div>

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
require_once __DIR__ . '/includes/app.php';
require_once __DIR__ . '/includes/layout.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'));
@ -12,7 +12,7 @@ if (empty($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
$pdo = db();
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$action = $_POST['action'] ?? '';
if ($action === 'create' || $action === 'edit') {

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/app.php';
require_once __DIR__ . '/includes/app.php'; require_role('admin');
header('Location: ' . url_with_lang('admin_countries.php'), true, 302);
exit;

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
ensure_schema();
@ -47,7 +47,7 @@ if ($action === 'edit' && $id > 0) {
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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'] ?? '');
@ -98,7 +98,7 @@ if ($action === 'edit' && $id > 0) {
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="post">
<form method="post"> <?= csrf_field() ?>
<div class="row g-4">
<div class="col-md-6">
<h5 class="mb-3 border-bottom pb-2">English</h5>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
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') {
@ -23,7 +23,7 @@ $message = '';
$error = '';
// Handle Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$action = $_POST['action'] ?? '';
if ($action === 'create' || $action === 'edit') {
@ -194,7 +194,7 @@ render_header(t('nav_platform_users'), 'platform_users', true);
<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">
<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">
@ -239,7 +239,7 @@ render_header(t('nav_platform_users'), 'platform_users', true);
</div>
<!-- Delete Form -->
<form method="post" id="deleteForm">
<form method="post" id="deleteForm"> <?= csrf_field() ?>
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" id="deleteId">
</form>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
// Check permission
if (!has_permission('view_reports') && !has_permission('manage_shippers')) {

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
// Helper to validate date
function validate_date($date, $format = 'Y-m-d') {
@ -13,6 +13,7 @@ function validate_date($date, $format = 'Y-m-d') {
$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');
@ -95,7 +96,7 @@ try {
$error = "Database error: " . $e->getMessage();
}
// Calculate totals for footer
// Calculate totals
$totalShipments = 0;
$grandTotalAmount = 0.0;
$grandTotalProfit = 0.0;
@ -106,6 +107,33 @@ foreach ($results as $row) {
$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);
?>
@ -162,6 +190,9 @@ render_header(t('summary_report'), 'reports_summary', true);
<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>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
// Check permission
if (!has_permission('view_reports') && !has_permission('manage_truck_owners')) {

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$id = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
@ -63,7 +63,7 @@ if ($destination_country_id) {
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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
@ -172,7 +172,7 @@ if (!$isAjax):
<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">
<form method="post" action="admin_shipment_edit.php?id=<?= $id ?> <?= csrf_field() ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
<?php if ($isAjax): ?>
<div class="modal-header">
<h5 class="modal-title"><?= t('edit_shipment_title') ?><?= e((string)$shipment['id']) ?></h5>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -174,7 +174,7 @@ render_header(t('manage_shipments'), 'admin', true);
title="<?= t('edit_shipment_tooltip') ?>">
<i class="bi bi-pencil"></i>
</a>
<form method="post" class="d-inline m-0 p-0">
<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>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$userId = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
@ -41,7 +41,7 @@ if (!$shipper) {
$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 ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? '');
@ -151,7 +151,7 @@ if (!$isAjax):
<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">
<form method="post" action="admin_shipper_edit.php?id=<?= $userId ?> <?= csrf_field() ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
<?php if ($isAjax): ?>
<div class="modal-header">
<h5 class="modal-title">Edit Shipper: <?= e($shipper['full_name']) ?></h5>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -174,7 +174,7 @@ render_header(t('manage_shippers'), 'admin', true);
title="<?= e(t('edit_shipper')) ?>">
<i class="bi bi-pencil"></i>
</a>
<form method="post" class="d-inline m-0 p-0">
<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')) ?>">

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$userId = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1';
@ -43,7 +43,7 @@ if (!$owner) {
$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 ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? '');
@ -178,7 +178,7 @@ if (!$isAjax):
<?php endif; // End non-ajax wrapper ?>
<!-- The Form (Shared) -->
<form method="post" action="admin_truck_owner_edit.php?id=<?= $userId ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
<form method="post" action="admin_truck_owner_edit.php?id=<?= $userId ?> <?= csrf_field() ?><?= $isAjax ? '&ajax=1' : '' ?>" class="ajax-form">
<?php if ($isAjax): ?>
<div class="modal-header">
<h5 class="modal-title">Edit Truck Owner: <?= e($owner['full_name']) ?></h5>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
@ -182,7 +182,7 @@ render_header(t('manage_truck_owners'), 'admin', true);
title="<?= e(t('edit_owner')) ?>">
<i class="bi bi-pencil"></i>
</a>
<form method="post" class="d-inline m-0 p-0">
<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')) ?>">

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

View File

@ -295,6 +295,9 @@ $translations = [
'create_account' => 'Create Account',
'back_to_admin' => 'Back to Admin',
'back_to_home' => 'Back to Home',
'total_revenue' => 'Total Revenue',
'shipments_analytics' => 'Shipments Analytics',
'export_csv' => 'Export CSV',
),
"ar" => array (
'app_name' => 'CargoLink',
@ -578,6 +581,9 @@ $translations = [
'create_account' => 'إنشاء حساب',
'back_to_admin' => 'العودة للإدارة',
'back_to_home' => 'العودة للرئيسية',
'total_revenue' => 'إجمالي الإيرادات',
'shipments_analytics' => 'تحليلات الشحنات',
'export_csv' => 'تصدير CSV',
)
];
@ -593,6 +599,7 @@ function e($value): string
}
function ensure_schema(): void
try { db()->exec("ALTER TABLE shipments ADD COLUMN pod_path VARCHAR(255) NULL AFTER offer_owner"); } catch (Exception $e) {}
{
db()->exec("\n CREATE TABLE IF NOT EXISTS landing_sections (
id INT AUTO_INCREMENT PRIMARY KEY,
@ -792,6 +799,47 @@ function format_currency(float $amount): string
return number_format($amount, 3) . ' OMR';
}
function require_login(): void
{
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
}
function require_role(string $role): void
{
require_login();
if ($_SESSION['role'] !== $role && $_SESSION['role'] !== 'admin') {
http_response_code(403);
die("Access Denied. You must be a " . ucfirst($role) . " to view this page.");
}
}
function generate_csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
$token = generate_csrf_token();
return '<input type="hidden" name="csrf_token" value="' . $token . '">';
}
function validate_csrf_token(): void
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
http_response_code(403);
die("CSRF validation failed. Please refresh the page and try again.");
}
}
}
// Set timezone from settings
try {
$tz = get_setting('timezone', 'UTC');

View File

@ -14,7 +14,7 @@ if (isset($_SESSION['user_id'])) {
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['action']) && $_POST['action'] === 'login') {
$email = trim($_POST['email'] ?? '');
$password = (string)($_POST['password'] ?? '');
@ -117,7 +117,7 @@ render_header('Login / Reset Password', 'login', false, false);
<p class="text-muted"><?= e(t('login_subtitle')) ?></p>
</div>
<form method="post" action="">
<form method="post" action=""> <?= csrf_field() ?>
<input type="hidden" name="action" value="login">
<div class="mb-3">
@ -149,7 +149,7 @@ render_header('Login / Reset Password', 'login', false, false);
<p class="text-muted"><?= e(t('reset_password_subtitle')) ?></p>
</div>
<form method="post" action="">
<form method="post" action=""> <?= csrf_field() ?>
<input type="hidden" name="action" value="reset_password">
<div class="mb-4">

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_login();
ensure_schema();
@ -131,7 +131,7 @@ $flash = get_flash();
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data"> <?= csrf_field() ?>
<input type="hidden" name="action" value="update_profile">
<div class="text-center mb-4">

View File

@ -27,7 +27,7 @@ $values = [
$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 ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$role = $_POST['role'] ?? 'shipper';
$fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? '');
@ -263,7 +263,7 @@ render_header('Shipper & Truck Owner Registration');
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" id="regForm" novalidate>
<form method="post" enctype="multipart/form-data" id="regForm" novalidate> <?= csrf_field() ?>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label" for="role"><?= e(t('role')) ?></label>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_login();
require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema();
@ -18,19 +18,21 @@ if ($shipmentId > 0) {
$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;
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$action = $_POST['action'] ?? '';
if ($action === 'submit_offer') {
if ($action === 'submit_offer') { if (!$isTruckOwner && !$isAdmin) { $errors[] = 'Only truck owners can submit offers.'; }
$offerOwner = trim($_POST['offer_owner'] ?? '');
$offerPrice = trim($_POST['offer_price'] ?? '');
if ($offerOwner === '' || $offerPrice === '') {
@ -127,6 +129,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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.';
}
}
}
@ -263,13 +304,13 @@ render_header(t('shipment_detail'));
</div>
<div class="d-grid gap-2">
<form method="post">
<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?');">
<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
@ -291,10 +332,27 @@ render_header(t('shipment_detail'));
<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) -->
<!-- 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): ?>
@ -305,7 +363,7 @@ render_header(t('shipment_detail'));
<?php endif; ?>
<?php if ($shipment['status'] === 'posted' || $shipment['status'] === 'offered'): ?>
<form method="post">
<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>
@ -322,6 +380,39 @@ render_header(t('shipment_detail'));
This shipment is already confirmed/processed.
</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>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('shipper');
require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema();
@ -168,7 +168,7 @@ $flash = get_flash();
<?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?>
<form method="post">
<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>

View File

@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
require_once __DIR__ . '/includes/layout.php'; require_role('truck_owner');
ensure_schema();