Files
zibo-dashboard/public/userarea/ppe-items.php
T

1449 lines
52 KiB
PHP

<?php
include('include/headscript.php');
if (function_exists('requirePermission')) {
requirePermission('hr.employees.view');
}
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
/*
|--------------------------------------------------------------------------
| Upload configuration
|--------------------------------------------------------------------------
| The page is expected to be inside /public.
| Files will be stored in /public/uploads/ppe.
| The database stores the relative browser path, for example:
| uploads/ppe/ppe_20260604_123456_abcd.jpg
|--------------------------------------------------------------------------
*/
const PPE_UPLOAD_RELATIVE_DIR = 'uploads/ppe';
const PPE_UPLOAD_MAX_SIZE = 5242880; // 5 MB
$ppeUploadAbsoluteDir = __DIR__ . '/' . PPE_UPLOAD_RELATIVE_DIR;
if (!is_dir($ppeUploadAbsoluteDir)) {
mkdir($ppeUploadAbsoluteDir, 0755, true);
}
function jsonResponse(bool $success, string $message = '', array $extra = []): void
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array_merge([
'success' => $success,
'message' => $message,
], $extra));
exit;
}
function cleanString(?string $value): string
{
return trim((string)$value);
}
function cleanNullableString(?string $value): ?string
{
$value = trim((string)$value);
return $value === '' ? null : $value;
}
function toActiveValue($value): int
{
return ((string)$value === '1') ? 1 : 0;
}
function normalizePathForDb(string $path): string
{
return str_replace('\\', '/', $path);
}
function deletePpePhotoIfExists(?string $relativePath): void
{
if (!$relativePath) {
return;
}
$relativePath = normalizePathForDb($relativePath);
if (strpos($relativePath, PPE_UPLOAD_RELATIVE_DIR . '/') !== 0) {
return;
}
$absolutePath = __DIR__ . '/' . $relativePath;
if (is_file($absolutePath)) {
unlink($absolutePath);
}
}
function uploadPpePhoto(array $file): ?string
{
if (!isset($file['error']) || $file['error'] === UPLOAD_ERR_NO_FILE) {
return null;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Errore durante il caricamento della foto.');
}
if (($file['size'] ?? 0) > PPE_UPLOAD_MAX_SIZE) {
throw new RuntimeException('La foto supera la dimensione massima consentita di 5 MB.');
}
$tmpPath = $file['tmp_name'] ?? '';
if (!is_uploaded_file($tmpPath)) {
throw new RuntimeException('File caricato non valido.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($tmpPath);
$allowedMimeTypes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
if (!isset($allowedMimeTypes[$mimeType])) {
throw new RuntimeException('Formato foto non consentito. Sono ammessi JPG, PNG e WEBP.');
}
$extension = $allowedMimeTypes[$mimeType];
$randomName = bin2hex(random_bytes(6));
$fileName = 'ppe_' . date('Ymd_His') . '_' . $randomName . '.' . $extension;
$absoluteDestination = __DIR__ . '/' . PPE_UPLOAD_RELATIVE_DIR . '/' . $fileName;
$relativeDestination = PPE_UPLOAD_RELATIVE_DIR . '/' . $fileName;
if (!move_uploaded_file($tmpPath, $absoluteDestination)) {
throw new RuntimeException('Impossibile salvare la foto nella cartella DPI.');
}
return normalizePathForDb($relativeDestination);
}
/*
|--------------------------------------------------------------------------
| AJAX actions
|--------------------------------------------------------------------------
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
try {
$action = $_POST['action'];
/*
|--------------------------------------------------------------------------
| Save PPE
|--------------------------------------------------------------------------
*/
if ($action === 'save_ppe') {
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
$name = cleanString($_POST['name'] ?? '');
$description = cleanNullableString($_POST['description'] ?? null);
$category = cleanNullableString($_POST['category'] ?? null);
$standardReference = cleanNullableString($_POST['standard_reference'] ?? null);
$validityMonths = isset($_POST['validity_months']) && $_POST['validity_months'] !== '' ? (int)$_POST['validity_months'] : null;
$sortOrder = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
$isActive = toActiveValue($_POST['is_active'] ?? 1);
$removePhoto = (int)($_POST['remove_photo'] ?? 0) === 1;
if ($name === '') {
jsonResponse(false, 'Il nome del DPI è obbligatorio.');
}
$uploadedPhotoPath = null;
if (isset($_FILES['photo'])) {
$uploadedPhotoPath = uploadPpePhoto($_FILES['photo']);
}
if ($id) {
$stmt = $pdo->prepare("
SELECT photo
FROM ppe_items
WHERE id = ?
LIMIT 1
");
$stmt->execute([$id]);
$existingPhoto = $stmt->fetchColumn();
if ($existingPhoto === false) {
jsonResponse(false, 'DPI non trovato.');
}
$finalPhoto = $existingPhoto;
if ($removePhoto) {
deletePpePhotoIfExists($existingPhoto);
$finalPhoto = null;
}
if ($uploadedPhotoPath) {
deletePpePhotoIfExists($existingPhoto);
$finalPhoto = $uploadedPhotoPath;
}
$stmt = $pdo->prepare("
UPDATE ppe_items
SET name = :name,
description = :description,
category = :category,
photo = :photo,
standard_reference = :standard_reference,
validity_months = :validity_months,
sort_order = :sort_order,
is_active = :is_active,
updated_at = CURRENT_TIMESTAMP
WHERE id = :id
");
$stmt->execute([
':name' => $name,
':description' => $description,
':category' => $category,
':photo' => $finalPhoto,
':standard_reference' => $standardReference,
':validity_months' => $validityMonths,
':sort_order' => $sortOrder,
':is_active' => $isActive,
':id' => $id,
]);
jsonResponse(true, 'DPI aggiornato correttamente.');
}
$stmt = $pdo->prepare("
INSERT INTO ppe_items
(
name,
description,
category,
photo,
standard_reference,
validity_months,
sort_order,
is_active,
created_at,
updated_at
)
VALUES
(
:name,
:description,
:category,
:photo,
:standard_reference,
:validity_months,
:sort_order,
:is_active,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
");
$stmt->execute([
':name' => $name,
':description' => $description,
':category' => $category,
':photo' => $uploadedPhotoPath,
':standard_reference' => $standardReference,
':validity_months' => $validityMonths,
':sort_order' => $sortOrder,
':is_active' => $isActive,
]);
jsonResponse(true, 'DPI creato correttamente.');
}
/*
|--------------------------------------------------------------------------
| Get PPE
|--------------------------------------------------------------------------
*/
if ($action === 'get_ppe') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
jsonResponse(false, 'ID DPI non valido.');
}
$stmt = $pdo->prepare("
SELECT
id,
name,
description,
category,
photo,
standard_reference,
validity_months,
sort_order,
is_active
FROM ppe_items
WHERE id = ?
LIMIT 1
");
$stmt->execute([$id]);
$ppe = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ppe) {
jsonResponse(false, 'DPI non trovato.');
}
jsonResponse(true, '', [
'ppe' => $ppe,
]);
}
/*
|--------------------------------------------------------------------------
| Delete PPE
|--------------------------------------------------------------------------
*/
if ($action === 'delete_ppe') {
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
jsonResponse(false, 'ID DPI non valido.');
}
$stmt = $pdo->prepare("
SELECT photo
FROM ppe_items
WHERE id = ?
LIMIT 1
");
$stmt->execute([$id]);
$photo = $stmt->fetchColumn();
if ($photo === false) {
jsonResponse(false, 'DPI non trovato.');
}
$pdo->beginTransaction();
$stmt = $pdo->prepare("
DELETE FROM ppe_items
WHERE id = ?
");
$stmt->execute([$id]);
$pdo->commit();
deletePpePhotoIfExists($photo ?: null);
jsonResponse(true, 'DPI eliminato correttamente.');
}
jsonResponse(false, 'Azione non riconosciuta.');
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
jsonResponse(false, 'Errore: ' . $e->getMessage());
}
}
/*
|--------------------------------------------------------------------------
| Page data
|--------------------------------------------------------------------------
*/
$stmt = $pdo->prepare("
SELECT
id,
name,
description,
category,
photo,
standard_reference,
validity_months,
sort_order,
is_active,
created_at,
updated_at
FROM ppe_items
ORDER BY sort_order ASC, name ASC
");
$stmt->execute();
$ppeItems = $stmt->fetchAll(PDO::FETCH_ASSOC);
$totalPpe = count($ppeItems);
$totalActivePpe = 0;
$totalWithPhoto = 0;
$categories = [];
foreach ($ppeItems as $ppe) {
if ((int)$ppe['is_active'] === 1) {
$totalActivePpe++;
}
if (!empty($ppe['photo'])) {
$totalWithPhoto++;
}
if (!empty($ppe['category'])) {
$categories[$ppe['category']] = true;
}
}
$totalCategories = count($categories);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Gestione DPI - <?= htmlspecialchars($titlewebsite ?? '', ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.03rem;
background: #f8fafc;
}
.card {
border: 0;
border-radius: 18px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.page-title-box {
background: linear-gradient(135deg, #e8f1ff 0%, #ffffff 70%);
border: 1px solid #dbeafe;
border-radius: 18px;
padding: 18px 20px;
margin-bottom: 18px;
}
.page-title-box h4 {
color: #1f2d3d;
font-weight: 800;
}
.page-title-box p {
color: #64748b;
margin-bottom: 0;
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-main-action {
background: #2563eb;
color: #ffffff;
border: 1px solid #2563eb;
border-radius: 10px;
font-weight: 700;
padding: 10px 18px;
box-shadow: 0 5px 14px rgba(37, 99, 235, 0.22);
transition: all 0.2s ease-in-out;
}
.btn-main-action:hover {
color: #ffffff;
background: #1d4ed8;
transform: translateY(-2px);
}
.stat-card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 16px;
height: 100%;
}
.stat-label {
color: #64748b;
font-size: 0.87rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .04em;
}
.stat-number {
color: #0f172a;
font-size: 1.7rem;
font-weight: 800;
margin-top: 6px;
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
#tabPpeItems {
table-layout: fixed;
width: 100% !important;
}
#tabPpeItems th,
#tabPpeItems td {
vertical-align: middle;
}
#tabPpeItems th:nth-child(1),
#tabPpeItems td:nth-child(1) {
width: 75px;
text-align: center;
}
#tabPpeItems th:nth-child(2),
#tabPpeItems td:nth-child(2) {
width: 105px;
text-align: center;
}
#tabPpeItems th:nth-child(4),
#tabPpeItems td:nth-child(4) {
width: 150px;
text-align: center;
}
#tabPpeItems th:nth-child(5),
#tabPpeItems td:nth-child(5) {
width: 160px;
text-align: center;
}
#tabPpeItems th:nth-child(6),
#tabPpeItems td:nth-child(6) {
width: 110px;
text-align: center;
}
#tabPpeItems th:nth-child(7),
#tabPpeItems td:nth-child(7) {
width: 105px;
text-align: center;
}
#tabPpeItems th:nth-child(8),
#tabPpeItems td:nth-child(8) {
width: 230px;
text-align: center;
}
.ppe-name {
font-weight: 800;
color: #0f172a;
}
.ppe-description {
color: #64748b;
font-size: 0.92rem;
margin-top: 2px;
}
.ppe-thumb {
width: 74px;
height: 74px;
object-fit: cover;
border-radius: 14px;
border: 1px solid #dbeafe;
background: #f8fafc;
}
.ppe-no-photo {
width: 74px;
height: 74px;
border-radius: 14px;
border: 1px dashed #cbd5e1;
background: #f8fafc;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
font-weight: 700;
margin: 0 auto;
}
.badge-active {
background: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
font-weight: 700;
}
.badge-inactive {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fecaca;
font-weight: 700;
}
.badge-category {
background: #eff6ff;
color: #1e40af;
border: 1px solid #bfdbfe;
font-weight: 700;
}
.action-btn {
border-radius: 9px;
font-weight: 700;
}
.modal-content {
border: 0;
border-radius: 18px;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22);
}
.modal-header {
border-bottom: 1px solid #e2e8f0;
background: #f8fafc;
border-radius: 18px 18px 0 0;
}
.modal-title {
font-weight: 800;
color: #1f2d3d;
}
.form-label {
font-weight: 700;
color: #334155;
}
.form-control,
.form-select {
border-radius: 10px;
}
.photo-preview-box {
border: 1px dashed #cbd5e1;
background: #f8fafc;
border-radius: 16px;
padding: 14px;
min-height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.photo-preview-img {
max-width: 100%;
max-height: 220px;
border-radius: 14px;
border: 1px solid #e2e8f0;
display: none;
}
.photo-preview-empty {
color: #94a3b8;
font-weight: 700;
text-align: center;
}
.view-photo {
width: 100%;
max-height: 360px;
object-fit: contain;
border-radius: 16px;
border: 1px solid #e2e8f0;
background: #f8fafc;
}
.detail-label {
color: #64748b;
font-size: 0.82rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .04em;
}
.detail-value {
color: #0f172a;
font-weight: 700;
margin-bottom: 12px;
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="page-title-box d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h4 class="mb-1">Gestione DPI</h4>
<p>Anagrafica dei Dispositivi di Protezione Individuale, con foto, categorie, validità e standard di riferimento.</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn back-dashboard" onclick="history.back();">
↩️ Indietro
</button>
<button type="button" class="btn btn-main-action" id="btnOpenCreatePpe">
+ Nuovo DPI
</button>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-12 col-md-3">
<div class="stat-card">
<div class="stat-label">DPI totali</div>
<div class="stat-number"><?= (int)$totalPpe; ?></div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="stat-card">
<div class="stat-label">DPI attivi</div>
<div class="stat-number"><?= (int)$totalActivePpe; ?></div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="stat-card">
<div class="stat-label">Categorie</div>
<div class="stat-number"><?= (int)$totalCategories; ?></div>
</div>
</div>
<div class="col-12 col-md-3">
<div class="stat-card">
<div class="stat-label">Con foto</div>
<div class="stat-number"><?= (int)$totalWithPhoto; ?></div>
</div>
</div>
</div>
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5 class="mb-0">Elenco DPI</h5>
<div class="d-flex align-items-center gap-2 flex-wrap">
<label class="fw-semibold mb-0">Stato:</label>
<select id="filterStatus" class="form-select form-select-sm" style="width: 180px;">
<option value="">Tutti</option>
<option value="Attivo">Solo attivi</option>
<option value="Inattivo">Solo inattivi</option>
</select>
<label class="fw-semibold mb-0 ms-2">Categoria:</label>
<select id="filterCategory" class="form-select form-select-sm" style="width: 220px;">
<option value="">Tutte</option>
<?php foreach (array_keys($categories) as $category): ?>
<option value="<?= htmlspecialchars($category, ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($category, ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="tabPpeItems" class="table table-striped align-middle" style="width:100%;">
<thead>
<tr>
<th>ID</th>
<th>Foto</th>
<th>DPI</th>
<th>Categoria</th>
<th>Standard</th>
<th>Validità</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($ppeItems as $ppe): ?>
<?php
$ppeId = (int)$ppe['id'];
$isActive = (int)$ppe['is_active'] === 1;
$photo = $ppe['photo'] ?? null;
?>
<tr data-ppe-id="<?= $ppeId; ?>">
<td><?= $ppeId; ?></td>
<td>
<?php if (!empty($photo)): ?>
<img src="<?= htmlspecialchars($photo, ENT_QUOTES, 'UTF-8'); ?>"
class="ppe-thumb"
alt="<?= htmlspecialchars($ppe['name'], ENT_QUOTES, 'UTF-8'); ?>">
<?php else: ?>
<div class="ppe-no-photo">No foto</div>
<?php endif; ?>
</td>
<td>
<div class="ppe-name">
<?= htmlspecialchars($ppe['name'], ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php if (!empty($ppe['description'])): ?>
<div class="ppe-description">
<?= htmlspecialchars($ppe['description'], ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php else: ?>
<div class="ppe-description">
Nessuna descrizione
</div>
<?php endif; ?>
<div class="text-muted small mt-1">
Ordine: <?= (int)$ppe['sort_order']; ?>
</div>
</td>
<td>
<?php if (!empty($ppe['category'])): ?>
<span class="badge badge-category">
<?= htmlspecialchars($ppe['category'], ENT_QUOTES, 'UTF-8'); ?>
</span>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td>
<?= !empty($ppe['standard_reference'])
? htmlspecialchars($ppe['standard_reference'], ENT_QUOTES, 'UTF-8')
: '<span class="text-muted">-</span>'; ?>
</td>
<td>
<?php if (!empty($ppe['validity_months'])): ?>
<?= (int)$ppe['validity_months']; ?> mesi
<?php else: ?>
<span class="text-muted">Nessuna</span>
<?php endif; ?>
</td>
<td>
<?php if ($isActive): ?>
<span class="badge badge-active">Attivo</span>
<?php else: ?>
<span class="badge badge-inactive">Inattivo</span>
<?php endif; ?>
</td>
<td>
<div class="d-flex justify-content-center flex-wrap gap-1">
<button type="button"
class="btn btn-sm btn-outline-primary action-btn btnViewPpe"
data-ppe-id="<?= $ppeId; ?>">
Visualizza
</button>
<button type="button"
class="btn btn-sm btn-outline-warning action-btn btnEditPpe"
data-ppe-id="<?= $ppeId; ?>">
Edita
</button>
<button type="button"
class="btn btn-sm btn-outline-danger action-btn btnDeletePpe"
data-ppe-id="<?= $ppeId; ?>"
data-ppe-name="<?= htmlspecialchars($ppe['name'], ENT_QUOTES, 'UTF-8'); ?>">
Cancella
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$ppeItems): ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="text-muted small mt-3">
Le foto vengono salvate nella cartella <strong><?= PPE_UPLOAD_RELATIVE_DIR; ?></strong>. Nel database viene salvato solo il percorso relativo.
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<!-- PPE FORM MODAL -->
<div class="modal fade" id="ppeModal" tabindex="-1" aria-labelledby="ppeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<form id="ppeForm" class="modal-content" enctype="multipart/form-data">
<div class="modal-header">
<h5 class="modal-title" id="ppeModalLabel">Nuovo DPI</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<input type="hidden" name="action" value="save_ppe">
<input type="hidden" name="id" id="ppe_id">
<input type="hidden" name="remove_photo" id="ppe_remove_photo" value="0">
<div class="row g-4">
<div class="col-12 col-lg-8">
<div class="row g-3">
<div class="col-12 col-md-8">
<label class="form-label" for="ppe_name">Nome DPI *</label>
<input type="text" class="form-control" name="name" id="ppe_name" required maxlength="255">
</div>
<div class="col-12 col-md-4">
<label class="form-label" for="ppe_category">Categoria</label>
<input type="text" class="form-control" name="category" id="ppe_category" maxlength="100" list="ppeCategoryList">
<datalist id="ppeCategoryList">
<option value="Head">
<option value="Eyes">
<option value="Hands">
<option value="Feet">
<option value="Respiratory">
<option value="Hearing">
<option value="Body">
<option value="Fall protection">
<?php foreach (array_keys($categories) as $category): ?>
<option value="<?= htmlspecialchars($category, ENT_QUOTES, 'UTF-8'); ?>">
<?php endforeach; ?>
</datalist>
</div>
<div class="col-12 col-md-5">
<label class="form-label" for="ppe_standard_reference">Standard di riferimento</label>
<input type="text" class="form-control" name="standard_reference" id="ppe_standard_reference" maxlength="255" placeholder="Es. EN ISO 20345">
</div>
<div class="col-12 col-md-3">
<label class="form-label" for="ppe_validity_months">Validità mesi</label>
<input type="number" class="form-control" name="validity_months" id="ppe_validity_months" min="0" placeholder="Es. 12">
</div>
<div class="col-12 col-md-2">
<label class="form-label" for="ppe_sort_order">Ordine</label>
<input type="number" class="form-control" name="sort_order" id="ppe_sort_order" value="999" min="0">
</div>
<div class="col-12 col-md-2">
<label class="form-label" for="ppe_is_active">Stato</label>
<select class="form-select" name="is_active" id="ppe_is_active">
<option value="1">Attivo</option>
<option value="0">Inattivo</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="ppe_description">Descrizione</label>
<textarea class="form-control" name="description" id="ppe_description" rows="5"></textarea>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<label class="form-label" for="ppe_photo">Foto DPI</label>
<div class="photo-preview-box mb-3">
<img src="" id="ppe_photo_preview" class="photo-preview-img" alt="Anteprima DPI">
<div id="ppe_photo_empty" class="photo-preview-empty">
Nessuna foto selezionata
</div>
</div>
<input type="file" class="form-control" name="photo" id="ppe_photo" accept="image/jpeg,image/png,image/webp">
<div class="text-muted small mt-2">
Formati ammessi: JPG, PNG, WEBP. Dimensione massima: 5 MB.
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3 d-none" id="btnRemovePhoto">
Rimuovi foto
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">Salva DPI</button>
</div>
</form>
</div>
</div>
<!-- PPE VIEW MODAL -->
<div class="modal fade" id="ppeViewModal" tabindex="-1" aria-labelledby="ppeViewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title" id="ppeViewModalLabel">Dettaglio DPI</h5>
<div class="text-muted small" id="ppeViewSubtitle"></div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
</div>
<div class="modal-body">
<div class="row g-4">
<div class="col-12 col-lg-5">
<div id="ppeViewPhotoBox"></div>
</div>
<div class="col-12 col-lg-7">
<div class="detail-label">Nome</div>
<div class="detail-value" id="ppeViewName"></div>
<div class="row">
<div class="col-12 col-md-6">
<div class="detail-label">Categoria</div>
<div class="detail-value" id="ppeViewCategory"></div>
</div>
<div class="col-12 col-md-6">
<div class="detail-label">Stato</div>
<div class="detail-value" id="ppeViewStatus"></div>
</div>
<div class="col-12 col-md-6">
<div class="detail-label">Standard</div>
<div class="detail-value" id="ppeViewStandard"></div>
</div>
<div class="col-12 col-md-6">
<div class="detail-label">Validità</div>
<div class="detail-value" id="ppeViewValidity"></div>
</div>
<div class="col-12 col-md-6">
<div class="detail-label">Ordine</div>
<div class="detail-value" id="ppeViewSortOrder"></div>
</div>
</div>
<div class="detail-label">Descrizione</div>
<div class="detail-value" id="ppeViewDescription"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" id="btnEditFromView">
Edita
</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal">
Chiudi
</button>
</div>
</div>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
let dt;
let ppeModal;
let ppeViewModal;
let currentViewPpeId = null;
function htmlEscape(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function showSuccess(message) {
Swal.fire({
icon: 'success',
title: message,
timer: 1100,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
}
function showError(message) {
Swal.fire({
icon: 'error',
title: 'Errore',
text: message || 'Operazione non riuscita'
});
}
function ajaxPost(data) {
return fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(data)
})
.then(async response => {
const text = await response.text();
try {
return JSON.parse(text);
} catch (e) {
throw new Error('Risposta non JSON: ' + text);
}
});
}
function ajaxForm(form) {
const formData = new FormData(form);
return fetch(window.location.href, {
method: 'POST',
body: formData
})
.then(async response => {
const text = await response.text();
try {
return JSON.parse(text);
} catch (e) {
throw new Error('Risposta non JSON: ' + text);
}
});
}
function resetPpeForm() {
$('#ppeForm')[0].reset();
$('#ppe_id').val('');
$('#ppe_remove_photo').val('0');
$('#ppe_sort_order').val('999');
$('#ppe_is_active').val('1');
$('#ppe_photo_preview').attr('src', '').hide();
$('#ppe_photo_empty').show().text('Nessuna foto selezionata');
$('#btnRemovePhoto').addClass('d-none');
}
function setPhotoPreview(photoPath) {
if (photoPath) {
$('#ppe_photo_preview').attr('src', photoPath).show();
$('#ppe_photo_empty').hide();
$('#btnRemovePhoto').removeClass('d-none');
} else {
$('#ppe_photo_preview').attr('src', '').hide();
$('#ppe_photo_empty').show().text('Nessuna foto selezionata');
$('#btnRemovePhoto').addClass('d-none');
}
}
function loadPpeIntoForm(ppeId, openAfterLoad = true) {
return ajaxPost({
action: 'get_ppe',
id: ppeId
})
.then(data => {
if (!data.success) {
showError(data.message);
return false;
}
resetPpeForm();
$('#ppeModalLabel').text('Modifica DPI');
$('#ppe_id').val(data.ppe.id);
$('#ppe_name').val(data.ppe.name);
$('#ppe_description').val(data.ppe.description || '');
$('#ppe_category').val(data.ppe.category || '');
$('#ppe_standard_reference').val(data.ppe.standard_reference || '');
$('#ppe_validity_months').val(data.ppe.validity_months || '');
$('#ppe_sort_order').val(data.ppe.sort_order || 999);
$('#ppe_is_active').val(data.ppe.is_active);
setPhotoPreview(data.ppe.photo || '');
if (openAfterLoad) {
ppeModal.show();
}
return true;
});
}
$(document).ready(function() {
ppeModal = new bootstrap.Modal(document.getElementById('ppeModal'));
ppeViewModal = new bootstrap.Modal(document.getElementById('ppeViewModal'));
dt = $('#tabPpeItems').DataTable({
order: [
[6, 'asc'],
[2, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
$('#filterStatus').on('change', function() {
dt.column(6).search($(this).val()).draw();
});
$('#filterCategory').on('change', function() {
dt.column(3).search($(this).val()).draw();
});
});
/*
|--------------------------------------------------------------------------
| Create PPE
|--------------------------------------------------------------------------
*/
$(document).on('click', '#btnOpenCreatePpe', function() {
resetPpeForm();
$('#ppeModalLabel').text('Nuovo DPI');
ppeModal.show();
});
/*
|--------------------------------------------------------------------------
| Edit PPE
|--------------------------------------------------------------------------
*/
$(document).on('click', '.btnEditPpe', function() {
const ppeId = $(this).data('ppe-id');
loadPpeIntoForm(ppeId).catch(err => {
showError(err.message);
});
});
/*
|--------------------------------------------------------------------------
| Save PPE
|--------------------------------------------------------------------------
*/
$('#ppeForm').on('submit', function(e) {
e.preventDefault();
ajaxForm(this)
.then(data => {
if (data.success) {
showSuccess(data.message);
} else {
showError(data.message);
}
})
.catch(err => {
showError(err.message);
});
});
/*
|--------------------------------------------------------------------------
| Delete PPE
|--------------------------------------------------------------------------
*/
$(document).on('click', '.btnDeletePpe', function() {
const ppeId = $(this).data('ppe-id');
const ppeName = $(this).data('ppe-name');
Swal.fire({
icon: 'warning',
title: 'Confermi la cancellazione?',
html: `Il DPI <strong>${htmlEscape(ppeName)}</strong> verrà eliminato definitivamente.<br>Se è già collegato ad assegnazioni o sottomansioni, il database potrebbe bloccare la cancellazione.`,
showCancelButton: true,
confirmButtonText: 'Sì, cancella',
cancelButtonText: 'Annulla',
confirmButtonColor: '#dc2626'
}).then(result => {
if (!result.isConfirmed) {
return;
}
ajaxPost({
action: 'delete_ppe',
id: ppeId
})
.then(data => {
if (data.success) {
showSuccess(data.message);
} else {
showError(data.message);
}
})
.catch(err => {
showError(err.message);
});
});
});
/*
|--------------------------------------------------------------------------
| View PPE
|--------------------------------------------------------------------------
*/
$(document).on('click', '.btnViewPpe', function() {
const ppeId = $(this).data('ppe-id');
currentViewPpeId = ppeId;
ajaxPost({
action: 'get_ppe',
id: ppeId
})
.then(data => {
if (!data.success) {
showError(data.message);
return;
}
const ppe = data.ppe;
$('#ppeViewModalLabel').text('Dettaglio DPI');
$('#ppeViewSubtitle').text(ppe.name || '');
$('#ppeViewName').text(ppe.name || '-');
$('#ppeViewCategory').html(ppe.category ? `<span class="badge badge-category">${htmlEscape(ppe.category)}</span>` : '<span class="text-muted">-</span>');
$('#ppeViewStandard').text(ppe.standard_reference || '-');
$('#ppeViewValidity').text(ppe.validity_months ? `${ppe.validity_months} mesi` : 'Nessuna validità impostata');
$('#ppeViewSortOrder').text(ppe.sort_order || '999');
$('#ppeViewDescription').text(ppe.description || 'Nessuna descrizione');
if (String(ppe.is_active) === '1') {
$('#ppeViewStatus').html('<span class="badge badge-active">Attivo</span>');
} else {
$('#ppeViewStatus').html('<span class="badge badge-inactive">Inattivo</span>');
}
if (ppe.photo) {
$('#ppeViewPhotoBox').html(`
<img src="${htmlEscape(ppe.photo)}" class="view-photo" alt="${htmlEscape(ppe.name)}">
`);
} else {
$('#ppeViewPhotoBox').html(`
<div class="photo-preview-box" style="min-height:260px;">
<div class="photo-preview-empty">Nessuna foto disponibile</div>
</div>
`);
}
ppeViewModal.show();
})
.catch(err => {
showError(err.message);
});
});
/*
|--------------------------------------------------------------------------
| Edit from view modal
|--------------------------------------------------------------------------
*/
$(document).on('click', '#btnEditFromView', function() {
if (!currentViewPpeId) {
return;
}
ppeViewModal.hide();
setTimeout(function() {
loadPpeIntoForm(currentViewPpeId).catch(err => {
showError(err.message);
});
}, 250);
});
/*
|--------------------------------------------------------------------------
| Photo preview
|--------------------------------------------------------------------------
*/
$('#ppe_photo').on('change', function() {
const file = this.files && this.files[0] ? this.files[0] : null;
if (!file) {
return;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
showError('Formato non consentito. Usa JPG, PNG oppure WEBP.');
$(this).val('');
return;
}
if (file.size > 5242880) {
showError('La foto supera la dimensione massima consentita di 5 MB.');
$(this).val('');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
$('#ppe_remove_photo').val('0');
$('#ppe_photo_preview').attr('src', e.target.result).show();
$('#ppe_photo_empty').hide();
$('#btnRemovePhoto').removeClass('d-none');
};
reader.readAsDataURL(file);
});
/*
|--------------------------------------------------------------------------
| Remove photo
|--------------------------------------------------------------------------
*/
$('#btnRemovePhoto').on('click', function() {
$('#ppe_photo').val('');
$('#ppe_remove_photo').val('1');
$('#ppe_photo_preview').attr('src', '').hide();
$('#ppe_photo_empty').show().text('Foto rimossa. Verrà eliminata al salvataggio.');
$('#btnRemovePhoto').addClass('d-none');
});
</script>
</body>
</html>