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 <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = []; $errors = [];
$flash = null; $flash = null;
@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS cities (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"); ");
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['add_city'])) { if (isset($_POST['add_city'])) {
$countryId = (int)($_POST['country_id'] ?? 0); $countryId = (int)($_POST['country_id'] ?? 0);
$cityNameEn = trim($_POST['city_name_en'] ?? ''); $cityNameEn = trim($_POST['city_name_en'] ?? '');
@ -129,7 +129,7 @@ render_header('Manage Cities', 'admin', true);
<div class="panel p-4"> <div class="panel p-4">
<h2 class="h5 mb-3">Add city</h2> <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"> <div class="col-md-4">
<label class="form-label" for="country_id">Country</label> <label class="form-label" for="country_id">Country</label>
<select id="country_id" name="country_id" class="form-select" required> <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> <h2 class="h5 mb-2">Cities list</h2>
<?php if ($editingCity): ?> <?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']) ?>"> <input type="hidden" name="city_id" value="<?= e((string)$editingCity['id']) ?>">
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label mb-1">Country</label> <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']])) ?>"> <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> <i class="bi bi-pencil"></i>
</a> </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']) ?>"> <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"> <button type="submit" name="delete_city" class="btn btn-sm p-1 border-0 bg-transparent text-danger">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>

View File

@ -1,14 +1,14 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
ensure_schema(); ensure_schema();
$errors = []; $errors = [];
$success = ''; $success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$companyName = trim($_POST['company_name'] ?? ''); $companyName = trim($_POST['company_name'] ?? '');
$companyEmail = trim($_POST['company_email'] ?? ''); $companyEmail = trim($_POST['company_email'] ?? '');
$companyPhone = trim($_POST['company_phone'] ?? ''); $companyPhone = trim($_POST['company_phone'] ?? '');
@ -111,7 +111,7 @@ render_header('Company Profile', 'admin', true);
<?php endif; ?> <?php endif; ?>
<div class="panel p-4"> <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"> <ul class="nav nav-tabs mb-4" id="companySettingsTab" role="tablist">
<li class="nav-item" role="presentation"> <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"> <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 <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = []; $errors = [];
$flash = null; $flash = null;
@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS cities (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"); ");
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['add_country'])) { if (isset($_POST['add_country'])) {
$countryNameEn = trim($_POST['country_name_en'] ?? ''); $countryNameEn = trim($_POST['country_name_en'] ?? '');
$countryNameAr = trim($_POST['country_name_ar'] ?? ''); $countryNameAr = trim($_POST['country_name_ar'] ?? '');
@ -107,7 +107,7 @@ render_header('Manage Countries', 'admin', true);
<div class="panel p-4"> <div class="panel p-4">
<h2 class="h5 mb-3">Add country</h2> <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"> <div class="col-md-6">
<label class="form-label" for="country_name_en">Country name (EN)</label> <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> <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> <h2 class="h5 mb-2">Countries list</h2>
<?php if ($editingCountry): ?> <?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']) ?>"> <input type="hidden" name="country_id" value="<?= e((string)$editingCountry['id']) ?>">
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label mb-1">Country (EN)</label> <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']])) ?>"> <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> <i class="bi bi-pencil"></i>
</a> </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']) ?>"> <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"> <button type="submit" name="delete_country" class="btn btn-sm p-1 border-0 bg-transparent text-danger">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>

View File

@ -1,17 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
ensure_schema(); ensure_schema();
$errors = []; $errors = [];
// Removed inline status update logic as per UI cleanup
$shipments = []; $shipments = [];
try { 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(); $shipments = $stmt->fetchAll();
} catch (Throwable $e) { } catch (Throwable $e) {
$shipments = []; $shipments = [];
@ -22,14 +20,48 @@ $stats = [
'active_shipments' => 0, 'active_shipments' => 0,
'total_shippers' => 0, 'total_shippers' => 0,
'total_truck_owners' => 0, 'total_truck_owners' => 0,
'total_revenue' => 0.0,
];
$chartData = [
'labels' => [],
'data' => []
]; ];
try { try {
$stats['total_shipments'] = (int)db()->query("SELECT COUNT(*) FROM shipments")->fetchColumn(); $pdo = db();
$stats['active_shipments'] = (int)db()->query("SELECT COUNT(*) FROM shipments WHERE status != 'delivered'")->fetchColumn(); $stats['total_shipments'] = (int)$pdo->query("SELECT COUNT(*) FROM shipments")->fetchColumn();
$stats['total_shippers'] = (int)db()->query("SELECT COUNT(*) FROM users WHERE role = 'shipper'")->fetchColumn(); $stats['active_shipments'] = (int)$pdo->query("SELECT COUNT(*) FROM shipments WHERE status != 'delivered'")->fetchColumn();
$stats['total_truck_owners'] = (int)db()->query("SELECT COUNT(*) FROM users WHERE role = 'truck_owner'")->fetchColumn(); $stats['total_shippers'] = (int)$pdo->query("SELECT COUNT(*) FROM users WHERE role = 'shipper'")->fetchColumn();
} catch (Throwable $e) {} $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(); $flash = get_flash();
@ -48,7 +80,7 @@ render_header(t('admin_dashboard'), 'admin', true);
<!-- Stats Row --> <!-- Stats Row -->
<div class="row g-3 mb-4"> <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="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="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> <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> <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> </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="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="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> <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> <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> </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="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="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> <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> <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> </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="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="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> <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> <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> </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>
<div class="row g-0"> <div class="row g-4">
<!-- Main Content: Shipments --> <!-- Main Content: Shipments Chart -->
<div class="col-lg-8"> <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"> <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> <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>
<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): ?> <?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 endif; ?>
<?php if ($errors): ?> <?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 endif; ?>
<?php if (!$shipments): ?> <?php if (!$shipments): ?>
<div class="text-center p-5 text-muted flex-grow-1 d-flex flex-column justify-content-center"> <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> </div>
<?php else: ?> <?php else: ?>
<div class="table-responsive flex-grow-1"> <div class="table-responsive flex-grow-1">
<table class="table align-middle mb-0"> <table class="table align-middle mb-0 table-hover">
<thead> <thead class="bg-light">
<tr> <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('route')) ?></th>
<th class="text-uppercase small text-muted border-top-0"><?= e(t('status')) ?></th> <th class="text-uppercase small text-muted border-top-0"><?= e(t('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> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($shipments as $row): ?> <?php foreach ($shipments as $row): ?>
<tr> <tr>
<td> <td class="ps-4">
<div class="fw-bold"><?= e($row['shipper_company']) ?></div> <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> <small class="text-muted"><?= e($row['payment_method'] === 'bank_transfer' ? t('payment_bank') : t('payment_thawani')) ?></small>
</td> </td>
<td> <td>
<div class="d-flex align-items-center gap-2"> <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> <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> </div>
</td> </td>
<td><span class="badge <?= e($row['status']) ?> rounded-pill px-3 py-2"><?= e(status_label($row['status'])) ?></span></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"> <td class="text-end pe-4">
<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')) ?>"> <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> <i class="bi bi-eye"></i>
</a> </a>
</td> </td>
@ -183,11 +233,11 @@ render_header(t('admin_dashboard'), 'admin', true);
</div> </div>
<i class="bi bi-chevron-right ms-auto text-muted small"></i> <i class="bi bi-chevron-right ms-auto text-muted small"></i>
</a> </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"> <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-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> <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> <div>
<h6 class="mb-1 fw-bold"><?= e(t('landing_pages')) ?></h6> <h6 class="mb-1 fw-bold"><?= e(t('summary_report')) ?></h6>
<small class="text-muted d-block line-height-sm"><?= e(t('edit_homepage')) ?></small> <small class="text-muted d-block line-height-sm"><?= e(t('view_analytics')) ?></small>
</div> </div>
<i class="bi bi-chevron-right ms-auto text-muted small"></i> <i class="bi bi-chevron-right ms-auto text-muted small"></i>
</a> </a>
@ -199,4 +249,79 @@ render_header(t('admin_dashboard'), 'admin', true);
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('shipmentsChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: <?= json_encode($chartData['labels']) ?>,
datasets: [{
label: '<?= t('shipments') ?>',
data: <?= json_encode($chartData['data']) ?>,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
borderWidth: 2,
tension: 0.4,
fill: true,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#0d6efd',
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#fff',
titleColor: '#000',
bodyColor: '#666',
borderColor: '#eee',
borderWidth: 1,
padding: 10,
displayColors: false,
callbacks: {
label: function(context) {
return context.parsed.y + ' Shipments';
}
}
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: '#f0f0f0',
drawBorder: false
},
ticks: {
stepSize: 1,
font: {
size: 11
}
}
},
x: {
grid: {
display: false
},
ticks: {
font: {
size: 11
},
maxTicksLimit: 10
}
}
}
}
});
});
</script>
<?php render_footer(); ?> <?php render_footer(); ?>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = []; $errors = [];
$flash = null; $flash = null;
@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS faqs (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"); ");
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['add_faq'])) { if (isset($_POST['add_faq'])) {
$questionEn = trim($_POST['question_en'] ?? ''); $questionEn = trim($_POST['question_en'] ?? '');
$answerEn = trim($_POST['answer_en'] ?? ''); $answerEn = trim($_POST['answer_en'] ?? '');
@ -133,7 +133,7 @@ render_header('Manage FAQs', 'admin', true);
<h5 class="mb-0">Edit FAQ</h5> <h5 class="mb-0">Edit FAQ</h5>
</div> </div>
<div class="card-body"> <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']) ?>"> <input type="hidden" name="faq_id" value="<?= e((string)$editingFaq['id']) ?>">
<div class="col-md-6"> <div class="col-md-6">
@ -172,7 +172,7 @@ render_header('Manage FAQs', 'admin', true);
<h5 class="mb-0">Add New FAQ</h5> <h5 class="mb-0">Add New FAQ</h5>
</div> </div>
<div class="card-body"> <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"> <div class="col-md-6">
<label class="form-label" for="question_en">Question (EN) <span class="text-danger">*</span></label> <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> <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"> <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> <i class="bi bi-pencil"></i>
</a> </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']) ?>"> <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"> <button type="submit" name="delete_faq" class="btn btn-sm btn-light border text-danger" title="Delete">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); 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'; require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema(); ensure_schema();
@ -11,7 +11,7 @@ $success = '';
$testSuccess = ''; $testSuccess = '';
$testError = ''; $testError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
if (isset($_POST['action']) && $_POST['action'] === 'test_whatsapp') { if (isset($_POST['action']) && $_POST['action'] === 'test_whatsapp') {
$testPhone = trim($_POST['test_phone'] ?? ''); $testPhone = trim($_POST['test_phone'] ?? '');
$testMessage = trim($_POST['test_message'] ?? ''); $testMessage = trim($_POST['test_message'] ?? '');
@ -106,7 +106,7 @@ render_header('Integrations', 'admin', true);
<div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div> <div class="alert alert-danger"><?= e(implode('<br>', $errors)) ?></div>
<?php endif; ?> <?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"> <ul class="nav nav-tabs mb-4" id="integrationsTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@ -285,7 +285,7 @@ render_header('Integrations', 'admin', true);
</form> </form>
<!-- Hidden Test 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"> <input type="hidden" name="action" value="test_whatsapp">
</form> </form>
</div> </div>

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); 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); header('Location: ' . url_with_lang('admin_countries.php'), true, 302);
exit; exit;

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); 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 // Ensure user is logged in and is an admin
if (!isset($_SESSION['user_id']) || ($_SESSION['user_role'] ?? '') !== 'admin') { if (!isset($_SESSION['user_id']) || ($_SESSION['user_role'] ?? '') !== 'admin') {
@ -23,7 +23,7 @@ $message = '';
$error = ''; $error = '';
// Handle Actions // Handle Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
if ($action === 'create' || $action === 'edit') { 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> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form method="post" id="userForm"> <form method="post" id="userForm"> <?= csrf_field() ?>
<div class="modal-body p-4"> <div class="modal-body p-4">
<input type="hidden" name="action" id="formAction" value="create"> <input type="hidden" name="action" id="formAction" value="create">
<input type="hidden" name="id" id="userId"> <input type="hidden" name="id" id="userId">
@ -239,7 +239,7 @@ render_header(t('nav_platform_users'), 'platform_users', true);
</div> </div>
<!-- Delete Form --> <!-- 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="action" value="delete">
<input type="hidden" name="id" id="deleteId"> <input type="hidden" name="id" id="deleteId">
</form> </form>

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
// Helper to validate date // Helper to validate date
function validate_date($date, $format = 'Y-m-d') { 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 $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 $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 $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($startDate)) $startDate = date('Y-m-01');
if (!validate_date($endDate)) $endDate = date('Y-m-t'); if (!validate_date($endDate)) $endDate = date('Y-m-t');
@ -95,7 +96,7 @@ try {
$error = "Database error: " . $e->getMessage(); $error = "Database error: " . $e->getMessage();
} }
// Calculate totals for footer // Calculate totals
$totalShipments = 0; $totalShipments = 0;
$grandTotalAmount = 0.0; $grandTotalAmount = 0.0;
$grandTotalProfit = 0.0; $grandTotalProfit = 0.0;
@ -106,6 +107,33 @@ foreach ($results as $row) {
$grandTotalProfit += $row['total_profit']; $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); 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> <p class="muted mb-0"><?= e(t('analyze_performance')) ?></p>
</div> </div>
<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()"> <button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
<i class="bi bi-printer me-2"></i><?= e(t('print')) ?> <i class="bi bi-printer me-2"></i><?= e(t('print')) ?>
</button> </button>

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$id = (int)($_GET['id'] ?? 0); $id = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1'; $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_name = trim($_POST['shipper_name'] ?? '');
$shipper_company = trim($_POST['shipper_company'] ?? ''); $shipper_company = trim($_POST['shipper_company'] ?? '');
// These are now selected from dropdowns, but the value is still the city name // These are now selected from dropdowns, but the value is still the city name
@ -172,7 +172,7 @@ if (!$isAjax):
<div class="p-4"> <div class="p-4">
<?php endif; // End non-ajax wrapper ?> <?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): ?> <?php if ($isAjax): ?>
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><?= t('edit_shipment_title') ?><?= e((string)$shipment['id']) ?></h5> <h5 class="modal-title"><?= t('edit_shipment_title') ?><?= e((string)$shipment['id']) ?></h5>

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$userId = (int)($_GET['id'] ?? 0); $userId = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1'; $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(); $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(); $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'] ?? ''); $fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? ''); $phone = trim($_POST['phone'] ?? '');
@ -151,7 +151,7 @@ if (!$isAjax):
<div class="panel p-4"> <div class="panel p-4">
<?php endif; // End non-ajax wrapper ?> <?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): ?> <?php if ($isAjax): ?>
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Edit Shipper: <?= e($shipper['full_name']) ?></h5> <h5 class="modal-title">Edit Shipper: <?= e($shipper['full_name']) ?></h5>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = []; $errors = [];
$flash = null; $flash = null;
@ -174,7 +174,7 @@ render_header(t('manage_shippers'), 'admin', true);
title="<?= e(t('edit_shipper')) ?>"> title="<?= e(t('edit_shipper')) ?>">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </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']) ?>"> <input type="hidden" name="user_id" value="<?= e((string)$shipper['id']) ?>">
<?php if ($shipper['status'] !== 'active'): ?> <?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')) ?>"> <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 <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$userId = (int)($_GET['id'] ?? 0); $userId = (int)($_GET['id'] ?? 0);
$isAjax = isset($_GET['ajax']) && $_GET['ajax'] === '1'; $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(); $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(); $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'] ?? ''); $fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? ''); $phone = trim($_POST['phone'] ?? '');
@ -178,7 +178,7 @@ if (!$isAjax):
<?php endif; // End non-ajax wrapper ?> <?php endif; // End non-ajax wrapper ?>
<!-- The Form (Shared) --> <!-- 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): ?> <?php if ($isAjax): ?>
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Edit Truck Owner: <?= e($owner['full_name']) ?></h5> <h5 class="modal-title">Edit Truck Owner: <?= e($owner['full_name']) ?></h5>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = []; $errors = [];
$flash = null; $flash = null;
@ -182,7 +182,7 @@ render_header(t('manage_truck_owners'), 'admin', true);
title="<?= e(t('edit_owner')) ?>"> title="<?= e(t('edit_owner')) ?>">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </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']) ?>"> <input type="hidden" name="user_id" value="<?= e((string)$owner['id']) ?>">
<?php if ($owner['status'] !== 'active'): ?> <?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')) ?>"> <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', 'create_account' => 'Create Account',
'back_to_admin' => 'Back to Admin', 'back_to_admin' => 'Back to Admin',
'back_to_home' => 'Back to Home', 'back_to_home' => 'Back to Home',
'total_revenue' => 'Total Revenue',
'shipments_analytics' => 'Shipments Analytics',
'export_csv' => 'Export CSV',
), ),
"ar" => array ( "ar" => array (
'app_name' => 'CargoLink', 'app_name' => 'CargoLink',
@ -578,6 +581,9 @@ $translations = [
'create_account' => 'إنشاء حساب', 'create_account' => 'إنشاء حساب',
'back_to_admin' => 'العودة للإدارة', 'back_to_admin' => 'العودة للإدارة',
'back_to_home' => 'العودة للرئيسية', 'back_to_home' => 'العودة للرئيسية',
'total_revenue' => 'إجمالي الإيرادات',
'shipments_analytics' => 'تحليلات الشحنات',
'export_csv' => 'تصدير CSV',
) )
]; ];
@ -593,6 +599,7 @@ function e($value): string
} }
function ensure_schema(): void 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 ( db()->exec("\n CREATE TABLE IF NOT EXISTS landing_sections (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@ -792,6 +799,47 @@ function format_currency(float $amount): string
return number_format($amount, 3) . ' OMR'; 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 // Set timezone from settings
try { try {
$tz = get_setting('timezone', 'UTC'); $tz = get_setting('timezone', 'UTC');

View File

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

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_login();
ensure_schema(); ensure_schema();
@ -131,7 +131,7 @@ $flash = get_flash();
</div> </div>
<?php endif; ?> <?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"> <input type="hidden" name="action" value="update_profile">
<div class="text-center mb-4"> <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(); $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(); $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'; $role = $_POST['role'] ?? 'shipper';
$fullName = trim($_POST['full_name'] ?? ''); $fullName = trim($_POST['full_name'] ?? '');
$email = trim($_POST['email'] ?? ''); $email = trim($_POST['email'] ?? '');
@ -263,7 +263,7 @@ render_header('Shipper & Truck Owner Registration');
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div> <div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?> <?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="row g-3">
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label" for="role"><?= e(t('role')) ?></label> <label class="form-label" for="role"><?= e(t('role')) ?></label>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_once __DIR__ . '/includes/layout.php'; require_login();
require_once __DIR__ . '/includes/NotificationService.php'; require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema(); ensure_schema();
@ -18,19 +18,21 @@ if ($shipmentId > 0) {
$errors = []; $errors = [];
$flash = get_flash(); $flash = get_flash();
$userRole = $_SESSION['user_role'] ?? ''; $userRole = $_SESSION['user_role'] ?? '';
$currentUserId = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
$isAdmin = $userRole === 'admin'; $isAdmin = $userRole === 'admin';
$isShipper = $userRole === 'shipper'; $isShipper = $userRole === 'shipper';
$isTruckOwner = $userRole === 'truck_owner'; $isTruckOwner = $userRole === 'truck_owner';
$isAssignedTruckOwner = $shipment && $shipment['truck_owner_id'] == $currentUserId;
// Platform Fee Configuration // Platform Fee Configuration
$settings = get_settings(); $settings = get_settings();
$platformFeePercentage = (float)($settings['platform_charge_percentage'] ?? 0) / 100; $platformFeePercentage = (float)($settings['platform_charge_percentage'] ?? 0) / 100;
// Handle POST actions // Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') { validate_csrf_token();
$action = $_POST['action'] ?? ''; $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'] ?? ''); $offerOwner = trim($_POST['offer_owner'] ?? '');
$offerPrice = trim($_POST['offer_price'] ?? ''); $offerPrice = trim($_POST['offer_price'] ?? '');
if ($offerOwner === '' || $offerPrice === '') { if ($offerOwner === '' || $offerPrice === '') {
@ -127,6 +129,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId])); header('Location: ' . url_with_lang('shipment_detail.php', ['id' => $shipmentId]));
exit; 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>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<form method="post"> <form method="post"> <?= csrf_field() ?>
<input type="hidden" name="action" value="accept_offer"> <input type="hidden" name="action" value="accept_offer">
<button class="btn btn-success w-100 py-3 fw-bold shadow-sm" type="submit"> <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 <i class="bi bi-credit-card-2-front me-2"></i>Accept & Pay Now
</button> </button>
</form> </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"> <input type="hidden" name="action" value="reject_offer">
<button class="btn btn-outline-danger w-100 py-2 fw-bold" type="submit"> <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 <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> <p class="mb-0 text-muted">This shipment has been confirmed and paid for.</p>
</div> </div>
<?php endif; ?> <?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> </div>
<?php else: ?> <?php else: ?>
<!-- Truck Owner / Admin / Other View (Submit Offer) --> <!-- Truck Owner / Admin / Other View (Submit Offer & POD) -->
<div class="panel p-4"> <div class="panel p-4">
<h3 class="section-title"><?= e(t('submit_offer')) ?></h3> <h3 class="section-title"><?= e(t('submit_offer')) ?></h3>
<?php if ($flash): ?> <?php if ($flash): ?>
@ -305,7 +363,7 @@ render_header(t('shipment_detail'));
<?php endif; ?> <?php endif; ?>
<?php if ($shipment['status'] === 'posted' || $shipment['status'] === 'offered'): ?> <?php if ($shipment['status'] === 'posted' || $shipment['status'] === 'offered'): ?>
<form method="post"> <form method="post"> <?= csrf_field() ?>
<input type="hidden" name="action" value="submit_offer"> <input type="hidden" name="action" value="submit_offer">
<div class="mb-3"> <div class="mb-3">
<label class="form-label"><?= e(t('offer_owner')) ?></label> <label class="form-label"><?= e(t('offer_owner')) ?></label>
@ -322,6 +380,39 @@ render_header(t('shipment_detail'));
This shipment is already confirmed/processed. This shipment is already confirmed/processed.
</div> </div>
<?php endif; ?> <?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> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); 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'; require_once __DIR__ . '/includes/NotificationService.php';
ensure_schema(); ensure_schema();
@ -168,7 +168,7 @@ $flash = get_flash();
<?php if ($errors): ?> <?php if ($errors): ?>
<div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div> <div class="alert alert-warning"><?= e(implode(' ', $errors)) ?></div>
<?php endif; ?> <?php endif; ?>
<form method="post"> <form method="post"> <?= csrf_field() ?>
<input type="hidden" name="action" value="create_shipment"> <input type="hidden" name="action" value="create_shipment">
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold"><?= e(t('shipper_name')) ?></label> <label class="form-label text-muted small fw-bold"><?= e(t('shipper_name')) ?></label>

View File

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