Files
zibo-dashboard/public/userarea/manutenzioni/include/functions.php
T
2026-08-12 21:29:55 +03:00

484 lines
16 KiB
PHP

<?php
/**
* Manutenzioni — shared helpers.
*
* Included by every page and ajax endpoint of the module. Assumes a PDO
* connection is available through DBHandlerSelect (class/db-functions.php).
*/
require_once __DIR__ . '/../../class/db-functions.php';
if (!function_exists('mnt_pdo')) {
function mnt_pdo(): PDO
{
return DBHandlerSelect::getInstance()->getConnection();
}
}
if (!function_exists('mnt_h')) {
function mnt_h($value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
}
/**
* Minimal HTML email body, same visual language as the scadenzario mails.
*
* Lives here rather than in cron/send_notifications.php so that the preview
* (make mail-preview-html) renders exactly what the cron sends, instead of a
* copy that drifts.
*/
if (!function_exists('mnt_mail_body')) {
function mnt_mail_body(string $heading, string $subject, string $message, string $color, string $url): string
{
$safeHeading = htmlspecialchars($heading, ENT_QUOTES, 'UTF-8');
$safeSubject = htmlspecialchars($subject, ENT_QUOTES, 'UTF-8');
return '<!doctype html><html><body style="margin:0;padding:24px;background:#f4f6f8;font-family:Arial,Helvetica,sans-serif;color:#1e3a44">'
. '<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.06)">'
. '<div style="background:' . $color . ';color:#fff;padding:16px 20px;font-size:17px;font-weight:bold">' . $safeHeading . '</div>'
. '<div style="padding:20px">'
. '<p style="font-size:15px;font-weight:bold;margin:0 0 10px">' . $safeSubject . '</p>'
. '<p style="font-size:14px;line-height:1.55;margin:0 0 18px">' . $message . '</p>'
. '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" style="display:inline-block;background:#2f7d8f;color:#fff;text-decoration:none;padding:10px 18px;border-radius:6px;font-size:14px">Apri la scheda</a>'
. '</div></div></body></html>';
}
}
/**
* Subject and body of a notification, for both kinds.
*
* @param array $maintenance needs equipment_name, next_due_date, is_critical
* @return array{subject:string, body:string}
*/
if (!function_exists('mnt_notification_mail')) {
function mnt_notification_mail(array $maintenance, string $label, int $daysLeft, string $detailUrl, ?string $today = null): array
{
$today = $today ?? date('Y-m-d');
$isOverdue = $maintenance['next_due_date'] < $today;
$due = date('d/m/Y', strtotime($maintenance['next_due_date']));
$criticalNote = (int)($maintenance['is_critical'] ?? 0) === 1
? '<br><strong style="color:#b02a37">Manutenzione critica.</strong>'
: '';
if ($isOverdue) {
return [
'subject' => '⚠️ Manutenzione scaduta: ' . $maintenance['equipment_name'],
'body' => mnt_mail_body(
'Manutenzione scaduta',
$maintenance['equipment_name'] . ' — ' . $label,
'Era prevista per il <strong>' . $due . '</strong>, in ritardo di <strong>'
. abs($daysLeft) . ' giorni</strong>.' . $criticalNote,
'#dc3545',
$detailUrl
),
];
}
$daysText = $daysLeft === 0 ? '<strong>oggi</strong>' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
return [
'subject' => '🔧 Manutenzione in scadenza: ' . $maintenance['equipment_name'],
'body' => mnt_mail_body(
'Manutenzione in scadenza',
$maintenance['equipment_name'] . ' — ' . $label,
'È prevista per il <strong>' . $due . '</strong> (' . $daysText . ').' . $criticalNote,
'#2f7d8f',
$detailUrl
),
];
}
}
/**
* Reads ?page and ?per_page for the paged lists.
*
* per_page is a whitelist, not a free number: it lands in a LIMIT clause and
* an open value would let anyone ask for the whole table in one request.
* Returns [page, perPage] with page still unbounded above — the caller clamps
* it once it knows how many rows the filters actually match.
*/
if (!function_exists('mnt_page_params')) {
function mnt_page_params(int $default = 25): array
{
$allowed = [25, 50, 100];
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : $default;
if (!in_array($perPage, $allowed, true)) {
$perPage = $default;
}
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
return [max(1, $page), $perPage];
}
}
/**
* Frequency units offered in the maintenance form.
* 'on_demand' = no automatic due date (the client's "al bisogno" cases).
*/
if (!function_exists('mnt_frequency_units')) {
function mnt_frequency_units(): array
{
return [
'day' => 'Giorni',
'week' => 'Settimane',
'month' => 'Mesi',
'year' => 'Anni',
'on_demand' => 'Al bisogno (senza scadenza automatica)',
];
}
}
if (!function_exists('mnt_intervention_types')) {
function mnt_intervention_types(): array
{
return ['scheduled' => 'Programmata', 'extraordinary' => 'Straordinaria'];
}
}
if (!function_exists('mnt_execution_types')) {
function mnt_execution_types(): array
{
return ['internal' => 'Interna', 'external' => 'Esterna'];
}
}
if (!function_exists('mnt_equipment_statuses')) {
function mnt_equipment_statuses(): array
{
return [
'active' => 'Attivo',
'out_of_service' => 'Fuori servizio',
'decommissioned' => 'Dismesso',
];
}
}
/**
* Options managed through the existing lookup_values.php CRUD.
* Falls back to sensible defaults if the category has no rows yet.
*/
if (!function_exists('mnt_lookup')) {
function mnt_lookup(PDO $pdo, string $category, array $fallback = []): array
{
static $cache = [];
if (!isset($cache[$category])) {
$stmt = $pdo->prepare("
SELECT value, label
FROM ws_lookup_options
WHERE category = ? AND is_active = 1
ORDER BY sort_order ASC, label ASC
");
$stmt->execute([$category]);
$options = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$options[$row['value']] = $row['label'];
}
$cache[$category] = $options ?: $fallback;
}
return $cache[$category];
}
}
/**
* Next due date = last completed intervention + frequency.
* Returns null when the maintenance has no automatic schedule or was never done.
*/
if (!function_exists('mnt_calc_next_due')) {
function mnt_calc_next_due(?string $lastDoneDate, ?int $frequencyValue, string $frequencyUnit): ?string
{
if ($lastDoneDate === null || $frequencyUnit === 'on_demand' || !$frequencyValue) {
return null;
}
$map = ['day' => 'day', 'week' => 'week', 'month' => 'month', 'year' => 'year'];
if (!isset($map[$frequencyUnit])) {
return null;
}
$date = date_create($lastDoneDate);
if (!$date) {
return null;
}
$dayOfMonth = (int)$date->format('j');
$date->modify('+' . $frequencyValue . ' ' . $map[$frequencyUnit]);
// PHP fa traboccare i mesi corti: 31/01 +1 mese diventa 03/03.
// Per una manutenzione la data attesa è l'ultimo giorno del mese.
if (in_array($frequencyUnit, ['month', 'year'], true) && (int)$date->format('j') !== $dayOfMonth) {
$date->modify('last day of previous month');
}
return $date->format('Y-m-d');
}
}
/**
* Recompute last_done_date / next_due_date of a maintenance from its
* completed interventions. Call after any intervention insert/update/delete.
*/
if (!function_exists('mnt_recalc_maintenance')) {
function mnt_recalc_maintenance(PDO $pdo, int $maintenanceId): void
{
$stmt = $pdo->prepare("SELECT frequency_value, frequency_unit FROM maint_maintenances WHERE id = ?");
$stmt->execute([$maintenanceId]);
$maintenance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$maintenance) {
return;
}
// Only completed interventions move the schedule; planned/in-progress do not.
$stmt = $pdo->prepare("
SELECT MAX(performed_at)
FROM maint_interventions
WHERE maintenance_id = ? AND status = 'completed'
");
$stmt->execute([$maintenanceId]);
$lastDone = $stmt->fetchColumn() ?: null;
$nextDue = mnt_calc_next_due(
$lastDone,
$maintenance['frequency_value'] !== null ? (int)$maintenance['frequency_value'] : null,
(string)$maintenance['frequency_unit']
);
$stmt = $pdo->prepare("UPDATE maint_maintenances SET last_done_date = ?, next_due_date = ? WHERE id = ?");
$stmt->execute([$lastDone, $nextDue, $maintenanceId]);
}
}
/**
* Schedule state of a maintenance, used for badges and filters.
* Returns one of: overdue | due_soon | ok | none
*/
if (!function_exists('mnt_due_state')) {
function mnt_due_state(?string $nextDueDate, int $alertDays = 0, ?string $today = null): string
{
if (empty($nextDueDate)) {
return 'none';
}
$today = $today ?: date('Y-m-d');
if ($nextDueDate < $today) {
return 'overdue';
}
$threshold = date('Y-m-d', strtotime($today . ' +' . max(0, $alertDays) . ' days'));
return $nextDueDate <= $threshold ? 'due_soon' : 'ok';
}
}
if (!function_exists('mnt_due_badge')) {
/**
* Lo stato "none" copre due casi diversi che non vanno confusi:
* una manutenzione al bisogno (per scelta senza scadenza) e una
* programmata mai eseguita (la scadenza arriverà al primo intervento).
*/
function mnt_due_badge(string $state, ?string $frequencyUnit = null): array
{
switch ($state) {
case 'overdue':
return ['label' => 'Scaduta', 'class' => 'mnt-badge-overdue'];
case 'due_soon':
return ['label' => 'In scadenza', 'class' => 'mnt-badge-soon'];
case 'ok':
return ['label' => 'In regola', 'class' => 'mnt-badge-ok'];
default:
if ($frequencyUnit !== null && $frequencyUnit !== 'on_demand') {
return ['label' => 'Mai eseguita', 'class' => 'mnt-badge-none'];
}
return ['label' => 'Al bisogno', 'class' => 'mnt-badge-none'];
}
}
}
if (!function_exists('mnt_format_date')) {
function mnt_format_date(?string $date): string
{
if (empty($date) || $date === '0000-00-00') {
return '—';
}
$ts = strtotime($date);
return $ts ? date('d/m/Y', $ts) : '—';
}
}
/**
* Human-readable frequency ("Ogni 3 mesi", "Al bisogno").
*/
if (!function_exists('mnt_format_frequency')) {
function mnt_format_frequency(?int $value, string $unit, ?string $note = null): string
{
$units = [
'day' => ['giorno', 'giorni'],
'week' => ['settimana', 'settimane'],
'month' => ['mese', 'mesi'],
'year' => ['anno', 'anni'],
];
if ($unit === 'on_demand' || !$value || !isset($units[$unit])) {
$text = 'Al bisogno';
} else {
$word = $value === 1 ? $units[$unit][0] : $units[$unit][1];
$text = 'Ogni ' . $value . ' ' . $word;
}
if (!empty($note)) {
$text .= ' — ' . $note;
}
return $text;
}
}
/**
* Audit log entry. All ids are optional so the same call works for
* equipment, maintenance and intervention events.
*/
if (!function_exists('mnt_log')) {
function mnt_log(
PDO $pdo,
string $action,
?int $equipmentId = null,
?int $maintenanceId = null,
?int $interventionId = null,
?int $userId = null,
?array $changes = null,
?string $notes = null
): void {
$stmt = $pdo->prepare("
INSERT INTO maint_histories
(equipment_id, maintenance_id, intervention_id, user_id, action, changes, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$equipmentId,
$maintenanceId,
$interventionId,
$userId,
$action,
$changes !== null ? json_encode($changes, JSON_UNESCAPED_UNICODE) : null,
$notes,
]);
}
}
/**
* Store one uploaded file into attachments/ and return its metadata.
* Throws RuntimeException on a rejected file.
*/
if (!function_exists('mnt_store_upload')) {
function mnt_store_upload(array $file, string $prefix = 'mnt'): array
{
$allowedExtensions = [
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'odt', 'ods',
'jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'bmp', 'zip',
];
$maxBytes = 20 * 1024 * 1024;
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Errore durante il caricamento del file.');
}
if (($file['size'] ?? 0) > $maxBytes) {
throw new RuntimeException('File troppo grande (massimo 20 MB).');
}
$originalName = (string)($file['name'] ?? 'file');
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
throw new RuntimeException('Tipo di file non consentito: .' . $extension);
}
$uploadDir = __DIR__ . '/../attachments/';
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
throw new RuntimeException('Cartella allegati non scrivibile.');
}
$storedName = uniqid($prefix . '_', true) . '.' . $extension;
$safeStored = preg_replace('/[^A-Za-z0-9._-]/', '_', $storedName);
if (!move_uploaded_file($file['tmp_name'], $uploadDir . $safeStored)) {
throw new RuntimeException('Impossibile salvare il file.');
}
return [
'original_name' => $originalName,
'stored_name' => $safeStored,
'mime_type' => $file['type'] ?? null,
'size' => (int)($file['size'] ?? 0),
'is_image' => in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'], true),
];
}
}
if (!function_exists('mnt_delete_stored_file')) {
function mnt_delete_stored_file(string $storedName): void
{
$path = __DIR__ . '/../attachments/' . basename($storedName);
if (is_file($path)) {
@unlink($path);
}
}
}
/**
* Employees / suppliers / departments pickers, shared by several forms.
*/
if (!function_exists('mnt_form_data')) {
function mnt_form_data(PDO $pdo): array
{
return [
'categories' => $pdo->query("
SELECT id, name, color, requires_line
FROM inv_categories
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC),
'lines' => $pdo->query("
SELECT id, line_number, name, color
FROM production_lines
ORDER BY line_number ASC
")->fetchAll(PDO::FETCH_ASSOC),
'departments' => $pdo->query("
SELECT id, name
FROM departments
WHERE is_active = 1
ORDER BY sort_order ASC, name ASC
")->fetchAll(PDO::FETCH_ASSOC),
'employees' => $pdo->query("
SELECT id, CONCAT(first_name, ' ', last_name) AS full_name
FROM employees
WHERE status = 'active'
ORDER BY first_name ASC, last_name ASC
")->fetchAll(PDO::FETCH_ASSOC),
'suppliers' => $pdo->query("
SELECT idsupplier AS id, supplier_name AS name
FROM suppliers
ORDER BY supplier_name ASC
")->fetchAll(PDO::FETCH_ASSOC),
];
}
}