maintaince module

This commit is contained in:
2026-08-12 21:29:55 +03:00
parent 41c4838dab
commit 55d83c12b0
51 changed files with 8200 additions and 48 deletions
@@ -0,0 +1,82 @@
<?php
/**
* Auth + permission guard for the Manutenzioni ajax endpoints.
* Include at the top of every handler; it defines $currentUserId.
*
* Authorisation is enforced here, on the backend — the UI only hides
* buttons, it never decides what a request is allowed to do.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
if (empty($_SESSION['iduserlogin'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
exit;
}
$currentUserId = (int)$_SESSION['iduserlogin'];
require_once __DIR__ . '/../include/functions.php';
if (!function_exists('mnt_user_can')) {
/**
* Permission check straight from the Vanguard RBAC tables
* (the Auth facade is not bootstrapped in these endpoints).
*/
function mnt_user_can(string $permission): bool
{
global $currentUserId;
static $permissions = null;
if ($permissions === null) {
$stmt = mnt_pdo()->prepare("
SELECT p.name
FROM auth_users u
INNER JOIN auth_permission_role pr ON pr.role_id = u.role_id
INNER JOIN auth_permissions p ON p.id = pr.permission_id
WHERE u.id = ?
");
$stmt->execute([$currentUserId]);
$permissions = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
return in_array($permission, $permissions, true);
}
}
if (!function_exists('mnt_require_permission')) {
function mnt_require_permission(string $permission): void
{
if (!mnt_user_can($permission)) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Permesso negato.']);
exit;
}
}
}
if (!function_exists('mnt_json_fail')) {
function mnt_json_fail(string $message, int $code = 200): void
{
if ($code !== 200) {
http_response_code($code);
}
echo json_encode(['success' => false, 'message' => $message]);
exit;
}
}
if (!function_exists('mnt_json_ok')) {
function mnt_json_ok(array $payload = []): void
{
echo json_encode(['success' => true] + $payload);
exit;
}
}
@@ -0,0 +1,31 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
// The FK is ON DELETE SET NULL, so a delete would silently orphan items:
// refuse while the category is still in use.
$stmt = $pdo->prepare("SELECT COUNT(*) FROM inv_equipment WHERE category_id = ?");
$stmt->execute([$id]);
$inUse = (int)$stmt->fetchColumn();
if ($inUse > 0) {
mnt_json_fail('Impossibile eliminare: la categoria è usata da ' . $inUse . ' attrezzature.');
}
$pdo->prepare("DELETE FROM inv_categories WHERE id = ?")->execute([$id]);
mnt_json_ok();
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("SELECT id, name FROM inv_equipment WHERE id = ?");
$stmt->execute([$id]);
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$equipment) {
mnt_json_fail('Attrezzatura non trovata.');
}
// Legacy production records reference tools without ON DELETE CASCADE:
// refuse with a clear message instead of letting the FK throw.
$stmt = $pdo->prepare("SELECT COUNT(*) FROM productiondata_tools WHERE tool_id = ?");
$stmt->execute([$id]);
$usedInProduction = (int)$stmt->fetchColumn();
if ($usedInProduction > 0) {
mnt_json_fail(
'Impossibile eliminare: l\'attrezzatura è usata in ' . $usedInProduction .
' registrazioni di produzione. Impostala su "Dismesso" per escluderla dalle pianificazioni.'
);
}
// Collect stored files before the cascade removes their rows
$storedNames = [];
$stmt = $pdo->prepare("SELECT stored_name FROM inv_equipment_files WHERE equipment_id = ?");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
$stmt = $pdo->prepare("
SELECT f.stored_name
FROM maint_maintenance_files f
INNER JOIN maint_maintenances m ON m.id = f.maintenance_id
WHERE m.equipment_id = ?
");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
$stmt = $pdo->prepare("
SELECT f.stored_name
FROM maint_intervention_files f
INNER JOIN maint_interventions i ON i.id = f.intervention_id
WHERE i.equipment_id = ?
");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE equipment_id = ? AND signature_path IS NOT NULL");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
// cover_file_id points at inv_equipment_files, which cascades from the
// equipment row: clear it first so the FK does not block the delete.
$pdo->beginTransaction();
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE id = ?")->execute([$id]);
$pdo->prepare("DELETE FROM inv_equipment WHERE id = ?")->execute([$id]);
$pdo->commit();
foreach (array_filter($storedNames) as $storedName) {
mnt_delete_stored_file($storedName);
}
mnt_log($pdo, 'equipment_deleted', null, null, null, $currentUserId, null, $equipment['name']);
mnt_json_ok();
} catch (Throwable $e) {
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,56 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$scope = (string)($_POST['scope'] ?? 'equipment');
$id = (int)($_POST['id'] ?? 0);
$tables = [
'equipment' => 'inv_equipment_files',
'maintenance' => 'maint_maintenance_files',
'intervention' => 'maint_intervention_files',
];
if (!isset($tables[$scope])) {
mnt_json_fail('Scope non valido.');
}
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("SELECT * FROM {$tables[$scope]} WHERE id = ?");
$stmt->execute([$id]);
$file = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$file) {
mnt_json_fail('File non trovato.');
}
// Clear the cover reference first — the FK would block the delete
if ($scope === 'equipment') {
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = NULL WHERE cover_file_id = ?")->execute([$id]);
}
$pdo->prepare("DELETE FROM {$tables[$scope]} WHERE id = ?")->execute([$id]);
mnt_delete_stored_file((string)$file['stored_name']);
mnt_log(
$pdo,
'file_deleted',
$scope === 'equipment' ? (int)$file['equipment_id'] : null,
$scope === 'maintenance' ? (int)$file['maintenance_id'] : null,
$scope === 'intervention' ? (int)$file['intervention_id'] : null,
$currentUserId,
null,
(string)$file['original_name']
);
mnt_json_ok();
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
$stmt->execute([$id]);
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$intervention) {
mnt_json_fail('Intervento non trovato.');
}
$stmt = $pdo->prepare("SELECT stored_name FROM maint_intervention_files WHERE intervention_id = ?");
$stmt->execute([$id]);
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!empty($intervention['signature_path'])) {
$storedNames[] = $intervention['signature_path'];
}
$pdo->prepare("DELETE FROM maint_interventions WHERE id = ?")->execute([$id]);
foreach (array_filter($storedNames) as $storedName) {
mnt_delete_stored_file($storedName);
}
// Removing the last completed intervention must roll the due date back
mnt_recalc_maintenance($pdo, (int)$intervention['maintenance_id']);
mnt_log(
$pdo,
'intervention_deleted',
(int)$intervention['equipment_id'],
(int)$intervention['maintenance_id'],
null,
$currentUserId,
null,
'Intervento del ' . $intervention['performed_at']
);
mnt_json_ok();
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("SELECT id, equipment_id, title FROM maint_maintenances WHERE id = ?");
$stmt->execute([$id]);
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$maintenance) {
mnt_json_fail('Manutenzione non trovata.');
}
// Files of the maintenance and of its interventions (both cascade in DB)
$stmt = $pdo->prepare("SELECT stored_name FROM maint_maintenance_files WHERE maintenance_id = ?");
$stmt->execute([$id]);
$storedNames = $stmt->fetchAll(PDO::FETCH_COLUMN);
$stmt = $pdo->prepare("
SELECT f.stored_name
FROM maint_intervention_files f
INNER JOIN maint_interventions i ON i.id = f.intervention_id
WHERE i.maintenance_id = ?
");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
$stmt = $pdo->prepare("SELECT signature_path FROM maint_interventions WHERE maintenance_id = ? AND signature_path IS NOT NULL");
$stmt->execute([$id]);
$storedNames = array_merge($storedNames, $stmt->fetchAll(PDO::FETCH_COLUMN));
$pdo->prepare("DELETE FROM maint_maintenances WHERE id = ?")->execute([$id]);
foreach (array_filter($storedNames) as $storedName) {
mnt_delete_stored_file($storedName);
}
mnt_log($pdo, 'maintenance_deleted', (int)$maintenance['equipment_id'], null, null, $currentUserId, null, $maintenance['title']);
mnt_json_ok();
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,72 @@
<?php
/**
* Serves an attachment. The attachments/ folder itself is closed by
* .htaccess — this is the only way to read a file, and it requires a
* logged-in session.
*
* scope = equipment | maintenance | intervention | signature
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['iduserlogin'])) {
http_response_code(401);
exit('Non autorizzato.');
}
require_once __DIR__ . '/../include/functions.php';
$scope = (string)($_GET['scope'] ?? 'equipment');
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
exit('ID non valido.');
}
$tables = [
'equipment' => 'inv_equipment_files',
'maintenance' => 'maint_maintenance_files',
'intervention' => 'maint_intervention_files',
];
$pdo = mnt_pdo();
if ($scope === 'signature') {
$stmt = $pdo->prepare("SELECT signature_path AS stored_name, 'firma.png' AS original_name, 'image/png' AS mime_type FROM maint_interventions WHERE id = ?");
$stmt->execute([$id]);
} elseif (isset($tables[$scope])) {
$stmt = $pdo->prepare("SELECT stored_name, original_name, mime_type FROM {$tables[$scope]} WHERE id = ?");
$stmt->execute([$id]);
} else {
http_response_code(400);
exit('Scope non valido.');
}
$file = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$file || empty($file['stored_name'])) {
http_response_code(404);
exit('File non trovato.');
}
// basename() keeps a crafted stored_name from escaping the folder
$path = __DIR__ . '/../attachments/' . basename((string)$file['stored_name']);
if (!is_file($path)) {
http_response_code(404);
exit('File non presente sul server.');
}
$mimeType = $file['mime_type'] ?: 'application/octet-stream';
$disposition = isset($_GET['download']) ? 'attachment' : 'inline';
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . filesize($path));
header('Content-Disposition: ' . $disposition . '; filename="' . rawurlencode((string)$file['original_name']) . '"');
header('X-Content-Type-Options: nosniff');
readfile($path);
@@ -0,0 +1,80 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.calendar.view');
/**
* FullCalendar feed for maintenance due dates.
* Optional filters: type=scheduled|extraordinary, category=<id>
*/
try {
$pdo = mnt_pdo();
$start = (string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months')));
$end = (string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months')));
$type = (string)($_GET['type'] ?? '');
$categoryId = (int)($_GET['category'] ?? 0);
// Le attrezzature dismesse o fuori servizio restano fuori dalle viste di
// pianificazione: è a questo che serve lo stato (capitolato §1).
$where = [
"m.is_active = 1",
"e.status = 'active'",
"m.next_due_date IS NOT NULL",
"m.next_due_date BETWEEN ? AND ?",
];
$params = [substr($start, 0, 10), substr($end, 0, 10)];
if (array_key_exists($type, mnt_intervention_types())) {
$where[] = "m.intervention_type = ?";
$params[] = $type;
}
if ($categoryId > 0) {
$where[] = "e.category_id = ?";
$params[] = $categoryId;
}
$stmt = $pdo->prepare("
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical, m.intervention_type,
e.id AS equipment_id, e.name AS equipment_name,
c.color AS category_color
FROM maint_maintenances m
INNER JOIN inv_equipment e ON e.id = m.equipment_id
LEFT JOIN inv_categories c ON c.id = e.category_id
WHERE " . implode(' AND ', $where) . "
ORDER BY m.next_due_date ASC
");
$stmt->execute($params);
$events = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
// Overdue/soon override the category colour so urgency wins visually
$color = $state === 'overdue' ? '#dc3545' : ($state === 'due_soon' ? '#f0a202' : ($row['category_color'] ?: '#2f7d8f'));
$events[] = [
'id' => (int)$row['id'],
'title' => ($row['code'] ? $row['code'] . ' — ' : '') . $row['equipment_name'] . ': ' . $row['title'],
'start' => $row['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'extendedProps' => [
'equipmentId' => (int)$row['equipment_id'],
'equipmentName' => $row['equipment_name'],
'maintenanceTitle' => $row['title'],
'isCritical' => (int)$row['is_critical'] === 1,
'state' => $state,
'type' => $row['intervention_type'],
],
];
}
echo json_encode($events);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,25 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.equipment.view');
try {
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = mnt_pdo()->prepare("SELECT * FROM inv_equipment WHERE id = ?");
$stmt->execute([$id]);
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$equipment) {
mnt_json_fail('Attrezzatura non trovata.');
}
mnt_json_ok(['equipment' => $equipment]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,122 @@
<?php
require_once __DIR__ . '/auth_check.php';
/**
* Cumulative calendar feed: deadlines + trainings + maintenances.
* Each layer is returned only if the user may see that module, and only
* if the layer is requested via ?layers=deadlines,trainings,maintenances
*/
try {
$pdo = mnt_pdo();
$start = substr((string)($_GET['start'] ?? date('Y-m-01', strtotime('-6 months'))), 0, 10);
$end = substr((string)($_GET['end'] ?? date('Y-m-t', strtotime('+12 months'))), 0, 10);
$requested = array_filter(explode(',', (string)($_GET['layers'] ?? 'deadlines,trainings,maintenances')));
$wants = fn(string $layer): bool => in_array($layer, $requested, true);
$events = [];
// ---- Maintenances --------------------------------------------------
if ($wants('maintenances') && mnt_user_can('maintenance.calendar.view')) {
$stmt = $pdo->prepare("
SELECT m.id, m.title, m.code, m.next_due_date, m.alert_days, m.is_critical,
e.id AS equipment_id, e.name AS equipment_name
FROM maint_maintenances m
INNER JOIN inv_equipment e ON e.id = m.equipment_id
WHERE m.is_active = 1
AND e.status = 'active'
AND m.next_due_date BETWEEN ? AND ?
");
$stmt->execute([$start, $end]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$state = mnt_due_state($row['next_due_date'], (int)$row['alert_days']);
$color = $state === 'overdue' ? '#dc3545' : '#2f7d8f';
$events[] = [
'id' => 'maint-' . $row['id'],
'title' => '🔧 ' . $row['equipment_name'] . ': ' . $row['title'],
'start' => $row['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'extendedProps' => [
'layer' => 'maintenances',
'url' => 'manutenzioni/equipment.php?id=' . (int)$row['equipment_id'],
'state' => $state,
],
];
}
}
// ---- Deadlines (scadenzario) --------------------------------------
if ($wants('deadlines') && mnt_user_can('deadlines.view')) {
$stmt = $pdo->prepare("
SELECT d.id, d.topic, d.due_date, d.status, d.notification_days, s.name AS subject_name
FROM scad_deadlines d
LEFT JOIN scad_subjects s ON s.id = d.subject_id
WHERE d.due_date BETWEEN ? AND ?
");
$stmt->execute([$start, $end]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$isCompleted = $row['status'] === 'completed';
$state = $isCompleted ? 'ok' : mnt_due_state($row['due_date'], (int)$row['notification_days']);
$color = $isCompleted ? '#6c757d' : ($state === 'overdue' ? '#b02a37' : '#6f42c1');
$events[] = [
'id' => 'scad-' . $row['id'],
'title' => '📅 ' . $row['topic'],
'start' => $row['due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'extendedProps' => [
'layer' => 'deadlines',
'url' => 'scadenzario/detail.php?id=' . (int)$row['id'],
'state' => $state,
],
];
}
}
// ---- Trainings (formazione) ---------------------------------------
if ($wants('trainings') && mnt_user_can('hr.trainings.view')) {
$stmt = $pdo->prepare("
SELECT t.id, t.next_due_date, t.reminder_days,
tt.name AS topic_name,
CONCAT(e.first_name, ' ', e.last_name) AS employee_name
FROM employee_trainings t
INNER JOIN training_topics tt ON tt.id = t.training_topic_id
INNER JOIN employees e ON e.id = t.employee_id
WHERE t.next_due_date IS NOT NULL AND t.next_due_date BETWEEN ? AND ?
");
$stmt->execute([$start, $end]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$state = mnt_due_state($row['next_due_date'], (int)($row['reminder_days'] ?? 0));
$color = $state === 'overdue' ? '#a94442' : '#198754';
$events[] = [
'id' => 'train-' . $row['id'],
'title' => '🎓 ' . $row['topic_name'] . ' — ' . $row['employee_name'],
'start' => $row['next_due_date'],
'allDay' => true,
'backgroundColor' => $color,
'borderColor' => $color,
'extendedProps' => [
'layer' => 'trainings',
'url' => 'trainings.php',
'state' => $state,
],
];
}
}
echo json_encode($events);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
@@ -0,0 +1,51 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.maintenances.view');
try {
$pdo = mnt_pdo();
$maintenanceId = (int)($_GET['maintenance_id'] ?? 0);
$interventionId = (int)($_GET['id'] ?? 0);
if ($interventionId > 0) {
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
$stmt->execute([$interventionId]);
$intervention = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$intervention) {
mnt_json_fail('Intervento non trovato.');
}
$stmt = $pdo->prepare("SELECT id, original_name FROM maint_intervention_files WHERE intervention_id = ?");
$stmt->execute([$interventionId]);
mnt_json_ok([
'intervention' => $intervention,
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
]);
}
if ($maintenanceId <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("
SELECT i.*,
CONCAT(e.first_name, ' ', e.last_name) AS operator_full_name,
s.supplier_name,
(SELECT COUNT(*) FROM maint_intervention_files f WHERE f.intervention_id = i.id) AS file_count
FROM maint_interventions i
LEFT JOIN employees e ON e.id = i.operator_employee_id
LEFT JOIN suppliers s ON s.idsupplier = i.supplier_id
WHERE i.maintenance_id = ?
ORDER BY i.performed_at DESC, i.id DESC
");
$stmt->execute([$maintenanceId]);
mnt_json_ok(['interventions' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.maintenances.view');
try {
$pdo = mnt_pdo();
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("
SELECT m.*, e.name AS equipment_name
FROM maint_maintenances m
INNER JOIN inv_equipment e ON e.id = m.equipment_id
WHERE m.id = ?
");
$stmt->execute([$id]);
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$maintenance) {
mnt_json_fail('Manutenzione non trovata.');
}
$stmt = $pdo->prepare("
SELECT id, original_name, size
FROM maint_maintenance_files
WHERE maintenance_id = ?
ORDER BY created_at ASC
");
$stmt->execute([$id]);
mnt_json_ok([
'maintenance' => $maintenance,
'files' => $stmt->fetchAll(PDO::FETCH_ASSOC),
]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
$name = trim((string)($_POST['name'] ?? ''));
$code = trim((string)($_POST['code'] ?? ''));
$color = trim((string)($_POST['color'] ?? '#6c757d'));
$requiresLine = !empty($_POST['requires_line']) ? 1 : 0;
$sortOrder = (int)($_POST['sort_order'] ?? 999);
$isActive = !empty($_POST['is_active']) ? 1 : 0;
$description = trim((string)($_POST['description'] ?? ''));
if ($name === '') {
mnt_json_fail('Il nome è obbligatorio.');
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
$color = '#6c757d';
}
// name is UNIQUE — check first so the user gets a readable message
$stmt = $pdo->prepare("SELECT id FROM inv_categories WHERE name = ? AND id <> ?");
$stmt->execute([$name, $id]);
if ($stmt->fetchColumn()) {
mnt_json_fail('Esiste già una categoria con questo nome.');
}
if ($id > 0) {
$stmt = $pdo->prepare("
UPDATE inv_categories
SET name = ?, code = ?, color = ?, requires_line = ?, sort_order = ?, is_active = ?, description = ?
WHERE id = ?
");
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null, $id]);
mnt_json_ok(['id' => $id]);
}
$stmt = $pdo->prepare("
INSERT INTO inv_categories (name, code, color, requires_line, sort_order, is_active, description)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$name, $code ?: null, $color, $requiresLine, $sortOrder, $isActive, $description ?: null]);
mnt_json_ok(['id' => (int)$pdo->lastInsertId()]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,95 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
$name = trim((string)($_POST['name'] ?? ''));
$categoryId = (int)($_POST['category_id'] ?? 0);
$lineId = (int)($_POST['line_id'] ?? 0);
$registration = trim((string)($_POST['registration_number'] ?? ''));
$serial = trim((string)($_POST['serial_number'] ?? ''));
$batchLot = trim((string)($_POST['batch_lot'] ?? ''));
$manufacturer = trim((string)($_POST['manufacturer'] ?? ''));
$purchaseDate = trim((string)($_POST['purchase_date'] ?? ''));
$commissioningDate = trim((string)($_POST['commissioning_date'] ?? ''));
$toolType = trim((string)($_POST['tool_type'] ?? ''));
$status = (string)($_POST['status'] ?? 'active');
$departmentId = (int)($_POST['department_id'] ?? 0);
$location = trim((string)($_POST['location'] ?? ''));
$description = trim((string)($_POST['description'] ?? ''));
$notes = trim((string)($_POST['notes'] ?? ''));
if ($name === '') {
mnt_json_fail('Il nome è obbligatorio.');
}
if ($categoryId <= 0) {
mnt_json_fail('La categoria è obbligatoria.');
}
if (!array_key_exists($status, mnt_equipment_statuses())) {
mnt_json_fail('Stato non valido.');
}
// A category flagged requires_line must carry a production line
$stmt = $pdo->prepare("SELECT requires_line FROM inv_categories WHERE id = ?");
$stmt->execute([$categoryId]);
$requiresLine = $stmt->fetchColumn();
if ($requiresLine === false) {
mnt_json_fail('Categoria inesistente.');
}
if ((int)$requiresLine === 1 && $lineId <= 0) {
mnt_json_fail('Per questa categoria la linea di produzione è obbligatoria.');
}
$values = [
'name' => $name,
'category_id' => $categoryId,
'line_id' => $lineId ?: null,
'registration_number' => $registration ?: null,
'serial_number' => $serial ?: null,
'batch_lot' => $batchLot ?: null,
'manufacturer' => $manufacturer ?: null,
'purchase_date' => $purchaseDate ?: null,
'commissioning_date' => $commissioningDate ?: null,
'tool_type' => $toolType ?: null,
'status' => $status,
'department_id' => $departmentId ?: null,
'location' => $location ?: null,
'description' => $description ?: null,
'notes' => $notes ?: null,
];
if ($id > 0) {
$assignments = [];
foreach (array_keys($values) as $column) {
$assignments[] = "$column = :$column";
}
$stmt = $pdo->prepare("UPDATE inv_equipment SET " . implode(', ', $assignments) . " WHERE id = :id");
$stmt->execute($values + ['id' => $id]);
mnt_log($pdo, 'equipment_updated', $id, null, null, $currentUserId, $values);
mnt_json_ok(['id' => $id]);
}
$columns = array_keys($values);
$placeholders = array_map(fn($column) => ':' . $column, $columns);
$stmt = $pdo->prepare(
"INSERT INTO inv_equipment (" . implode(', ', $columns) . ", created_by)
VALUES (" . implode(', ', $placeholders) . ", :created_by)"
);
$stmt->execute($values + ['created_by' => $currentUserId]);
$newId = (int)$pdo->lastInsertId();
mnt_log($pdo, 'equipment_created', $newId, null, null, $currentUserId, $values);
mnt_json_ok(['id' => $newId]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,154 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
/**
* Registers (or updates) one intervention against a maintenance and
* recomputes the maintenance schedule.
*
* The operator signature arrives as a data URL from the tablet canvas and
* is stored as a PNG in attachments/.
*/
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
$maintenanceId = (int)($_POST['maintenance_id'] ?? 0);
$performedAt = trim((string)($_POST['performed_at'] ?? ''));
$status = (string)($_POST['status'] ?? 'completed');
$result = trim((string)($_POST['result'] ?? ''));
$notes = trim((string)($_POST['notes'] ?? ''));
$materials = trim((string)($_POST['materials'] ?? ''));
$operatorId = (int)($_POST['operator_employee_id'] ?? 0);
$operatorName = trim((string)($_POST['operator_name'] ?? ''));
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$signatureData = (string)($_POST['signature'] ?? '');
$clearSignature = !empty($_POST['clear_signature']);
if ($performedAt === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $performedAt)) {
mnt_json_fail('La data dell\'intervento è obbligatoria.');
}
$validStatuses = mnt_lookup($pdo, 'maint_status', ['planned' => '', 'in_progress' => '', 'completed' => '']);
if (!array_key_exists($status, $validStatuses)) {
mnt_json_fail('Stato intervento non valido.');
}
$validResults = mnt_lookup($pdo, 'maint_result', []);
if ($result !== '' && $validResults && !array_key_exists($result, $validResults)) {
mnt_json_fail('Esito intervento non valido.');
}
// Existing row: maintenance is taken from the record, not from the client
if ($id > 0) {
$stmt = $pdo->prepare("SELECT * FROM maint_interventions WHERE id = ?");
$stmt->execute([$id]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing) {
mnt_json_fail('Intervento non trovato.');
}
$maintenanceId = (int)$existing['maintenance_id'];
$equipmentId = (int)$existing['equipment_id'];
$signaturePath = $existing['signature_path'];
} else {
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
$stmt->execute([$maintenanceId]);
$equipmentId = (int)$stmt->fetchColumn();
if (!$equipmentId) {
mnt_json_fail('Manutenzione non trovata.');
}
$signaturePath = null;
}
// Signature handling
if ($clearSignature && $signaturePath) {
mnt_delete_stored_file((string)$signaturePath);
$signaturePath = null;
}
if ($signatureData !== '' && preg_match('#^data:image/png;base64,#', $signatureData)) {
$binary = base64_decode(substr($signatureData, strlen('data:image/png;base64,')), true);
if ($binary === false || strlen($binary) > 2 * 1024 * 1024) {
mnt_json_fail('Firma non valida.');
}
$uploadDir = __DIR__ . '/../attachments/';
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
mnt_json_fail('Cartella allegati non scrivibile.');
}
$newSignature = uniqid('sig_', true) . '.png';
$newSignature = preg_replace('/[^A-Za-z0-9._-]/', '_', $newSignature);
if (file_put_contents($uploadDir . $newSignature, $binary) === false) {
mnt_json_fail('Impossibile salvare la firma.');
}
if ($signaturePath) {
mnt_delete_stored_file((string)$signaturePath);
}
$signaturePath = $newSignature;
}
$values = [
'performed_at' => $performedAt,
'status' => $status,
'result' => $result ?: null,
'notes' => $notes ?: null,
'materials' => $materials ?: null,
'operator_employee_id' => $operatorId ?: null,
'operator_name' => $operatorName ?: null,
'supplier_id' => $supplierId ?: null,
'signature_path' => $signaturePath,
];
if ($id > 0) {
$assignments = [];
foreach (array_keys($values) as $column) {
$assignments[] = "$column = :$column";
}
$stmt = $pdo->prepare("UPDATE maint_interventions SET " . implode(', ', $assignments) . " WHERE id = :id");
$stmt->execute($values + ['id' => $id]);
$interventionId = $id;
$action = 'intervention_updated';
} else {
$values['maintenance_id'] = $maintenanceId;
$values['equipment_id'] = $equipmentId;
$values['created_by'] = $currentUserId;
$columns = array_keys($values);
$placeholders = array_map(fn($column) => ':' . $column, $columns);
$stmt = $pdo->prepare(
"INSERT INTO maint_interventions (" . implode(', ', $columns) . ")
VALUES (" . implode(', ', $placeholders) . ")"
);
$stmt->execute($values);
$interventionId = (int)$pdo->lastInsertId();
$action = 'intervention_created';
}
// Only completed interventions move the next due date; this call handles it
mnt_recalc_maintenance($pdo, $maintenanceId);
mnt_log($pdo, $action, $equipmentId, $maintenanceId, $interventionId, $currentUserId, $values);
$stmt = $pdo->prepare("SELECT last_done_date, next_due_date FROM maint_maintenances WHERE id = ?");
$stmt->execute([$maintenanceId]);
$schedule = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
mnt_json_ok(['id' => $interventionId, 'schedule' => $schedule]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,121 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$id = (int)($_POST['id'] ?? 0);
$equipmentId = (int)($_POST['equipment_id'] ?? 0);
$code = trim((string)($_POST['code'] ?? ''));
$title = trim((string)($_POST['title'] ?? ''));
$description = trim((string)($_POST['description'] ?? ''));
$interventionType = (string)($_POST['intervention_type'] ?? 'scheduled');
$executionType = (string)($_POST['execution_type'] ?? 'internal');
$isCritical = !empty($_POST['is_critical']) ? 1 : 0;
$frequencyUnit = (string)($_POST['frequency_unit'] ?? 'month');
$frequencyValue = (int)($_POST['frequency_value'] ?? 0);
$frequencyNote = trim((string)($_POST['frequency_note'] ?? ''));
$alertDays = (int)($_POST['alert_days'] ?? 7);
$assigneeId = (int)($_POST['assignee_employee_id'] ?? 0);
$supervisorId = (int)($_POST['supervisor_employee_id'] ?? 0);
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$isActive = isset($_POST['is_active']) ? (int)!empty($_POST['is_active']) : 1;
$notes = trim((string)($_POST['notes'] ?? ''));
if ($title === '') {
mnt_json_fail('Il titolo è obbligatorio.');
}
if (!array_key_exists($interventionType, mnt_intervention_types())) {
mnt_json_fail('Tipo di intervento non valido.');
}
if (!array_key_exists($executionType, mnt_execution_types())) {
mnt_json_fail('Tipo di esecuzione non valido.');
}
if (!array_key_exists($frequencyUnit, mnt_frequency_units())) {
mnt_json_fail('Unità di frequenza non valida.');
}
if ($frequencyUnit !== 'on_demand' && $frequencyValue <= 0) {
mnt_json_fail('Indica ogni quanto va eseguita la manutenzione, oppure scegli "Al bisogno".');
}
if ($executionType === 'external' && $supplierId <= 0) {
mnt_json_fail('Per una manutenzione esterna indica il fornitore.');
}
if ($frequencyUnit === 'on_demand') {
$frequencyValue = null;
}
$values = [
'code' => $code ?: null,
'title' => $title,
'description' => $description ?: null,
'intervention_type' => $interventionType,
'execution_type' => $executionType,
'is_critical' => $isCritical,
'frequency_value' => $frequencyValue,
'frequency_unit' => $frequencyUnit,
'frequency_note' => $frequencyNote ?: null,
'alert_days' => max(0, $alertDays),
'assignee_employee_id' => $assigneeId ?: null,
'supervisor_employee_id' => $supervisorId ?: null,
'supplier_id' => $supplierId ?: null,
'is_active' => $isActive,
'notes' => $notes ?: null,
];
if ($id > 0) {
$stmt = $pdo->prepare("SELECT equipment_id FROM maint_maintenances WHERE id = ?");
$stmt->execute([$id]);
$equipmentId = (int)$stmt->fetchColumn();
if (!$equipmentId) {
mnt_json_fail('Manutenzione non trovata.');
}
$assignments = [];
foreach (array_keys($values) as $column) {
$assignments[] = "$column = :$column";
}
$stmt = $pdo->prepare("UPDATE maint_maintenances SET " . implode(', ', $assignments) . " WHERE id = :id");
$stmt->execute($values + ['id' => $id]);
// frequency may have changed — the due date must follow
mnt_recalc_maintenance($pdo, $id);
mnt_log($pdo, 'maintenance_updated', $equipmentId, $id, null, $currentUserId, $values);
mnt_json_ok(['id' => $id]);
}
if ($equipmentId <= 0) {
mnt_json_fail('Attrezzatura non valida.');
}
$stmt = $pdo->prepare("SELECT id FROM inv_equipment WHERE id = ?");
$stmt->execute([$equipmentId]);
if (!$stmt->fetchColumn()) {
mnt_json_fail('Attrezzatura non trovata.');
}
$values['equipment_id'] = $equipmentId;
$values['created_by'] = $currentUserId;
$columns = array_keys($values);
$placeholders = array_map(fn($column) => ':' . $column, $columns);
$stmt = $pdo->prepare(
"INSERT INTO maint_maintenances (" . implode(', ', $columns) . ")
VALUES (" . implode(', ', $placeholders) . ")"
);
$stmt->execute($values);
$newId = (int)$pdo->lastInsertId();
mnt_log($pdo, 'maintenance_created', $equipmentId, $newId, null, $currentUserId, $values);
mnt_json_ok(['id' => $newId]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
try {
$pdo = mnt_pdo();
$fileId = (int)($_POST['file_id'] ?? 0);
if ($fileId <= 0) {
mnt_json_fail('ID non valido.');
}
$stmt = $pdo->prepare("SELECT equipment_id, kind FROM inv_equipment_files WHERE id = ?");
$stmt->execute([$fileId]);
$file = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$file) {
mnt_json_fail('Immagine non trovata.');
}
if ($file['kind'] !== 'photo') {
mnt_json_fail('Solo una foto può essere usata come copertina.');
}
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
->execute([$fileId, (int)$file['equipment_id']]);
mnt_log($pdo, 'cover_changed', (int)$file['equipment_id'], null, null, $currentUserId);
mnt_json_ok();
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}
@@ -0,0 +1,105 @@
<?php
require_once __DIR__ . '/auth_check.php';
mnt_require_permission('maintenance.manage');
/**
* Multi-file upload for the three attachment tables.
* scope = equipment | maintenance | intervention
* kind = file | photo (equipment only; photos feed the gallery)
*/
try {
$pdo = mnt_pdo();
$scope = (string)($_POST['scope'] ?? 'equipment');
$ownerId = (int)($_POST['owner_id'] ?? 0);
$kind = ($_POST['kind'] ?? 'file') === 'photo' ? 'photo' : 'file';
$config = [
'equipment' => ['table' => 'inv_equipment_files', 'owner' => 'equipment_id', 'parent' => 'inv_equipment'],
'maintenance' => ['table' => 'maint_maintenance_files', 'owner' => 'maintenance_id', 'parent' => 'maint_maintenances'],
'intervention' => ['table' => 'maint_intervention_files', 'owner' => 'intervention_id', 'parent' => 'maint_interventions'],
];
if (!isset($config[$scope])) {
mnt_json_fail('Scope non valido.');
}
if ($ownerId <= 0) {
mnt_json_fail('ID non valido.');
}
if (empty($_FILES['files']['name'][0])) {
mnt_json_fail('Nessun file selezionato.');
}
$target = $config[$scope];
$stmt = $pdo->prepare("SELECT id FROM {$target['parent']} WHERE id = ?");
$stmt->execute([$ownerId]);
if (!$stmt->fetchColumn()) {
mnt_json_fail('Record collegato non trovato.');
}
$columns = $scope === 'equipment'
? "({$target['owner']}, kind, original_name, stored_name, mime_type, size, uploaded_by)"
: "({$target['owner']}, original_name, stored_name, mime_type, size, uploaded_by)";
$placeholders = $scope === 'equipment' ? '(?, ?, ?, ?, ?, ?, ?)' : '(?, ?, ?, ?, ?, ?)';
$insert = $pdo->prepare("INSERT INTO {$target['table']} $columns VALUES $placeholders");
$uploaded = [];
$errors = [];
$fileCount = count($_FILES['files']['name']);
for ($i = 0; $i < $fileCount; $i++) {
$file = [
'name' => $_FILES['files']['name'][$i],
'type' => $_FILES['files']['type'][$i],
'tmp_name' => $_FILES['files']['tmp_name'][$i],
'error' => $_FILES['files']['error'][$i],
'size' => $_FILES['files']['size'][$i],
];
try {
$stored = mnt_store_upload($file, $scope);
} catch (RuntimeException $e) {
$errors[] = $file['name'] . ': ' . $e->getMessage();
continue;
}
// A file uploaded as a photo but not actually an image is kept as a file
$rowKind = ($kind === 'photo' && $stored['is_image']) ? 'photo' : 'file';
$values = $scope === 'equipment'
? [$ownerId, $rowKind, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId]
: [$ownerId, $stored['original_name'], $stored['stored_name'], $stored['mime_type'], $stored['size'], $currentUserId];
$insert->execute($values);
$uploaded[] = ['id' => (int)$pdo->lastInsertId(), 'name' => $stored['original_name']];
}
if (!$uploaded) {
mnt_json_fail($errors ? implode('; ', $errors) : 'Nessun file caricato.');
}
// The first photo of an item becomes its cover automatically
if ($scope === 'equipment' && $kind === 'photo') {
$stmt = $pdo->prepare("SELECT cover_file_id FROM inv_equipment WHERE id = ?");
$stmt->execute([$ownerId]);
if (!$stmt->fetchColumn()) {
$pdo->prepare("UPDATE inv_equipment SET cover_file_id = ? WHERE id = ?")
->execute([$uploaded[0]['id'], $ownerId]);
}
}
$equipmentId = $scope === 'equipment' ? $ownerId : null;
$maintenanceId = $scope === 'maintenance' ? $ownerId : null;
$interventionId = $scope === 'intervention' ? $ownerId : null;
mnt_log($pdo, 'file_added', $equipmentId, $maintenanceId, $interventionId, $currentUserId, null,
implode(', ', array_column($uploaded, 'name')));
mnt_json_ok(['uploaded' => $uploaded, 'errors' => $errors]);
} catch (Throwable $e) {
mnt_json_fail('Errore: ' . $e->getMessage());
}