433 lines
26 KiB
PHP
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>
|