update dashboard
This commit is contained in:
parent
414499c9c9
commit
56f2aad3d0
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(); ?>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')) ?>">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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')) ?>">
|
||||
|
||||
12
db/migrations/add_pod_file_to_shipments.php
Normal file
12
db/migrations/add_pod_file_to_shipments.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->exec("ALTER TABLE shipments ADD COLUMN pod_file VARCHAR(255) DEFAULT NULL;");
|
||||
echo "Added pod_file column to shipments table.\n";
|
||||
} catch (PDOException $e) {
|
||||
// Column might already exist
|
||||
echo "Error (or column exists): " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user