39038-vm/admin_shipments.php
2026-03-14 13:13:28 +00:00

319 lines
16 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php'; require_role('admin');
$errors = [];
$flash = null;
// Handle action (Delete)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'], $_POST['shipment_id'])) {
$shipmentId = (int)$_POST['shipment_id'];
$action = $_POST['action'];
if ($action === 'delete') {
db()->prepare("DELETE FROM shipments WHERE id = ?")->execute([$shipmentId]);
$flash = t('flash_shipment_deleted');
}
}
// Search, Filter, Sort and Pagination parameters
$q = trim($_GET['q'] ?? '');
$status = trim($_GET['status'] ?? '');
$sort = trim($_GET['sort'] ?? 'newest');
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
$whereClause = "1=1";
$params = [];
if ($q !== '') {
$whereClause .= " AND (shipper_name LIKE ? OR shipper_company LIKE ? OR origin_city LIKE ? OR destination_city LIKE ? OR cargo_description LIKE ?)";
$likeQ = "%$q%";
$params = array_merge($params, array_fill(0, 5, $likeQ));
}
if ($status !== '' && in_array($status, ['posted', 'offered', 'confirmed', 'in_transit', 'delivered'])) {
$whereClause .= " AND status = ?";
$params[] = $status;
}
// Sorting logic
$orderBy = match ($sort) {
'oldest' => 'created_at ASC',
'pickup_asc' => 'pickup_date ASC',
'pickup_desc' => 'pickup_date DESC',
default => 'created_at DESC', // newest
};
// Total count
$countSql = "SELECT COUNT(*) FROM shipments WHERE $whereClause";
$stmt = db()->prepare($countSql);
$stmt->execute($params);
$total = (int)$stmt->fetchColumn();
$totalPages = (int)ceil($total / $limit);
// Fetch shipments
$sql = "
SELECT *
FROM shipments
WHERE $whereClause
ORDER BY $orderBy
LIMIT $limit OFFSET $offset
";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$shipments = $stmt->fetchAll();
render_header(t('manage_shipments'), 'admin', true);
?>
<div class="row g-0">
<div class="col-md-2 bg-white border-end min-vh-100">
<?php render_admin_sidebar('shipments'); ?>
</div>
<div class="col-md-10 p-4">
<div class="page-intro d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
<div>
<h1 class="section-title mb-1"><?= t('shipments_header') ?></h1>
<p class="muted mb-0"><?= t('shipments_subtitle') ?></p>
</div>
</div>
<?php if ($flash): ?>
<div class="alert alert-success" data-auto-dismiss="true"><?= e($flash) ?></div>
<?php endif; ?>
<div class="panel p-4 mb-4">
<form method="get" class="row g-3">
<div class="col-md-5">
<label class="form-label small text-muted"><?= t('search_label') ?></label>
<input type="text" name="q" class="form-control" placeholder="<?= t('search_shipments_placeholder') ?>" value="<?= e($q) ?>">
</div>
<div class="col-md-3">
<label class="form-label small text-muted"><?= t('status') ?></label>
<select name="status" class="form-select">
<option value=""><?= t('all_statuses') ?></option>
<option value="posted" <?= $status === 'posted' ? 'selected' : '' ?>><?= t('status_posted') ?></option>
<option value="offered" <?= $status === 'offered' ? 'selected' : '' ?>><?= t('status_offered') ?></option>
<option value="confirmed" <?= $status === 'confirmed' ? 'selected' : '' ?>><?= t('status_confirmed') ?></option>
<option value="in_transit" <?= $status === 'in_transit' ? 'selected' : '' ?>><?= t('status_in_transit') ?></option>
<option value="delivered" <?= $status === 'delivered' ? 'selected' : '' ?>><?= t('status_delivered') ?></option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small text-muted"><?= t('sort_by') ?></label>
<select name="sort" class="form-select">
<option value="newest" <?= $sort === 'newest' ? 'selected' : '' ?>><?= t('sort_newest') ?></option>
<option value="oldest" <?= $sort === 'oldest' ? 'selected' : '' ?>><?= t('sort_oldest') ?></option>
<option value="pickup_asc" <?= $sort === 'pickup_asc' ? 'selected' : '' ?>><?= t('sort_pickup_soonest') ?></option>
<option value="pickup_desc" <?= $sort === 'pickup_desc' ? 'selected' : '' ?>><?= t('sort_pickup_latest') ?></option>
</select>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100"><?= t('filter') ?></button>
</div>
</form>
</div>
<div class="panel p-0">
<?php if (!$shipments && ($q || $status)): ?>
<div class="p-4"><p class="muted mb-0"><?= t('no_shipments_found_criteria') ?></p></div>
<?php elseif (!$shipments): ?>
<div class="p-4"><p class="muted mb-0"><?= t('no_shipments_platform') ?></p></div>
<?php else: ?>
<div class="table-responsive">
<table class="table mb-0 align-middle table-hover">
<thead>
<tr>
<th class="ps-4"><?= t('id_col') ?></th>
<th><?= t('shipper') ?></th>
<th><?= t('route_label') ?></th>
<th><?= t('dates_col') ?></th>
<th><?= t('status') ?></th>
<th class="text-end pe-4"><?= t('action') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($shipments as $shipment): ?>
<tr>
<td class="ps-4"><?= e((string)$shipment['id']) ?></td>
<td>
<div class="fw-bold text-dark"><?= e($shipment['shipper_name']) ?></div>
<div class="text-muted small"><?= e((string)$shipment['shipper_company']) ?></div>
</td>
<td>
<div><span class="text-muted small"><?= t('from_label') ?></span> <?= e($shipment['origin_city']) ?></div>
<div><span class="text-muted small"><?= t('to_label') ?></span> <?= e($shipment['destination_city']) ?></div>
</td>
<td>
<div class="small"><span class="text-muted"><?= t('pick_label') ?></span> <?= e($shipment['pickup_date']) ?></div>
<div class="small"><span class="text-muted"><?= t('drop_label') ?></span> <?= e($shipment['delivery_date']) ?></div>
</td>
<td>
<?php
$statusClass = 'bg-secondary-subtle text-secondary';
if ($shipment['status'] === 'posted') $statusClass = 'bg-primary-subtle text-primary';
elseif ($shipment['status'] === 'offered') $statusClass = 'bg-info-subtle text-info';
elseif ($shipment['status'] === 'confirmed') $statusClass = 'bg-success-subtle text-success';
elseif ($shipment['status'] === 'in_transit') $statusClass = 'bg-warning-subtle text-warning';
elseif ($shipment['status'] === 'delivered') $statusClass = 'bg-dark-subtle text-dark';
?>
<span class="badge <?= $statusClass ?>"><?= e(status_label($shipment['status'])) ?></span>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-1 align-items-center">
<a href="shipment_detail.php?id=<?= e((string)$shipment['id']) ?>" class="btn btn-sm p-1 border-0 bg-transparent text-primary" title="<?= t('view_shipment') ?>">
<i class="bi bi-eye"></i>
</a>
<a href="admin_shipment_edit.php?id=<?= e((string)$shipment['id']) ?>"
class="btn btn-sm p-1 border-0 bg-transparent text-primary ajax-modal-trigger"
data-bs-toggle="modal"
data-bs-target="#editModal"
title="<?= t('edit_shipment_tooltip') ?>">
<i class="bi bi-pencil"></i>
</a>
<form method="post" class="d-inline m-0 p-0"> <?= csrf_field() ?>
<input type="hidden" name="shipment_id" value="<?= e((string)$shipment['id']) ?>">
<button type="submit" name="action" value="delete" class="btn btn-sm p-1 border-0 bg-transparent text-danger" onclick="return confirm('<?= t('confirm_delete_shipment') ?>');" title="<?= t('delete') ?>">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div class="px-4 py-3 border-top d-flex justify-content-between align-items-center">
<span class="text-muted small"><?= t('showing') ?> <?= count($shipments) ?> <?= t('of') ?> <?= $total ?> <?= t('shipments_header') ?></span>
<ul class="pagination pagination-sm mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $page - 1 ?>"><?= t('previous') ?></a>
</li>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $i ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $page >= $totalPages ? 'disabled' : '' ?>">
<a class="page-link" href="?q=<?= urlencode($q) ?>&status=<?= urlencode($status) ?>&sort=<?= urlencode($sort) ?>&page=<?= $page + 1 ?>"><?= t('next') ?></a>
</li>
</ul>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel"><?= t('edit_shipment_tooltip') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center p-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden"><?= t('loading') ?></span>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const editModal = document.getElementById('editModal');
if (!editModal) return;
editModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
if (!button.classList.contains('ajax-modal-trigger')) return;
const url = button.getAttribute('href') + '&ajax=1';
const modalContent = editModal.querySelector('.modal-content');
modalContent.innerHTML = `
<div class="modal-header">
<h5 class="modal-title"><?= t('loading') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center p-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
`;
fetch(url)
.then(response => response.text())
.then(html => {
if (html.startsWith('{')) {
const data = JSON.parse(html);
modalContent.innerHTML = `<div class="modal-header"><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="alert alert-danger">${data.message}</div></div>`;
return;
}
modalContent.innerHTML = html;
modalContent.querySelectorAll('script').forEach(script => {
const newScript = document.createElement('script');
if (script.src) newScript.src = script.src;
newScript.textContent = script.textContent;
document.body.appendChild(newScript);
});
const form = modalContent.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
if(submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Loading...';
}
const formData = new FormData(form);
fetch(form.action, { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (data.success) {
location.reload();
} else {
if(submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '<?= t('save_changes') ?>';
}
const errDiv = form.querySelector('#form-errors');
if(errDiv) {
errDiv.classList.remove('d-none');
errDiv.innerHTML = data.message;
} else {
alert(data.message || '<?= t('error_occurred') ?>');
}
}
})
.catch(err => {
console.error(err);
if(submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '<?= t('save_changes') ?>';
}
alert('<?= t('error_occurred') ?>');
});
});
}
})
.catch(err => {
modalContent.innerHTML = `<div class="modal-body text-danger"><?= t('failed_load_form') ?></div>`;
});
});
});
</script>
<?php render_footer(); ?>