Files
2026-08-12 21:29:55 +03:00

433 lines
26 KiB
PHP

<?php include(__DIR__ . '/../include/headscript.php'); ?>
<?php
require_once __DIR__ . '/include/functions.php';
$pdo = mnt_pdo();
if (!userCan('maintenance.equipment.view')) {
http_response_code(403);
exit('Permesso negato.');
}
$canManage = userCan('maintenance.manage');
// ---------------------------------------------------------------- filters
$filterSearch = trim((string)($_GET['q'] ?? ''));
$filterCategory = isset($_GET['category']) && is_numeric($_GET['category']) ? (int)$_GET['category'] : null;
$filterLine = isset($_GET['line']) && is_numeric($_GET['line']) ? (int)$_GET['line'] : null;
$filterDepartment = isset($_GET['department']) && is_numeric($_GET['department']) ? (int)$_GET['department'] : null;
$filterStatus = (string)($_GET['status'] ?? 'active');
$where = [];
$params = [];
if ($filterSearch !== '') {
$where[] = "(e.name LIKE ? OR e.serial_number LIKE ? OR e.registration_number LIKE ? OR e.manufacturer LIKE ?)";
$like = '%' . $filterSearch . '%';
array_push($params, $like, $like, $like, $like);
}
if ($filterCategory) {
$where[] = "e.category_id = ?";
$params[] = $filterCategory;
}
if ($filterLine) {
$where[] = "e.line_id = ?";
$params[] = $filterLine;
}
if ($filterDepartment) {
$where[] = "e.department_id = ?";
$params[] = $filterDepartment;
}
if ($filterStatus !== '' && $filterStatus !== 'all') {
$where[] = "e.status = ?";
$params[] = $filterStatus;
}
$sql = "
SELECT e.*,
c.name AS category_name,
c.color AS category_color,
pl.name AS line_name,
pl.line_number,
d.name AS department_name,
cf.stored_name AS cover_stored,
(SELECT COUNT(*) FROM maint_maintenances m
WHERE m.equipment_id = e.id AND m.is_active = 1) AS maintenance_count,
(SELECT MIN(m.next_due_date) FROM maint_maintenances m
WHERE m.equipment_id = e.id AND m.is_active = 1 AND m.next_due_date IS NOT NULL) AS next_due_date
FROM inv_equipment e
LEFT JOIN inv_categories c ON c.id = e.category_id
LEFT JOIN production_lines pl ON pl.id = e.line_id
LEFT JOIN departments d ON d.id = e.department_id
LEFT JOIN inv_equipment_files cf ON cf.id = e.cover_file_id
";
$whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
$sql .= $whereSql;
$sql .= ' ORDER BY c.sort_order ASC, e.name ASC';
// The filters only touch columns of inv_equipment, so counting needs no joins.
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM inv_equipment e' . $whereSql);
$countStmt->execute($params);
$totalEquipment = (int)$countStmt->fetchColumn();
[$page, $perPage] = mnt_page_params();
$totalPages = max(1, (int)ceil($totalEquipment / $perPage));
$page = min($page, $totalPages);
// Cast to int above, so interpolating here cannot carry anything but digits;
// LIMIT/OFFSET placeholders would need PDO::ATTR_EMULATE_PREPARES off.
$sql .= sprintf(' LIMIT %d OFFSET %d', $perPage, ($page - 1) * $perPage);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$equipment = $stmt->fetchAll(PDO::FETCH_ASSOC);
$formData = mnt_form_data($pdo);
$statuses = mnt_equipment_statuses();
$MNT_TITLE = 'Registro attrezzature';
?>
<!doctype html>
<html lang="it">
<head>
<?php include __DIR__ . '/include/page_head.php'; ?>
</head>
<body>
<?php include __DIR__ . '/include/wrapper_open.php'; ?>
<?php include(__DIR__ . '/../include/navbar.php'); ?>
<?php include(__DIR__ . '/../include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card mnt-card">
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
<h5><i class="fa-solid fa-boxes-stacked me-2"></i>Registro attrezzature</h5>
<div class="header-actions d-flex gap-2 flex-wrap">
<a href="manutenzioni/maintenances.php" class="btn btn-mnt-outline d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-screwdriver-wrench"></i><span>Manutenzioni</span>
</a>
<?php if ($canManage): ?>
<button class="btn btn-mnt-primary d-inline-flex align-items-center gap-2" id="btnAddEquipment">
<i class="fa-solid fa-plus"></i><span>Nuova attrezzatura</span>
</button>
<?php endif; ?>
</div>
</div>
<div class="card-body">
<form class="mnt-filter-bar" method="get" id="filterForm">
<input type="text" class="form-control" name="q" placeholder="Cerca nome, matricola, seriale…"
value="<?= mnt_h($filterSearch) ?>">
<select class="form-select" name="category">
<option value="">Tutte le categorie</option>
<?php foreach ($formData['categories'] as $category): ?>
<option value="<?= (int)$category['id'] ?>" <?= $filterCategory === (int)$category['id'] ? 'selected' : '' ?>>
<?= mnt_h($category['name']) ?>
</option>
<?php endforeach; ?>
</select>
<select class="form-select" name="line">
<option value="">Tutte le linee</option>
<?php foreach ($formData['lines'] as $line): ?>
<option value="<?= (int)$line['id'] ?>" <?= $filterLine === (int)$line['id'] ? 'selected' : '' ?>>
Linea <?= (int)$line['line_number'] ?> — <?= mnt_h($line['name']) ?>
</option>
<?php endforeach; ?>
</select>
<select class="form-select" name="department">
<option value="">Tutti i reparti</option>
<?php foreach ($formData['departments'] as $department): ?>
<option value="<?= (int)$department['id'] ?>" <?= $filterDepartment === (int)$department['id'] ? 'selected' : '' ?>>
<?= mnt_h($department['name']) ?>
</option>
<?php endforeach; ?>
</select>
<select class="form-select" name="status">
<option value="all" <?= $filterStatus === 'all' ? 'selected' : '' ?>>Tutti gli stati</option>
<?php foreach ($statuses as $value => $label): ?>
<option value="<?= mnt_h($value) ?>" <?= $filterStatus === $value ? 'selected' : '' ?>>
<?= mnt_h($label) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($perPage !== 25): ?>
<input type="hidden" name="per_page" value="<?= (int)$perPage ?>">
<?php endif; ?>
<!-- Every control applies itself, so there is no «Filtra» button to
press. This one stays only so that Enter in the search box
submits, and so the bar still works without JavaScript. -->
<button type="submit" class="visually-hidden">Filtra</button>
<a href="manutenzioni/index.php" class="btn btn-mnt-outline btn-reset-filters d-inline-flex align-items-center gap-2">
<i class="fa-solid fa-xmark"></i><span>Azzera filtri</span>
</a>
</form>
<?php if (!$equipment): ?>
<div class="empty-state">
<i class="fa-solid fa-boxes-stacked"></i>
<p>Nessuna attrezzatura trovata con questi filtri.</p>
</div>
<?php else: ?>
<div id="equipmentList">
<!-- CARD -->
<div class="d-xl-none">
<?php foreach ($equipment as $item): ?>
<?php
$state = mnt_due_state($item['next_due_date']);
// Qui la riga aggrega piu' manutenzioni: «nessuna data» non
// significa «al bisogno», quindi l'etichetta resta neutra.
$badge = $state === 'none'
? ['label' => 'Senza scadenza', 'class' => 'mnt-badge-none']
: mnt_due_badge($state);
?>
<div class="mnt-item-card" data-id="<?= (int)$item['id'] ?>"
style="--row-color: <?= mnt_h($item['category_color'] ?? '#e9ecef') ?>">
<div class="d-flex gap-2 align-items-start">
<?php if (!empty($item['cover_stored'])): ?>
<img class="mnt-thumb" src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$item['cover_file_id'] ?>" alt="">
<?php else: ?>
<span class="mnt-thumb mnt-thumb-placeholder"><i class="fa-solid fa-gear"></i></span>
<?php endif; ?>
<div class="flex-grow-1">
<div class="ic-title">
<?= mnt_h($item['name']) ?>
<?php if ($item['status'] !== 'active'): ?>
<span class="mnt-badge mnt-badge-status-<?= mnt_h($item['status']) ?>">
<?= mnt_h($statuses[$item['status']] ?? $item['status']) ?>
</span>
<?php endif; ?>
</div>
<div class="ic-meta">
<?= mnt_h($item['category_name'] ?? 'Senza categoria') ?>
<?php if ($item['line_name']): ?>
· Linea <?= (int)$item['line_number'] ?>
<?php endif; ?>
</div>
<div class="ic-meta">
Manutenzioni: <strong><?= (int)$item["maintenance_count"] ?></strong> attive
<?php if ($item['maintenance_count'] > 0): ?>
· <span class="mnt-badge <?= $badge['class'] ?>"><?= $badge['label'] ?></span>
<?php endif; ?>
</div>
</div>
</div>
<div class="ic-actions">
<a class="btn-action btn-action-view" title="Apri scheda"
href="manutenzioni/equipment.php?id=<?= (int)$item['id'] ?>">
<i class="fa-solid fa-eye"></i>
</a>
<?php if ($canManage): ?>
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
<button class="btn-action btn-action-delete btn-delete"
title="Elimina"
data-name="<?= mnt_h($item['name']) ?>"
data-maintenances="<?= (int)$item['maintenance_count'] ?>">
<i class="fa-solid fa-trash"></i>
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- DESKTOP -->
<!-- Table only from 1200px up: below that the 8 columns force
horizontal scrolling, which is unusable on a tablet. -->
<div class="d-none d-xl-block table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:60px"></th>
<th class="mnt-name-cell">Nome</th>
<th>Categoria</th>
<th>Matricola / Seriale</th>
<th>Ubicazione</th>
<th class="text-center">Stato</th>
<th class="text-center">Manutenzioni</th>
<th class="text-center" style="width:140px">Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($equipment as $item): ?>
<?php
$state = mnt_due_state($item['next_due_date']);
// Qui la riga aggrega piu' manutenzioni: «nessuna data» non
// significa «al bisogno», quindi l'etichetta resta neutra.
$badge = $state === 'none'
? ['label' => 'Senza scadenza', 'class' => 'mnt-badge-none']
: mnt_due_badge($state);
$statusKey = (string)$item['status'];
?>
<tr data-id="<?= (int)$item['id'] ?>">
<td>
<?php if (!empty($item['cover_stored'])): ?>
<img class="mnt-thumb" src="manutenzioni/ajax/download_file.php?scope=equipment&id=<?= (int)$item['cover_file_id'] ?>" alt="">
<?php else: ?>
<span class="mnt-thumb mnt-thumb-placeholder"><i class="fa-solid fa-gear"></i></span>
<?php endif; ?>
</td>
<td class="mnt-name-cell">
<div class="fw-semibold mnt-title-clamp" style="color:var(--mnt-heading)"
title="<?= mnt_h($item['name']) ?>"><?= mnt_h($item['name']) ?></div>
<?php if ($item['manufacturer']): ?>
<div class="small text-muted mnt-title-clamp"
title="<?= mnt_h($item['manufacturer']) ?>"><?= mnt_h($item['manufacturer']) ?></div>
<?php endif; ?>
</td>
<td>
<span class="d-inline-flex align-items-center gap-2">
<span class="mnt-cat-dot" style="background: <?= mnt_h($item['category_color'] ?? '#adb5bd') ?>"></span>
<span><?= mnt_h($item['category_name'] ?? '—') ?></span>
</span>
<?php if ($item['line_name']): ?>
<div class="small text-muted">Linea <?= (int)$item['line_number'] ?> — <?= mnt_h($item['line_name']) ?></div>
<?php endif; ?>
</td>
<td class="small">
<?= mnt_h($item['registration_number'] ?: '—') ?>
<?php if ($item['serial_number']): ?>
<div class="text-muted"><?= mnt_h($item['serial_number']) ?></div>
<?php endif; ?>
</td>
<td class="small">
<?= mnt_h($item['department_name'] ?: '—') ?>
<?php if ($item['location']): ?>
<div class="text-muted"><?= mnt_h($item['location']) ?></div>
<?php endif; ?>
</td>
<td class="text-center">
<span class="mnt-badge mnt-badge-status-<?= mnt_h($statusKey) ?>">
<?= mnt_h($statuses[$statusKey] ?? $statusKey) ?>
</span>
</td>
<td class="text-center">
<?php if ((int)$item['maintenance_count'] === 0): ?>
<span class="text-muted small">—</span>
<?php else: ?>
<span class="mnt-badge <?= $badge['class'] ?>"><?= $badge['label'] ?></span>
<div class="small text-muted mt-1"><?= (int)$item['maintenance_count'] ?> attive</div>
<?php endif; ?>
</td>
<td class="text-center">
<div class="d-inline-flex gap-1">
<a class="btn-action btn-action-view" title="Apri scheda"
href="manutenzioni/equipment.php?id=<?= (int)$item['id'] ?>">
<i class="fa-solid fa-eye"></i>
</a>
<?php if ($canManage): ?>
<button class="btn-action btn-action-edit btn-edit" title="Modifica"><i class="fa-solid fa-pen"></i></button>
<button class="btn-action btn-action-delete btn-delete"
title="Elimina"
data-name="<?= mnt_h($item['name']) ?>"
data-maintenances="<?= (int)$item['maintenance_count'] ?>">
<i class="fa-solid fa-trash"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php
$mntPagerUrl = 'manutenzioni/index.php';
$mntPagerPage = $page;
$mntPagerPages = $totalPages;
$mntPagerTotal = $totalEquipment;
$mntPagerPer = $perPage;
include __DIR__ . '/include/pagination.php';
?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include(__DIR__ . '/../include/footer.php'); ?>
</div>
<?php if ($canManage): ?>
<?php include __DIR__ . '/include/equipment_modal.php'; ?>
<?php endif; ?>
<?php include(__DIR__ . '/../jsinclude.php'); ?>
<script>
$(function () {
$('#filterForm select').on('change', function () { $('#filterForm').submit(); });
// The search box applies itself too, so the whole bar behaves the
// same way; the pause keeps it from reloading on every keystroke.
let mntSearchTimer = null;
$('#filterForm input[name=q]').on('input', function () {
clearTimeout(mntSearchTimer);
mntSearchTimer = setTimeout(function () { $('#filterForm').submit(); }, 500);
});
// The reload lands with the box empty of focus, mid-word: put the
// caret back where it was so typing can simply continue.
const mntSearch = document.querySelector('#filterForm input[name=q]');
if (mntSearch && mntSearch.value) {
mntSearch.focus();
mntSearch.setSelectionRange(mntSearch.value.length, mntSearch.value.length);
}
<?php if ($canManage): ?>
$('#btnAddEquipment').on('click', function () { mntOpenEquipmentModal(null); });
$('#equipmentList').on('click', '.btn-edit', function () {
const id = $(this).closest('[data-id]').data('id');
$.getJSON('manutenzioni/ajax/get_equipment.php', { id: id })
.done(res => {
if (res.success) { mntOpenEquipmentModal(res.equipment); }
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
})
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
});
$('#equipmentList').on('click', '.btn-delete', function () {
const $btn = $(this);
const id = $btn.closest('[data-id]').data('id');
const name = $btn.data('name');
const maintenances = parseInt($btn.data('maintenances') || 0, 10);
Swal.fire({
title: `Eliminare "${name}"?`,
html: maintenances > 0
? `<p>Verranno eliminate anche <strong>${maintenances}</strong> manutenzioni e tutto lo storico interventi.</p>`
: '<p>L\'operazione non è reversibile.</p>',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#dc3545'
}).then(r => {
if (!r.isConfirmed) return;
$.post('manutenzioni/ajax/delete_equipment.php', { id: id })
.done(res => {
if (res.success) { location.reload(); }
else { Swal.fire({ icon: 'error', title: 'Errore', text: res.message }); }
})
.fail(() => Swal.fire({ icon: 'error', title: 'Errore di rete' }));
});
});
<?php endif; ?>
});
</script>
</body>
</html>