1078 lines
49 KiB
PHP
1078 lines
49 KiB
PHP
<?php
|
|
|
|
/**
|
|
* kiosk_admin.php - gestione dello schermo di reparto (kiosk.php).
|
|
* Comunicazioni, benvenuti, turni, menu, avvisi scorrevoli, obiettivi, sicurezza e impostazioni.
|
|
*/
|
|
|
|
ob_start();
|
|
include('include/headscript.php');
|
|
require_once __DIR__ . '/class/Kiosk.php';
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
/** Permesso richiesto: cambialo se crei un permesso dedicato (es. 'kiosk.manage'). */
|
|
const KIOSK_PERM = 'production.programming.view';
|
|
const KIOSK_IMG_DIR = __DIR__ . '/photos/kiosk/';
|
|
|
|
function kOut(array $a)
|
|
{
|
|
while (ob_get_level()) ob_end_clean();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode($a);
|
|
exit;
|
|
}
|
|
|
|
if (!userCan(KIOSK_PERM)) {
|
|
if (isset($_GET['action']) || isset($_POST['action'])) kOut(['success' => false, 'message' => 'Non hai i permessi per questa pagina']);
|
|
while (ob_get_level()) ob_end_clean();
|
|
http_response_code(403);
|
|
echo '<div style="font-family:sans-serif;padding:60px;text-align:center;"><h3>Accesso non consentito</h3><a href="production_dashboard.php">Torna alla Dashboard</a></div>';
|
|
exit;
|
|
}
|
|
|
|
$userName = '';
|
|
if (!empty($iduserlogin)) {
|
|
try {
|
|
$st = $pdo->prepare("SELECT COALESCE(NULLIF(TRIM(CONCAT_WS(' ', first_name, last_name)), ''), username, email) FROM auth_users WHERE id = ?");
|
|
$st->execute([(int)$iduserlogin]);
|
|
$userName = (string)$st->fetchColumn();
|
|
} catch (Throwable $e) {
|
|
}
|
|
}
|
|
|
|
function kDate($v)
|
|
{
|
|
$v = trim((string)$v);
|
|
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null;
|
|
}
|
|
|
|
/** Salva l'immagine caricata (ridotta a 1920 px se serve) e restituisce il nome del file. */
|
|
function kSaveImage(array $f)
|
|
{
|
|
if (($f['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return null;
|
|
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException('Immagine non caricata (errore ' . $f['error'] . ')');
|
|
if ($f['size'] > 15 * 1024 * 1024) throw new RuntimeException('Immagine troppo grande (massimo 15 MB)');
|
|
$info = @getimagesize($f['tmp_name']);
|
|
$ext = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'][$info['mime'] ?? ''] ?? null;
|
|
if (!$ext) throw new RuntimeException('Formato non supportato: usa JPG, PNG o WEBP');
|
|
if (!is_dir(KIOSK_IMG_DIR) && !mkdir(KIOSK_IMG_DIR, 0775, true)) throw new RuntimeException('Impossibile creare la cartella photos/kiosk');
|
|
$name = date('Ymd_His') . '_' . bin2hex(random_bytes(4));
|
|
|
|
// riduco le foto grandi (dal telefono) a 1920 px: lo schermo non ne ha bisogno di più
|
|
if (function_exists('imagecreatefromstring') && max($info[0], $info[1]) > 1920 && $ext !== 'gif') {
|
|
$src = @imagecreatefromstring(file_get_contents($f['tmp_name']));
|
|
if ($src) {
|
|
$r = 1920 / max($info[0], $info[1]);
|
|
$w = (int)round($info[0] * $r);
|
|
$h = (int)round($info[1] * $r);
|
|
$dst = imagecreatetruecolor($w, $h);
|
|
imagecopyresampled($dst, $src, 0, 0, 0, 0, $w, $h, $info[0], $info[1]);
|
|
imagejpeg($dst, KIOSK_IMG_DIR . $name . '.jpg', 82);
|
|
imagedestroy($src);
|
|
imagedestroy($dst);
|
|
return $name . '.jpg';
|
|
}
|
|
}
|
|
if (!move_uploaded_file($f['tmp_name'], KIOSK_IMG_DIR . $name . '.' . $ext)) throw new RuntimeException('Impossibile salvare l\'immagine');
|
|
return $name . '.' . $ext;
|
|
}
|
|
|
|
function kDeleteImage($name)
|
|
{
|
|
$name = basename((string)$name);
|
|
if ($name !== '' && is_file(KIOSK_IMG_DIR . $name)) @unlink(KIOSK_IMG_DIR . $name);
|
|
}
|
|
|
|
function kioskUrl(PDO $pdo)
|
|
{
|
|
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
|
|
$dir = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'])), '/');
|
|
return ($https ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $dir . '/kiosk.php?k=' . Kiosk::token($pdo);
|
|
}
|
|
|
|
/* ===================== AJAX ===================== */
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? '';
|
|
if ($action !== '') {
|
|
try {
|
|
if ($action === 'load') {
|
|
$s = Kiosk::settings($pdo);
|
|
unset($s['token']);
|
|
$goals = Kiosk::goals($pdo, false);
|
|
kOut([
|
|
'success' => true,
|
|
'url' => kioskUrl($pdo),
|
|
'settings' => $s,
|
|
'safety' => Kiosk::safety(Kiosk::settings($pdo)),
|
|
'items' => Kiosk::items($pdo, false),
|
|
'goals' => $goals,
|
|
'slides' => Kiosk::SLIDES,
|
|
'types' => Kiosk::ITEM_TYPES,
|
|
'sources' => Kiosk::GOAL_SOURCES,
|
|
'today' => Kiosk::day()->format('Y-m-d'),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'save_settings') {
|
|
$slides = array_values(array_intersect(array_keys(Kiosk::SLIDES), (array)($_POST['slides'] ?? [])));
|
|
$vals = [
|
|
'company' => mb_substr(trim((string)($_POST['company'] ?? '')), 0, 60),
|
|
'slide_seconds' => (string)max(6, min(120, (int)($_POST['slide_seconds'] ?? 15))),
|
|
'slides' => implode(',', $slides),
|
|
'scarto_target' => (string)max(0, (float)str_replace(',', '.', (string)($_POST['scarto_target'] ?? 3))),
|
|
'city' => mb_substr(trim((string)($_POST['city'] ?? '')), 0, 60),
|
|
'lat' => is_numeric($_POST['lat'] ?? '') ? (string)(float)$_POST['lat'] : '',
|
|
'lon' => is_numeric($_POST['lon'] ?? '') ? (string)(float)$_POST['lon'] : '',
|
|
'last_incident' => (string)kDate($_POST['last_incident'] ?? ''),
|
|
'record_days' => ($_POST['record_days'] ?? '') === '' ? '' : (string)max(0, (int)$_POST['record_days']),
|
|
'safety_note' => mb_substr(trim((string)($_POST['safety_note'] ?? '')), 0, 250),
|
|
];
|
|
foreach ($vals as $k => $v) Kiosk::set($pdo, $k, $v);
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'new_token') {
|
|
Kiosk::token($pdo, true);
|
|
kOut(['success' => true, 'url' => kioskUrl($pdo)]);
|
|
}
|
|
|
|
if ($action === 'item_save') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$type = (string)($_POST['item_type'] ?? '');
|
|
if (!isset(Kiosk::ITEM_TYPES[$type])) throw new RuntimeException('Scegli il tipo di contenuto');
|
|
$title = mb_substr(trim((string)($_POST['title'] ?? '')), 0, 150);
|
|
if ($title === '') throw new RuntimeException('Scrivi il titolo');
|
|
$from = kDate($_POST['valid_from'] ?? '');
|
|
$to = kDate($_POST['valid_to'] ?? '');
|
|
if ($from && $to && $to < $from) throw new RuntimeException('La data di fine è prima di quella di inizio');
|
|
|
|
$old = '';
|
|
if ($id) {
|
|
$st = $pdo->prepare("SELECT image FROM kiosk_items WHERE id = ?");
|
|
$st->execute([$id]);
|
|
$old = $st->fetchColumn();
|
|
if ($old === false) throw new RuntimeException('Contenuto non trovato');
|
|
}
|
|
$img = $old;
|
|
$new = kSaveImage($_FILES['image'] ?? ['error' => UPLOAD_ERR_NO_FILE]);
|
|
if ($new !== null || !empty($_POST['remove_image'])) {
|
|
kDeleteImage($old);
|
|
$img = $new ?? '';
|
|
}
|
|
$f = [
|
|
'item_type' => $type,
|
|
'title' => $title,
|
|
'body' => trim((string)($_POST['body'] ?? '')),
|
|
'image' => $img,
|
|
'valid_from' => $from,
|
|
'valid_to' => $to,
|
|
'sort' => (int)($_POST['sort'] ?? 0),
|
|
'active' => !empty($_POST['active']) ? 1 : 0,
|
|
];
|
|
if ($id) {
|
|
$f['id'] = $id;
|
|
$pdo->prepare("UPDATE kiosk_items SET item_type = :item_type, title = :title, body = :body, image = :image, valid_from = :valid_from,
|
|
valid_to = :valid_to, sort = :sort, active = :active, updated_at = NOW() WHERE id = :id")->execute($f);
|
|
} else {
|
|
$f['by'] = mb_substr($userName, 0, 100);
|
|
$pdo->prepare("INSERT INTO kiosk_items (item_type, title, body, image, valid_from, valid_to, sort, active, created_by, created_at, updated_at)
|
|
VALUES (:item_type, :title, :body, :image, :valid_from, :valid_to, :sort, :active, :by, NOW(), NOW())")->execute($f);
|
|
}
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'item_toggle') {
|
|
$pdo->prepare("UPDATE kiosk_items SET active = 1 - active, updated_at = NOW() WHERE id = ?")->execute([(int)($_POST['id'] ?? 0)]);
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'item_delete') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$st = $pdo->prepare("SELECT image FROM kiosk_items WHERE id = ?");
|
|
$st->execute([$id]);
|
|
kDeleteImage($st->fetchColumn());
|
|
$pdo->prepare("DELETE FROM kiosk_items WHERE id = ?")->execute([$id]);
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'goal_save') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$src = (string)($_POST['source'] ?? 'manuale');
|
|
if (!isset(Kiosk::GOAL_SOURCES[$src])) $src = 'manuale';
|
|
$title = mb_substr(trim((string)($_POST['title'] ?? '')), 0, 150);
|
|
if ($title === '') throw new RuntimeException('Scrivi il nome dell\'obiettivo');
|
|
$target = (float)str_replace(',', '.', (string)($_POST['target'] ?? 0));
|
|
if ($target <= 0) throw new RuntimeException('Indica il valore da raggiungere');
|
|
$from = kDate($_POST['period_from'] ?? '');
|
|
$to = kDate($_POST['period_to'] ?? '');
|
|
if ($from && $to && $to < $from) throw new RuntimeException('La data di fine è prima di quella di inizio');
|
|
$unit = mb_substr(trim((string)($_POST['unit'] ?? '')), 0, 20);
|
|
if ($src !== 'manuale') $unit = Kiosk::GOAL_SOURCES[$src][1];
|
|
$cur = ($_POST['current_value'] ?? '') === '' ? null : (float)str_replace(',', '.', (string)$_POST['current_value']);
|
|
$f = [
|
|
'title' => $title,
|
|
'source' => $src,
|
|
'unit' => $unit,
|
|
'target' => $target,
|
|
'cur' => $src === 'manuale' ? $cur : null,
|
|
'lower' => $src === 'manuale' ? (!empty($_POST['lower_is_better']) ? 1 : 0) : (Kiosk::GOAL_SOURCES[$src][2] ? 1 : 0),
|
|
'pf' => $from,
|
|
'pt' => $to,
|
|
'sort' => (int)($_POST['sort'] ?? 0),
|
|
'active' => !empty($_POST['active']) ? 1 : 0,
|
|
];
|
|
if ($id) {
|
|
$f['id'] = $id;
|
|
$pdo->prepare("UPDATE kiosk_goals SET title = :title, source = :source, unit = :unit, target = :target, current_value = :cur,
|
|
lower_is_better = :lower, period_from = :pf, period_to = :pt, sort = :sort, active = :active, updated_at = NOW() WHERE id = :id")->execute($f);
|
|
} else {
|
|
$pdo->prepare("INSERT INTO kiosk_goals (title, source, unit, target, current_value, lower_is_better, period_from, period_to, sort, active, updated_at)
|
|
VALUES (:title, :source, :unit, :target, :cur, :lower, :pf, :pt, :sort, :active, NOW())")->execute($f);
|
|
}
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'goal_delete') {
|
|
$pdo->prepare("DELETE FROM kiosk_goals WHERE id = ?")->execute([(int)($_POST['id'] ?? 0)]);
|
|
kOut(['success' => true]);
|
|
}
|
|
|
|
kOut(['success' => false, 'message' => 'Azione sconosciuta']);
|
|
} catch (Throwable $e) {
|
|
kOut(['success' => false, 'message' => $e->getMessage()]);
|
|
}
|
|
}
|
|
?>
|
|
<!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>Schermo di reparto - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--ink: #1f2d3d;
|
|
--muted: #64748b;
|
|
--line: #e2e8f0;
|
|
}
|
|
|
|
body {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.k-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.k-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.k-icon {
|
|
width: 52px;
|
|
height: 52px;
|
|
border-radius: 14px;
|
|
background: linear-gradient(135deg, #0f172a, #334155);
|
|
color: #7dd3fc;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.45rem;
|
|
box-shadow: 0 6px 16px rgba(15, 23, 42, .25);
|
|
}
|
|
|
|
.k-title h5 {
|
|
font-weight: 800;
|
|
margin: 0 0 2px;
|
|
}
|
|
|
|
.k-title small {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.kc {
|
|
background: #fff;
|
|
border: 1px solid var(--line);
|
|
border-radius: 16px;
|
|
padding: 18px 20px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.kc h6 {
|
|
font-weight: 800;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.kc h6 .ms-auto {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.preview {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 16/9;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
background: #0a1120;
|
|
border: 4px solid #0f172a;
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, .25);
|
|
}
|
|
|
|
.preview iframe {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 1920px;
|
|
height: 1080px;
|
|
border: 0;
|
|
transform-origin: 0 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.url {
|
|
font-family: monospace;
|
|
font-size: .85rem;
|
|
}
|
|
|
|
.tchips {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.tchip {
|
|
border: 1.5px solid var(--line);
|
|
background: #fff;
|
|
border-radius: 999px;
|
|
padding: 4px 12px;
|
|
font-weight: 700;
|
|
font-size: .85rem;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.tchip.on {
|
|
background: #0f172a;
|
|
border-color: #0f172a;
|
|
color: #fff;
|
|
}
|
|
|
|
.tchip .n {
|
|
opacity: .6;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.it {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 10px 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.it.off {
|
|
opacity: .5;
|
|
}
|
|
|
|
.it .th {
|
|
width: 64px;
|
|
height: 48px;
|
|
border-radius: 8px;
|
|
background: #f1f5f9;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #94a3b8;
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
.it .th img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.it .tx {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.it .tx b {
|
|
display: block;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.it .tx small {
|
|
color: var(--muted);
|
|
display: block;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ttag {
|
|
font-size: .72rem;
|
|
font-weight: 800;
|
|
border-radius: 6px;
|
|
padding: 2px 8px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.ttag.comunicazione {
|
|
background: #e0f2fe;
|
|
color: #075985;
|
|
}
|
|
|
|
.ttag.benvenuto {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.ttag.turni {
|
|
background: #ede9fe;
|
|
color: #5b21b6;
|
|
}
|
|
|
|
.ttag.menu {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.ttag.ticker {
|
|
background: #ffedd5;
|
|
color: #9a3412;
|
|
}
|
|
|
|
.state {
|
|
font-size: .75rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.gl {
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 10px 14px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.gl .r {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.gl .bar {
|
|
height: 8px;
|
|
background: #e2e8f0;
|
|
border-radius: 99px;
|
|
overflow: hidden;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.gl .bar span {
|
|
display: block;
|
|
height: 100%;
|
|
background: #2563eb;
|
|
}
|
|
|
|
.gl.ok .bar span {
|
|
background: #16a34a;
|
|
}
|
|
|
|
.safe-n {
|
|
font-size: 3rem;
|
|
font-weight: 900;
|
|
color: #16a34a;
|
|
line-height: 1;
|
|
}
|
|
|
|
.slide-list .form-check {
|
|
padding: 6px 10px 6px 2.6em;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.slide-list .form-check:hover {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.k-toast {
|
|
position: fixed;
|
|
right: 20px;
|
|
bottom: 20px;
|
|
z-index: 3000;
|
|
background: #0f172a;
|
|
color: #fff;
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
font-weight: 600;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all .25s;
|
|
}
|
|
|
|
.k-toast.err {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
.k-toast.show {
|
|
opacity: 1;
|
|
transform: none;
|
|
}
|
|
|
|
.type-pick .btn {
|
|
font-weight: 700;
|
|
}
|
|
|
|
#imgPrev {
|
|
max-height: 160px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--line);
|
|
}
|
|
|
|
.hint {
|
|
font-size: .82rem;
|
|
color: var(--muted);
|
|
margin-top: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrapper toggled" id="appWrapper">
|
|
<?php include('include/navbar.php'); ?>
|
|
<?php include('include/topbar.php'); ?>
|
|
|
|
<div class="page-wrapper">
|
|
<div class="page-content">
|
|
<div class="k-head mb-3">
|
|
<div class="k-title">
|
|
<div class="k-icon"><i class="fa-solid fa-tv"></i></div>
|
|
<div>
|
|
<h5>Schermo di reparto</h5>
|
|
<small>Cosa vedono i dipendenti in sala pausa: contenuti, obiettivi e sicurezza</small>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-outline-secondary" id="btnCopy"><i class="fa-regular fa-copy me-1"></i> Copia link</button>
|
|
<a class="btn btn-dark" id="btnOpen" target="_blank" rel="noopener"><i class="fa-solid fa-up-right-from-square me-1"></i> Apri lo schermo</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-xl-8">
|
|
<div class="kc">
|
|
<h6><i class="fa-solid fa-layer-group text-primary"></i> Contenuti
|
|
<button class="btn btn-primary btn-sm ms-auto" id="btnNewItem"><i class="fa-solid fa-plus me-1"></i> Nuovo contenuto</button>
|
|
</h6>
|
|
<div class="tchips" id="typeChips"></div>
|
|
<div id="items"></div>
|
|
</div>
|
|
|
|
<div class="kc">
|
|
<h6><i class="fa-solid fa-bullseye text-danger"></i> Obiettivi
|
|
<button class="btn btn-outline-primary btn-sm ms-auto" id="btnNewGoal"><i class="fa-solid fa-plus me-1"></i> Nuovo obiettivo</button>
|
|
</h6>
|
|
<div id="goals"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-4">
|
|
<div class="kc">
|
|
<h6><i class="fa-solid fa-display text-dark"></i> Anteprima dal vivo</h6>
|
|
<div class="preview" id="prevBox"><iframe id="prevFrame" title="Anteprima schermo" tabindex="-1"></iframe></div>
|
|
<div class="hint mt-2">Apri il link sul PC o sul TV della sala pausa e mettilo a schermo intero (F11). Nessun login: chi ha il link vede lo schermo.</div>
|
|
<div class="input-group input-group-sm mt-2">
|
|
<input class="form-control url" id="url" readonly>
|
|
<button class="btn btn-outline-danger" id="btnToken" title="Crea un nuovo link: quello vecchio smette di funzionare"><i class="fa-solid fa-rotate"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="kc" id="fSafety">
|
|
<h6><i class="fa-solid fa-shield-halved text-success"></i> Sicurezza</h6>
|
|
<div class="d-flex align-items-center gap-3 mb-2">
|
|
<div class="safe-n" id="safeN">—</div>
|
|
<div class="small text-muted" id="safeTxt">giorni senza infortuni</div>
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col-7"><label class="form-label small fw-bold mb-1">Ultimo infortunio</label><input type="date" class="form-control" name="last_incident" id="sLast"></div>
|
|
<div class="col-5"><label class="form-label small fw-bold mb-1">Record (giorni)</label><input type="number" min="0" class="form-control" name="record_days" id="sRec"></div>
|
|
<div class="col-12"><label class="form-label small fw-bold mb-1">Messaggio</label><input type="text" maxlength="250" class="form-control" name="safety_note" id="sNote" placeholder="Es. Indossa sempre i DPI vicino agli estrusori"></div>
|
|
</div>
|
|
<button class="btn btn-success btn-sm mt-3"><i class="fa-solid fa-check me-1"></i> Salva</button>
|
|
</form>
|
|
|
|
<form class="kc" id="fSettings">
|
|
<h6><i class="fa-solid fa-sliders text-secondary"></i> Impostazioni</h6>
|
|
<div class="row g-2">
|
|
<div class="col-7"><label class="form-label small fw-bold mb-1">Nome azienda</label><input class="form-control" name="company" id="cCompany"></div>
|
|
<div class="col-5"><label class="form-label small fw-bold mb-1">Secondi per slide</label><input type="number" min="6" max="120" class="form-control" name="slide_seconds" id="cSec"></div>
|
|
<div class="col-7"><label class="form-label small fw-bold mb-1">Obiettivo scarto %</label><input type="number" step="0.1" min="0" class="form-control" name="scarto_target" id="cScarto"></div>
|
|
<div class="col-12"><label class="form-label small fw-bold mb-1">Città per il meteo</label>
|
|
<div class="input-group"><input class="form-control" name="city" id="cCity" placeholder="Es. Lecco"><button type="button" class="btn btn-outline-secondary" id="btnGeo">Trova</button></div>
|
|
<input type="hidden" name="lat" id="cLat"><input type="hidden" name="lon" id="cLon">
|
|
<div class="hint" id="geoTxt"></div>
|
|
</div>
|
|
</div>
|
|
<label class="form-label small fw-bold mt-3 mb-1">Slide da mostrare</label>
|
|
<div class="slide-list" id="slideList"></div>
|
|
<button class="btn btn-primary btn-sm mt-2"><i class="fa-solid fa-check me-1"></i> Salva</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
|
|
<!-- MODULO CONTENUTO -->
|
|
<div class="modal fade" id="mItem" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<form class="modal-content" id="fItem" enctype="multipart/form-data">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" id="mItemTitle">Nuovo contenuto</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="iId">
|
|
<label class="form-label fw-bold">Tipo</label>
|
|
<div class="d-flex flex-wrap gap-2 type-pick mb-1" id="typePick"></div>
|
|
<div class="hint mb-3" id="typeHint"></div>
|
|
<div class="mb-3"><label class="form-label fw-bold">Titolo</label><input class="form-control form-control-lg" name="title" id="iTitle" maxlength="150" required></div>
|
|
<div class="mb-3"><label class="form-label fw-bold">Testo</label><textarea class="form-control" name="body" id="iBody" rows="6"></textarea></div>
|
|
<div class="row g-3">
|
|
<div class="col-md-4"><label class="form-label fw-bold">Visibile dal</label><input type="date" class="form-control" name="valid_from" id="iFrom"></div>
|
|
<div class="col-md-4"><label class="form-label fw-bold">al</label><input type="date" class="form-control" name="valid_to" id="iTo">
|
|
<div class="hint">vuoto = sempre</div>
|
|
</div>
|
|
<div class="col-md-2"><label class="form-label fw-bold">Ordine</label><input type="number" class="form-control" name="sort" id="iSort" value="0"></div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" name="active" value="1" id="iActive" checked><label class="form-check-label fw-bold" for="iActive">Attivo</label></div>
|
|
</div>
|
|
<div class="col-12" id="wImg">
|
|
<label class="form-label fw-bold">Immagine <span class="text-muted fw-normal">(facoltativa)</span></label>
|
|
<input type="file" class="form-control" name="image" id="iImg" accept="image/jpeg,image/png,image/webp">
|
|
<div class="d-flex align-items-center gap-3 mt-2 d-none" id="wImgPrev">
|
|
<img id="imgPrev" alt="">
|
|
<div class="form-check"><input class="form-check-input" type="checkbox" name="remove_image" value="1" id="iRemImg"><label class="form-check-label" for="iRemImg">Togli l'immagine</label></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-danger me-auto" id="btnDelItem"><i class="fa-solid fa-trash me-1"></i> Elimina</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
|
<button class="btn btn-primary" id="btnSaveItem"><i class="fa-solid fa-check me-1"></i> Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODULO OBIETTIVO -->
|
|
<div class="modal fade" id="mGoal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-scrollable">
|
|
<form class="modal-content" id="fGoal">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" id="mGoalTitle">Nuovo obiettivo</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="gId">
|
|
<div class="mb-3"><label class="form-label fw-bold">Nome</label><input class="form-control" name="title" id="gTitle" maxlength="150" placeholder="Es. Metri prodotti a ottobre" required></div>
|
|
<div class="mb-3"><label class="form-label fw-bold">Valore attuale</label><select class="form-select" name="source" id="gSource"></select>
|
|
<div class="hint">I valori automatici si calcolano da soli dai dati di produzione del periodo.</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-6"><label class="form-label fw-bold">Obiettivo</label><input type="number" step="0.01" min="0" class="form-control" name="target" id="gTarget" required></div>
|
|
<div class="col-6 gman"><label class="form-label fw-bold">Unità</label><input class="form-control" name="unit" id="gUnit" maxlength="20" placeholder="es. pezzi, %"></div>
|
|
<div class="col-6 gman"><label class="form-label fw-bold">Valore attuale</label><input type="number" step="0.01" class="form-control" name="current_value" id="gCur"></div>
|
|
<div class="col-6 gman d-flex align-items-end">
|
|
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" name="lower_is_better" value="1" id="gLower"><label class="form-check-label" for="gLower">Meno è meglio</label></div>
|
|
</div>
|
|
<div class="col-6"><label class="form-label fw-bold">Dal</label><input type="date" class="form-control" name="period_from" id="gFrom"></div>
|
|
<div class="col-6"><label class="form-label fw-bold">Al</label><input type="date" class="form-control" name="period_to" id="gTo"></div>
|
|
<div class="col-12 hint" style="margin-top:2px">Date vuote = mese in corso.</div>
|
|
<div class="col-6"><label class="form-label fw-bold">Ordine</label><input type="number" class="form-control" name="sort" id="gSort" value="0"></div>
|
|
<div class="col-6 d-flex align-items-end">
|
|
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" name="active" value="1" id="gActive" checked><label class="form-check-label fw-bold" for="gActive">Attivo</label></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-danger me-auto" id="btnDelGoal"><i class="fa-solid fa-trash me-1"></i> Elimina</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
|
<button class="btn btn-primary"><i class="fa-solid fa-check me-1"></i> Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
<script>
|
|
$(function() {
|
|
let L = null;
|
|
let filter = '';
|
|
const mItem = new bootstrap.Modal(document.getElementById('mItem'));
|
|
const mGoal = new bootstrap.Modal(document.getElementById('mGoal'));
|
|
const HINTS = {
|
|
comunicazione: 'Una slide con titolo, testo e immagine facoltativa (es. formazione, nuovi macchinari, chiusure).',
|
|
benvenuto: 'Nuovo collega o ringraziamento: metti una foto (con il consenso della persona).',
|
|
turni: 'Una riga per voce, es. "Lunedì: Rossi, Bianchi". La parte prima dei due punti viene evidenziata.',
|
|
menu: 'Una riga per piatto, es. "Primo: Pasta al pomodoro".',
|
|
ticker: 'Frase breve che scorre in basso su tutte le slide. Il testo è facoltativo.'
|
|
};
|
|
|
|
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
} [c]));
|
|
const num = (v, d = 0) => (+v || 0).toLocaleString('it-IT', {
|
|
minimumFractionDigits: d,
|
|
maximumFractionDigits: d
|
|
});
|
|
const dIt = s => s ? s.slice(8, 10) + '/' + s.slice(5, 7) + '/' + s.slice(0, 4) : '';
|
|
|
|
function toast(msg, err) {
|
|
const el = $(`<div class="k-toast ${err ? 'err' : ''}">${esc(msg)}</div>`).appendTo('body');
|
|
setTimeout(() => el.addClass('show'), 10);
|
|
setTimeout(() => {
|
|
el.removeClass('show');
|
|
setTimeout(() => el.remove(), 300);
|
|
}, err ? 4500 : 2200);
|
|
}
|
|
|
|
function post(data, isForm) {
|
|
const o = isForm ? {
|
|
url: '?',
|
|
type: 'POST',
|
|
data,
|
|
processData: false,
|
|
contentType: false,
|
|
dataType: 'json'
|
|
} : {
|
|
url: '?',
|
|
type: 'POST',
|
|
data,
|
|
dataType: 'json'
|
|
};
|
|
return $.ajax(o).then(r => {
|
|
if (!r.success) {
|
|
toast(r.message || 'Errore', true);
|
|
return $.Deferred().reject().promise();
|
|
}
|
|
return r;
|
|
}, () => {
|
|
toast('Errore di rete', true);
|
|
return $.Deferred().reject().promise();
|
|
});
|
|
}
|
|
|
|
function refreshPreview() {
|
|
const f = document.getElementById('prevFrame');
|
|
f.src = L.url + '&_=' + Date.now();
|
|
}
|
|
|
|
function scalePreview() {
|
|
const w = document.getElementById('prevBox').clientWidth;
|
|
document.getElementById('prevFrame').style.transform = `scale(${w / 1920})`;
|
|
}
|
|
$(window).on('resize', scalePreview);
|
|
|
|
function load(preview) {
|
|
return $.getJSON('?action=load').then(r => {
|
|
if (!r.success) return toast(r.message || 'Errore', true);
|
|
const first = !L;
|
|
L = r;
|
|
$('#url').val(r.url);
|
|
$('#btnOpen').attr('href', r.url);
|
|
renderItems();
|
|
renderGoals();
|
|
if (first) fillSettings();
|
|
renderSafety();
|
|
if (first || preview) refreshPreview();
|
|
scalePreview();
|
|
});
|
|
}
|
|
|
|
/* ---------- contenuti ---------- */
|
|
function itemState(it) {
|
|
const t = L.today;
|
|
if (+it.active !== 1) return '<span class="state text-muted">disattivato</span>';
|
|
if (it.valid_from && it.valid_from > t) return `<span class="state text-primary">dal ${dIt(it.valid_from)}</span>`;
|
|
if (it.valid_to && it.valid_to < t) return '<span class="state text-danger">scaduto</span>';
|
|
return `<span class="state text-success"><i class="fa-solid fa-circle" style="font-size:.5rem"></i> in onda${it.valid_to ? ' fino al ' + dIt(it.valid_to) : ''}</span>`;
|
|
}
|
|
|
|
function renderItems() {
|
|
const cnt = {};
|
|
L.items.forEach(i => cnt[i.item_type] = (cnt[i.item_type] || 0) + 1);
|
|
$('#typeChips').html(`<button class="tchip ${filter ? '' : 'on'}" data-f="">Tutti<span class="n">${L.items.length}</span></button>` +
|
|
Object.entries(L.types).map(([k, v]) => `<button class="tchip ${filter === k ? 'on' : ''}" data-f="${k}">${esc(v)}<span class="n">${cnt[k] || 0}</span></button>`).join(''));
|
|
const list = L.items.filter(i => !filter || i.item_type === filter);
|
|
$('#items').html(list.length ? list.map(i => `<div class="it ${+i.active ? '' : 'off'}" data-item="${i.id}" role="button">
|
|
<div class="th">${i.image ? `<img src="photos/kiosk/${esc(i.image)}" alt="">` : '<i class="fa-regular fa-image"></i>'}</div>
|
|
<div class="tx"><b>${esc(i.title)}</b><small>${esc((i.body || '').replace(/\s+/g, ' ').slice(0, 140))}</small></div>
|
|
<span class="ttag ${esc(i.item_type)}">${esc(L.types[i.item_type] || i.item_type)}</span>
|
|
<div style="min-width:120px;text-align:right">${itemState(i)}</div>
|
|
<div class="form-check form-switch mb-0 ms-2" title="Attivo"><input class="form-check-input item-toggle" type="checkbox" data-id="${i.id}" ${+i.active ? 'checked' : ''}></div>
|
|
</div>`).join('') : '<div class="text-muted p-3 text-center">Nessun contenuto. Crea il primo con "Nuovo contenuto".</div>');
|
|
}
|
|
$(document).on('click', '.tchip', function() {
|
|
filter = this.dataset.f;
|
|
renderItems();
|
|
});
|
|
$(document).on('click', '.item-toggle', function(e) {
|
|
e.stopPropagation();
|
|
post({
|
|
action: 'item_toggle',
|
|
id: this.dataset.id
|
|
}).then(() => load(true));
|
|
});
|
|
$(document).on('click', '[data-item]', function(e) {
|
|
if ($(e.target).closest('.form-check').length) return;
|
|
openItem(L.items.find(i => +i.id === +this.dataset.item));
|
|
});
|
|
$('#btnNewItem').on('click', () => openItem(null));
|
|
|
|
function setType(t) {
|
|
$('#typePick input[value="' + t + '"]').prop('checked', true);
|
|
$('#typeHint').text(HINTS[t] || '');
|
|
$('#wImg').toggle(t !== 'ticker');
|
|
$('#iBody').attr('rows', t === 'ticker' ? 2 : 6);
|
|
}
|
|
$('#typePick').html(Object.entries(<?= json_encode(Kiosk::ITEM_TYPES) ?>).map(([k, v]) =>
|
|
`<input type="radio" class="btn-check" name="item_type" id="t_${k}" value="${k}"><label class="btn btn-outline-dark" for="t_${k}">${esc(v)}</label>`).join(''));
|
|
$(document).on('change', '#typePick input', function() {
|
|
setType(this.value);
|
|
});
|
|
|
|
let editingItem = null;
|
|
|
|
function openItem(i) {
|
|
editingItem = i;
|
|
$('#fItem')[0].reset();
|
|
$('#mItemTitle').text(i ? 'Modifica contenuto' : 'Nuovo contenuto');
|
|
$('#iId').val(i ? i.id : '');
|
|
setType(i ? i.item_type : (filter || 'comunicazione'));
|
|
$('#iTitle').val(i ? i.title : '');
|
|
$('#iBody').val(i ? i.body || '' : '');
|
|
$('#iFrom').val(i ? i.valid_from || '' : L.today);
|
|
$('#iTo').val(i ? i.valid_to || '' : '');
|
|
$('#iSort').val(i ? i.sort : 0);
|
|
$('#iActive').prop('checked', i ? +i.active === 1 : true);
|
|
$('#wImgPrev').toggleClass('d-none', !(i && i.image));
|
|
if (i && i.image) $('#imgPrev').attr('src', 'photos/kiosk/' + i.image);
|
|
$('#btnDelItem').toggle(!!i).removeClass('btn-danger').addClass('btn-outline-danger').html('<i class="fa-solid fa-trash me-1"></i> Elimina');
|
|
mItem.show();
|
|
}
|
|
$('#iImg').on('change', function() {
|
|
const f = this.files[0];
|
|
if (!f) return;
|
|
$('#imgPrev').attr('src', URL.createObjectURL(f));
|
|
$('#wImgPrev').removeClass('d-none');
|
|
});
|
|
|
|
$('#fItem').on('submit', function(e) {
|
|
e.preventDefault();
|
|
const fd = new FormData(this);
|
|
fd.append('action', 'item_save');
|
|
$('#btnSaveItem').prop('disabled', true);
|
|
post(fd, true).then(() => {
|
|
mItem.hide();
|
|
toast(editingItem ? 'Contenuto aggiornato' : 'Contenuto creato');
|
|
load(true);
|
|
}).always(() => $('#btnSaveItem').prop('disabled', false));
|
|
});
|
|
$('#btnDelItem').on('click', function() {
|
|
const b = $(this);
|
|
if (!b.hasClass('btn-danger')) return b.removeClass('btn-outline-danger').addClass('btn-danger').html('<i class="fa-solid fa-trash me-1"></i> Conferma eliminazione');
|
|
post({
|
|
action: 'item_delete',
|
|
id: editingItem.id
|
|
}).then(() => {
|
|
mItem.hide();
|
|
toast('Contenuto eliminato');
|
|
load(true);
|
|
});
|
|
});
|
|
|
|
/* ---------- obiettivi ---------- */
|
|
function renderGoals() {
|
|
$('#goals').html(L.goals.length ? L.goals.map(g => {
|
|
const dec = g.unit === '%' || g.unit === 'h' ? 1 : 0;
|
|
const src = L.sources[g.source] ? L.sources[g.source][0] : g.source;
|
|
return `<div class="gl ${g.ok ? 'ok' : ''} ${+g.active ? '' : 'opacity-50'}" data-goal="${g.id}" role="button">
|
|
<div class="r"><b>${esc(g.title)}</b><span class="text-muted small">${esc(g.period_label)} · ${esc(src)}</span>
|
|
<span class="ms-auto fw-bold">${g.current === null ? '—' : num(g.current, dec)} / ${+g.lower_is_better ? '≤ ' : ''}${num(g.target, dec)} ${esc(g.unit)}</span></div>
|
|
<div class="bar"><span style="width:${g.pct || 0}%"></span></div></div>`;
|
|
}).join('') : '<div class="text-muted p-3 text-center">Nessun obiettivo. Esempio: "Metri prodotti a ottobre" con valore automatico.</div>');
|
|
}
|
|
$('#gSource').html(Object.entries(<?= json_encode(Kiosk::GOAL_SOURCES) ?>).map(([k, v]) => `<option value="${k}">${esc(v[0])}${k !== 'manuale' ? ' (automatico)' : ''}</option>`).join(''));
|
|
$('#gSource').on('change', function() {
|
|
$('.gman').toggle(this.value === 'manuale');
|
|
});
|
|
let editingGoal = null;
|
|
|
|
function openGoal(g) {
|
|
editingGoal = g;
|
|
$('#fGoal')[0].reset();
|
|
$('#mGoalTitle').text(g ? 'Modifica obiettivo' : 'Nuovo obiettivo');
|
|
$('#gId').val(g ? g.id : '');
|
|
$('#gTitle').val(g ? g.title : '');
|
|
$('#gSource').val(g ? g.source : 'metri_mese').trigger('change');
|
|
$('#gTarget').val(g ? +g.target : '');
|
|
$('#gUnit').val(g ? g.unit : '');
|
|
$('#gCur').val(g && g.current_value !== null ? +g.current_value : '');
|
|
$('#gLower').prop('checked', g ? +g.lower_is_better === 1 : false);
|
|
$('#gFrom').val(g ? g.period_from || '' : '');
|
|
$('#gTo').val(g ? g.period_to || '' : '');
|
|
$('#gSort').val(g ? g.sort : 0);
|
|
$('#gActive').prop('checked', g ? +g.active === 1 : true);
|
|
$('#btnDelGoal').toggle(!!g).removeClass('btn-danger').addClass('btn-outline-danger').html('<i class="fa-solid fa-trash me-1"></i> Elimina');
|
|
mGoal.show();
|
|
}
|
|
$('#btnNewGoal').on('click', () => openGoal(null));
|
|
$(document).on('click', '[data-goal]', function() {
|
|
openGoal(L.goals.find(g => +g.id === +this.dataset.goal));
|
|
});
|
|
$('#fGoal').on('submit', function(e) {
|
|
e.preventDefault();
|
|
const d = $(this).serializeArray();
|
|
d.push({
|
|
name: 'action',
|
|
value: 'goal_save'
|
|
});
|
|
post(d).then(() => {
|
|
mGoal.hide();
|
|
toast('Obiettivo salvato');
|
|
load(true);
|
|
});
|
|
});
|
|
$('#btnDelGoal').on('click', function() {
|
|
const b = $(this);
|
|
if (!b.hasClass('btn-danger')) return b.removeClass('btn-outline-danger').addClass('btn-danger').html('<i class="fa-solid fa-trash me-1"></i> Conferma eliminazione');
|
|
post({
|
|
action: 'goal_delete',
|
|
id: editingGoal.id
|
|
}).then(() => {
|
|
mGoal.hide();
|
|
toast('Obiettivo eliminato');
|
|
load(true);
|
|
});
|
|
});
|
|
|
|
/* ---------- sicurezza e impostazioni ---------- */
|
|
function renderSafety() {
|
|
const s = L.safety;
|
|
$('#safeN').text(s ? s.days : '—');
|
|
$('#safeTxt').html(s ? `giorni senza infortuni<br>dal ${esc(s.since)}${s.record ? ' · record ' + s.record : ''}` : 'Inserisci la data dell\'ultimo infortunio per mostrare il contatore');
|
|
}
|
|
|
|
function fillSettings() {
|
|
const s = L.settings;
|
|
$('#sLast').val(s.last_incident);
|
|
$('#sRec').val(s.record_days);
|
|
$('#sNote').val(s.safety_note);
|
|
$('#cCompany').val(s.company);
|
|
$('#cSec').val(s.slide_seconds);
|
|
$('#cScarto').val(s.scarto_target);
|
|
$('#cCity').val(s.city);
|
|
$('#cLat').val(s.lat);
|
|
$('#cLon').val(s.lon);
|
|
$('#geoTxt').text(s.lat ? `Posizione: ${s.lat}, ${s.lon}` : 'Scrivi la città e premi "Trova" per attivare il meteo');
|
|
const on = s.slides.split(',');
|
|
$('#slideList').html(Object.entries(L.slides).map(([k, v]) => `<div class="form-check form-switch"><input class="form-check-input" type="checkbox" name="slides[]" value="${k}" id="sl_${k}" ${on.includes(k) ? 'checked' : ''}><label class="form-check-label" for="sl_${k}">${esc(v)}</label></div>`).join(''));
|
|
}
|
|
|
|
// sicurezza e impostazioni si salvano insieme (stessa tabella)
|
|
function saveAll() {
|
|
const d = $('#fSettings').serializeArray().concat($('#fSafety').serializeArray());
|
|
d.push({
|
|
name: 'action',
|
|
value: 'save_settings'
|
|
});
|
|
return post(d).then(() => {
|
|
toast('Impostazioni salvate');
|
|
load(true);
|
|
});
|
|
}
|
|
$('#fSettings, #fSafety').on('submit', function(e) {
|
|
e.preventDefault();
|
|
saveAll();
|
|
});
|
|
|
|
$('#btnGeo').on('click', function() {
|
|
const q = $('#cCity').val().trim();
|
|
if (!q) return;
|
|
$('#geoTxt').text('Cerco…');
|
|
fetch('https://geocoding-api.open-meteo.com/v1/search?count=1&language=it&name=' + encodeURIComponent(q))
|
|
.then(r => r.json()).then(r => {
|
|
const g = r.results && r.results[0];
|
|
if (!g) return $('#geoTxt').text('Città non trovata');
|
|
$('#cLat').val(g.latitude.toFixed(4));
|
|
$('#cLon').val(g.longitude.toFixed(4));
|
|
$('#cCity').val(g.name);
|
|
$('#geoTxt').text(`Trovata: ${g.name} (${g.admin1 || g.country || ''}). Premi Salva.`);
|
|
}).catch(() => $('#geoTxt').text('Ricerca non disponibile'));
|
|
});
|
|
|
|
/* ---------- link ---------- */
|
|
$('#btnCopy').on('click', () => {
|
|
const u = $('#url').val();
|
|
(navigator.clipboard ? navigator.clipboard.writeText(u) : Promise.reject()).then(() => toast('Link copiato'), () => {
|
|
$('#url').trigger('select');
|
|
document.execCommand('copy');
|
|
toast('Link copiato');
|
|
});
|
|
});
|
|
let armToken = false;
|
|
$('#btnToken').on('click', function() {
|
|
if (!armToken) {
|
|
armToken = true;
|
|
$(this).removeClass('btn-outline-danger').addClass('btn-danger').html('Nuovo link?');
|
|
setTimeout(() => {
|
|
armToken = false;
|
|
$('#btnToken').removeClass('btn-danger').addClass('btn-outline-danger').html('<i class="fa-solid fa-rotate"></i>');
|
|
}, 4000);
|
|
return;
|
|
}
|
|
post({
|
|
action: 'new_token'
|
|
}).then(() => {
|
|
toast('Nuovo link creato: aggiornalo sullo schermo');
|
|
armToken = false;
|
|
$('#btnToken').removeClass('btn-danger').addClass('btn-outline-danger').html('<i class="fa-solid fa-rotate"></i>');
|
|
load(true);
|
|
});
|
|
});
|
|
|
|
load();
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|