Files
booking-service/frontend/admin.html

773 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestionale — Farmacia Ianni</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&display=swap" rel="stylesheet">
<style>
:root{
/* ── Farmacia Ianni — colori esatti dal logo SVG ── */
--navy-50:#e9eff4;--navy-100:#c7d5e2;--navy-200:#8dabc4;--navy-300:#5381a5;
--navy-400:#1b5786;--navy-500:#002c50;--navy-600:#002544;--navy-700:#001e38;
--navy-800:#00172c;--navy-900:#001020;
--green-50:#f2f8eb;--green-100:#ddecc9;--green-200:#b3d878;--green-300:#8ec44a;
--green-400:#80ba27;--green-500:#6a971f;--green-600:#577c19;--green-700:#446113;
/* ── Neutrals ── */
--n0:#fff;--n50:#f8f9fc;--n100:#f0f1f6;--n200:#e2e4ed;--n300:#c8cade;
--n400:#9699b8;--n500:#6a6d8e;--n600:#474a6a;--n700:#2f3154;--n800:#1e2040;--n900:#0f1028;
/* ── Semantic ── */
--ok-bg:#ecfaf3;--ok:#1a8c52;--warn-bg:#fff8eb;--warn:#b87d00;
--err-bg:#fff0f0;--err:#c52b2b;--info-bg:#eef6ff;--info:#1c6fd4;
/* ── System ── */
--font:'DM Sans',system-ui,sans-serif;
--r:8px;--r-lg:12px;--r-xl:16px;
--sh-sm:0 1px 3px rgba(0,44,80,.08),0 1px 2px rgba(0,0,0,.04);
--sh-md:0 4px 14px rgba(0,44,80,.10),0 2px 4px rgba(0,0,0,.04);
--sh-lg:0 8px 28px rgba(0,44,80,.14),0 4px 8px rgba(0,0,0,.06);
--sidebar-w:260px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{font-size:16px;-webkit-font-smoothing:antialiased}
body{font-family:var(--font);font-size:1rem;color:var(--n800);background:var(--n50);line-height:1.6}
button{font-family:inherit;cursor:pointer;border:none;background:none}
input,select,textarea{font-family:inherit;font-size:.9375rem}
/* ═══ LOGIN ═══ */
#login-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(145deg,var(--navy-900) 0%,var(--navy-500) 100%)}
.login-card{background:#fff;border-radius:var(--r-xl);padding:3.5rem 3rem;max-width:420px;width:100%;box-shadow:var(--sh-lg);text-align:center}
.login-card h1{font-size:1.5rem;font-weight:700;margin-bottom:.5rem;color:var(--navy-500)}
.login-card p{font-size:.9375rem;color:var(--n500);margin-bottom:2.5rem}
.login-card .logo-wrap{margin:0 auto 2rem;width:200px}
.login-card .logo-wrap img{width:100%;height:auto}
.btn-google{display:inline-flex;align-items:center;gap:1rem;padding:1rem 2rem;border:2px solid var(--n200);border-radius:var(--r-lg);font-weight:600;font-size:1rem;color:var(--n700);transition:.2s}
.btn-google:hover{border-color:var(--navy-400);box-shadow:var(--sh-sm)}
.login-hint{font-size:.8125rem;color:var(--n400);margin-top:2rem}
/* ═══ APP SHELL ═══ */
#app{display:none}
.shell{display:flex;height:100vh;overflow:hidden}
/* Sidebar */
.sidebar{width:var(--sidebar-w);background:var(--navy-500);display:flex;flex-direction:column;flex-shrink:0;overflow-y:auto}
.sidebar::-webkit-scrollbar{width:3px}
.sidebar::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:2px}
.sb-brand{padding:1.75rem 1.75rem 1.5rem;border-bottom:1px solid rgba(255,255,255,.1)}
.sb-brand .logo-sidebar{width:160px;height:auto}
.sb-tag{display:block;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:rgba(255,255,255,.3);margin-top:.75rem}
.sb-nav{flex:1;padding:1rem 0}
.sb-section{font-size:.625rem;font-weight:700;text-transform:uppercase;letter-spacing:.14em;color:rgba(255,255,255,.2);padding:1.25rem 1.75rem .5rem}
.sb-item{display:flex;align-items:center;gap:1rem;padding:.875rem 1.5rem .875rem calc(1.75rem - 3px);font-size:1rem;font-weight:500;color:rgba(255,255,255,.5);border-left:3px solid transparent;cursor:pointer;transition:.15s}
.sb-item:hover{background:rgba(255,255,255,.06);color:rgba(255,255,255,.85)}
.sb-item.active{color:#fff;border-left-color:var(--green-400);background:rgba(255,255,255,.1);font-weight:600}
.sb-item svg{width:20px;height:20px;flex-shrink:0}
.sb-footer{padding:1.25rem 1.75rem;border-top:1px solid rgba(255,255,255,.1)}
.sb-user{font-size:.875rem;color:rgba(255,255,255,.5)}
.sb-user strong{display:block;color:rgba(255,255,255,.9);font-weight:600;font-size:.9375rem}
.btn-logout{font-size:.8125rem;color:rgba(255,255,255,.3);margin-top:.5rem;text-decoration:underline}
.btn-logout:hover{color:rgba(255,255,255,.7)}
/* Main */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.topbar{height:68px;background:#fff;border-bottom:1px solid var(--n200);display:flex;align-items:center;padding:0 2.5rem;flex-shrink:0}
.topbar h2{font-size:1.375rem;font-weight:700;color:var(--navy-500)}
.topbar .pill{margin-left:1rem;font-size:.8125rem;font-weight:700;background:var(--green-50);color:var(--green-600);padding:4px 14px;border-radius:999px}
.content{flex:1;overflow-y:auto;padding:2rem 2.5rem 4rem}
/* ═══ KPI ═══ */
.kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:1.25rem;margin-bottom:2rem}
.kpi{background:#fff;border-radius:var(--r-lg);padding:1.75rem 2rem;box-shadow:var(--sh-sm);border:1px solid var(--n200)}
.kpi--hi{border-left:4px solid var(--green-400)}
.kpi__lbl{font-size:.8125rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--n500);margin-bottom:.5rem}
.kpi__val{font-size:2.5rem;font-weight:800;color:var(--navy-500);line-height:1.1}
.kpi__sub{font-size:.875rem;color:var(--n400);margin-top:.25rem}
/* ═══ CARD / TABLE ═══ */
.card{background:#fff;border-radius:var(--r-lg);box-shadow:var(--sh-sm);border:1px solid var(--n200);overflow:hidden}
.card__hd{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.75rem;border-bottom:1px solid var(--n100)}
.card__ti{font-size:1.0625rem;font-weight:700;color:var(--navy-500)}
.toolbar{display:flex;gap:.625rem;align-items:center}
table{width:100%;border-collapse:collapse}
th{font-size:.8125rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--n500);padding:1rem 1.25rem;text-align:left;background:var(--n50);border-bottom:1px solid var(--n200)}
td{padding:1rem 1.25rem;font-size:1rem;border-bottom:1px solid var(--n100);vertical-align:middle}
tr:last-child td{border-bottom:none}
tr:hover{background:var(--n50)}
/* ═══ BADGES ═══ */
.bs{display:inline-block;font-size:.8125rem;font-weight:600;padding:4px 14px;border-radius:999px}
.bs-confirmed{background:var(--ok-bg);color:var(--ok)}
.bs-completed{background:var(--info-bg);color:var(--info)}
.bs-cancelled{background:var(--err-bg);color:var(--err)}
.bs-no_show{background:var(--warn-bg);color:var(--warn)}
/* ═══ BUTTONS ═══ */
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.75rem 1.5rem;border-radius:var(--r);font-size:1rem;font-weight:600;transition:.15s}
.btn-p{background:var(--green-500);color:#fff}
.btn-p:hover{background:var(--green-600)}
.btn-s{padding:.5rem 1rem;font-size:.9375rem}
.btn-g{color:var(--n500);padding:.5rem .75rem;font-size:1rem}
.btn-g:hover{background:var(--n100);color:var(--n700)}
.btn-d{color:var(--err)}
.btn-d:hover{background:var(--err-bg)}
/* ═══ INPUTS ═══ */
.inp{padding:.75rem 1rem;border:1px solid var(--n200);border-radius:var(--r);outline:none;font-size:1rem;transition:.15s}
.inp:focus{border-color:var(--navy-400);box-shadow:0 0 0 3px rgba(0,44,80,.1)}
.inp-s{padding:.5rem .875rem;font-size:.9375rem}
select.inp{cursor:pointer}
/* ═══ MODAL ═══ */
.mo{position:fixed;inset:0;background:rgba(0,16,32,.45);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;pointer-events:none;transition:.2s}
.mo.open{opacity:1;pointer-events:auto}
.mo__box{background:#fff;border-radius:var(--r-xl);padding:2.5rem;width:100%;max-width:540px;box-shadow:var(--sh-lg)}
.mo__box h3{font-size:1.25rem;font-weight:700;margin-bottom:1.5rem;color:var(--navy-500)}
.fg{margin-bottom:1.25rem}
.fg label{display:block;font-size:.875rem;font-weight:600;color:var(--n600);margin-bottom:.375rem}
.fg .inp{width:100%}
.fg textarea.inp{resize:vertical}
.f-actions{display:flex;justify-content:flex-end;gap:.75rem;margin-top:2rem}
/* ═══ MISC ═══ */
.prov-card{background:#fff;border-radius:var(--r-lg);box-shadow:var(--sh-sm);border:1px solid var(--n200);margin-bottom:1.25rem;overflow:hidden}
.prov-card__hd{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.75rem;border-bottom:1px solid var(--n100);background:var(--n50)}
.prov-card__name{font-size:1.0625rem;font-weight:700;color:var(--navy-500)}
.prov-card__meta{font-size:.8125rem;color:var(--n400)}
.svc-row{display:flex;align-items:center;gap:1rem;padding:.875rem 1.75rem;border-bottom:1px solid var(--n100)}
.svc-row:last-child{border-bottom:none}
.svc-name{font-weight:600;min-width:180px}
.svc-days{display:flex;gap:.375rem;flex-wrap:wrap}
.svc-day{font-size:.75rem;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--navy-50);color:var(--navy-500)}
.svc-time{font-size:.8125rem;color:var(--n500)}
.cal-grid{display:grid;grid-template-columns:80px repeat(7,1fr);min-height:500px}
.cal-hdr{font-size:.75rem;font-weight:700;text-transform:uppercase;color:var(--n500);padding:.75rem .5rem;text-align:center;border-bottom:2px solid var(--n200);background:var(--n50)}
.cal-hdr.today{background:var(--green-50);border-bottom-color:var(--green-400)}
.cal-time{font-size:.6875rem;color:var(--n400);padding:.25rem .5rem;text-align:right;border-right:1px solid var(--n200);height:40px;display:flex;align-items:start;justify-content:flex-end}
.cal-cell{border-right:1px solid var(--n100);border-bottom:1px solid var(--n100);height:40px;position:relative;padding:0 2px}
.cal-ev{position:absolute;left:2px;right:2px;border-radius:4px;padding:2px 4px;font-size:.625rem;font-weight:600;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;z-index:1;line-height:1.3}
.cal-ev:hover{z-index:10;overflow:visible;white-space:normal;box-shadow:var(--sh-md)}
.cat-diagnostica{background:#e3f2fd;color:#1565c0;border-left:3px solid #1565c0}
.cat-consulenza{background:#f3e5f5;color:#7b1fa2;border-left:3px solid #7b1fa2}
.cat-galenica{background:#fff3e0;color:#e65100;border-left:3px solid #e65100}
.cat-generale{background:var(--green-50);color:var(--green-700);border-left:3px solid var(--green-500)}
.empty{text-align:center;padding:4rem;color:var(--n400);font-size:1.0625rem}
.toast{position:fixed;bottom:2rem;right:2rem;background:var(--navy-500);color:#fff;padding:1rem 1.75rem;border-radius:var(--r-lg);font-size:1rem;font-weight:500;z-index:2000;transform:translateY(100px);opacity:0;transition:.3s}
.toast.show{transform:translateY(0);opacity:1}
.pgn{display:flex;align-items:center;justify-content:center;gap:.5rem;padding:1rem 1.5rem;border-top:1px solid var(--n100)}
.pgn button{padding:.375rem .75rem;border-radius:var(--r);font-size:.875rem;font-weight:600;color:var(--n500);background:var(--n0);border:1px solid var(--n200);cursor:pointer;transition:.15s}
.pgn button:hover:not(:disabled){background:var(--n100);color:var(--navy-500)}
.pgn button:disabled{opacity:.3;cursor:default}
.pgn button.active{background:var(--navy-500);color:#fff;border-color:var(--navy-500)}
.pgn span{font-size:.8125rem;color:var(--n400)}
.tab{display:none}.tab.active{display:block}
</style>
</head>
<body>
<!-- LOGIN -->
<div id="login-screen">
<div class="login-card">
<div class="logo-wrap"><img src="/static/logo.svg" alt="Farmacia Ianni"></div>
<h1>Area gestionale</h1>
<p>Gestione prenotazioni e servizi</p>
<a href="/auth/login" class="btn-google">
<svg width="22" height="22" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>
Accedi con Google
</a>
<p class="login-hint">Accesso riservato @kitzanos.com e @farmaciaianni.it</p>
</div>
</div>
<!-- APP -->
<div id="app"><div class="shell">
<aside class="sidebar">
<div class="sb-brand">
<img src="/static/logo-white.svg" class="logo-sidebar" alt="Farmacia Ianni">
<span class="sb-tag">Gestionale prenotazioni</span>
</div>
<div class="sb-section">Pannello</div>
<nav class="sb-nav">
<div class="sb-item active" data-tab="dashboard" onclick="go('dashboard')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>Dashboard</div>
<div class="sb-item" data-tab="bookings" onclick="go('bookings')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>Prenotazioni</div>
<div class="sb-item" data-tab="calendar" onclick="go('calendar')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M3 10h18"/><path d="M8 2v4"/><path d="M16 2v4"/><circle cx="8" cy="15" r="1"/><circle cx="12" cy="15" r="1"/><circle cx="16" cy="15" r="1"/></svg>Calendario</div>
<div class="sb-item" data-tab="services" onclick="go('services')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>Servizi</div>
<div class="sb-item" data-tab="providers" onclick="go('providers')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4-4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>Operatori</div>
<div class="sb-item" data-tab="customers" onclick="go('customers')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4-4v2"/><circle cx="12" cy="7" r="4"/></svg>Clienti</div>
<div class="sb-section" style="margin-top:.5rem">Sistema</div>
<div class="sb-item" data-tab="settings" onclick="go('settings')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>Impostazioni</div>
</nav>
<div class="sb-footer"><div class="sb-user"><strong id="u-name"></strong><span id="u-email"></span></div><button class="btn-logout" onclick="logout()">Esci</button></div>
</aside>
<div class="main">
<div class="topbar"><h2 id="tb-title">Dashboard</h2><span class="pill" id="tb-pill"></span></div>
<div class="content">
<!-- DASHBOARD -->
<div class="tab active" id="t-dashboard">
<!-- KPI row -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:1.25rem;margin-bottom:2rem">
<div class="kpi kpi--hi"><div class="kpi__lbl">Oggi</div><div class="kpi__val" id="k-today"></div><div class="kpi__sub">confermate</div></div>
<div class="kpi"><div class="kpi__lbl">Domani</div><div class="kpi__val" id="k-tomorrow"></div><div class="kpi__sub">confermate</div></div>
<div class="kpi"><div class="kpi__lbl">Prossimi 7gg</div><div class="kpi__val" id="k-week"></div><div class="kpi__sub">conf. + completate</div></div>
<div class="kpi"><div class="kpi__lbl">Clienti</div><div class="kpi__val" id="k-customers"></div><div class="kpi__sub">nel sistema</div></div>
<div class="kpi"><div class="kpi__lbl">No-show</div><div class="kpi__val" id="k-noshow"></div><div class="kpi__sub">non presentati</div></div>
</div>
<!-- Week chart + Top services -->
<div style="display:grid;grid-template-columns:2fr 1fr;gap:1.25rem;margin-bottom:1.5rem">
<div class="card"><div class="card__hd"><span class="card__ti">Prossimi 7 giorni</span></div>
<div id="d-weekchart" style="padding:1.25rem 1.5rem;height:180px;display:flex;align-items:flex-end;gap:.5rem"></div>
</div>
<div class="card"><div class="card__hd"><span class="card__ti">Top servizi (30gg)</span></div>
<div id="d-topsvcs" style="padding:1rem 1.5rem"></div>
</div>
</div>
<!-- Upcoming + Recent -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.25rem">
<div class="card"><div class="card__hd"><span class="card__ti">Prossimi appuntamenti</span></div><div id="d-upcoming"></div></div>
<div class="card"><div class="card__hd"><span class="card__ti">Attività recenti</span></div><div id="d-recent"></div></div>
</div>
</div>
<!-- PRENOTAZIONI -->
<div class="tab" id="t-bookings">
<div class="card">
<div class="card__hd">
<span class="card__ti">Tutte le prenotazioni</span>
<div class="toolbar">
<input type="date" id="f-date" class="inp inp-s" onchange="loadBookings(1)">
<select id="f-status" class="inp inp-s" onchange="loadBookings(1)"><option value="">Tutti gli stati</option><option value="confirmed">Confermate</option><option value="completed">Completate</option><option value="cancelled">Cancellate</option><option value="no_show">No-show</option></select>
<select id="f-prov" class="inp inp-s" onchange="loadBookings(1)"><option value="">Tutti gli operatori</option></select>
</div>
</div>
<div id="d-bookings"></div>
</div>
</div>
<!-- SERVIZI -->
<div class="tab" id="t-services">
<div class="card"><div class="card__hd"><span class="card__ti">Servizi</span><button class="btn btn-p btn-s" onclick="openSvcModal()">+ Nuovo servizio</button></div><div id="d-services"></div></div>
</div>
<!-- OPERATORI -->
<div class="tab" id="t-providers">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem">
<div></div>
<button class="btn btn-p btn-s" onclick="openProvModal()">+ Nuovo operatore</button>
</div>
<div id="d-providers-detail"></div>
</div>
<!-- CLIENTI -->
<div class="tab" id="t-customers">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem">
<input type="text" id="cust-search" class="inp" placeholder="Cerca per nome, telefono o email..." style="width:350px" oninput="searchCustomers()">
<span id="cust-count" style="font-size:.875rem;color:var(--n400)"></span>
</div>
<div class="card"><div id="d-customers"></div></div>
</div>
<!-- CALENDARIO -->
<div class="tab" id="t-calendar">
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem">
<button class="btn btn-s btn-g" onclick="calNav(-7)">← Sett. prec.</button>
<h3 id="cal-range" style="font-size:1.0625rem;font-weight:700;color:var(--navy-500)"></h3>
<button class="btn btn-s btn-g" onclick="calNav(7)">Sett. succ. →</button>
<select id="cal-prov-filter" class="inp inp-s" onchange="loadCalendar()" style="margin-left:auto">
<option value="">Tutti gli operatori</option>
</select>
</div>
<div class="card" style="overflow-x:auto">
<div id="d-calendar" style="min-width:800px"></div>
</div>
</div>
<!-- IMPOSTAZIONI -->
<div class="tab" id="t-settings">
<!-- Farmacia -->
<div class="card" style="margin-bottom:1.5rem">
<div class="card__hd"><span class="card__ti">Dati farmacia</span></div>
<div style="padding:1.5rem 1.75rem">
<div class="fg"><label>Nome</label><input class="inp" id="cfg-pharmacy_name"></div>
<div class="fg"><label>Indirizzo</label><input class="inp" id="cfg-pharmacy_address"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div class="fg"><label>Telefono</label><input class="inp" id="cfg-pharmacy_phone"></div>
<div class="fg"><label>WhatsApp</label><input class="inp" id="cfg-pharmacy_wa"></div>
</div>
<button class="btn btn-p btn-s" onclick="saveSettings(['pharmacy_name','pharmacy_address','pharmacy_phone','pharmacy_wa'])">Salva dati farmacia</button>
</div>
</div>
<!-- WhatsApp -->
<div class="card" style="margin-bottom:1.5rem">
<div class="card__hd"><span class="card__ti">WhatsApp (Baileys)</span><span class="bs" id="wa-badge"></span></div>
<div style="padding:1.5rem 1.75rem">
<div id="wa-status-box" style="margin-bottom:1.25rem">
<p style="color:var(--n500);font-size:.9375rem">Caricamento stato...</p>
</div>
<div id="wa-qr-box" style="text-align:center;margin-bottom:1.25rem;display:none">
<p style="font-size:.875rem;color:var(--n500);margin-bottom:.75rem">Scansiona il QR con WhatsApp → Dispositivi collegati → Collega dispositivo</p>
<div style="position:relative;display:inline-block">
<img id="wa-qr-img" style="max-width:260px;border-radius:var(--r-lg);border:1px solid var(--n200)">
<div id="wa-qr-overlay" style="display:none;position:absolute;inset:0;background:rgba(0,44,80,.85);border-radius:var(--r-lg);display:none;align-items:center;justify-content:center;flex-direction:column;gap:.75rem">
<span style="color:#fff;font-weight:700;font-size:1rem">QR scaduto</span>
<button class="btn btn-s" style="background:var(--green-500);color:#fff" onclick="loadWaQr()">Genera nuovo QR</button>
</div>
</div>
<div id="wa-qr-timer" style="margin-top:.75rem;font-size:.875rem;color:var(--n400)"></div>
</div>
<div style="display:flex;gap:.75rem;align-items:center">
<label style="font-size:.875rem;font-weight:600;color:var(--n600)">Notifiche WA attive</label>
<input type="checkbox" id="cfg-wa_enabled" onchange="toggleSetting('wa_enabled',this.checked)">
<button class="btn btn-s btn-g" onclick="checkWaStatus()" style="margin-left:auto">↻ Aggiorna stato</button>
</div>
</div>
</div>
<!-- Email SMTP -->
<div class="card">
<div class="card__hd"><span class="card__ti">Email (SMTP)</span><span class="bs" id="smtp-badge"></span></div>
<div style="padding:1.5rem 1.75rem">
<div style="display:grid;grid-template-columns:2fr 1fr;gap:1rem">
<div class="fg"><label>Server SMTP</label><input class="inp" id="cfg-smtp_host" placeholder="smtp.gmail.com"></div>
<div class="fg"><label>Porta</label><input class="inp" id="cfg-smtp_port" placeholder="587"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div class="fg"><label>Utente</label><input class="inp" id="cfg-smtp_user" placeholder="email@farmaciaianni.it"></div>
<div class="fg"><label>Password</label><input class="inp" id="cfg-smtp_pass" type="password" placeholder="App password Gmail"></div>
</div>
<div class="fg"><label>Mittente (From)</label><input class="inp" id="cfg-smtp_from" placeholder="prenotazioni@farmaciaianni.it"></div>
<div style="display:flex;gap:.75rem;align-items:center;margin-bottom:1rem">
<label style="font-size:.875rem;font-weight:600;color:var(--n600)">Notifiche email attive</label>
<input type="checkbox" id="cfg-smtp_enabled" onchange="toggleSetting('smtp_enabled',this.checked)">
</div>
<div style="display:flex;gap:.75rem">
<button class="btn btn-p btn-s" onclick="saveSettings(['smtp_host','smtp_port','smtp_user','smtp_pass','smtp_from'])">Salva SMTP</button>
<button class="btn btn-s btn-g" onclick="testEmail()">Invia email di test</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div></div>
<!-- MODAL: Servizio -->
<div class="mo" id="m-svc"><div class="mo__box">
<h3 id="m-svc-t">Nuovo servizio</h3>
<div class="fg"><label>Nome</label><input class="inp" id="s-name" placeholder="es. Consulenza dermatologica"></div>
<div class="fg"><label>Slug</label><input class="inp" id="s-slug" placeholder="es. consulenza-dermo"></div>
<div class="fg"><label>Durata (minuti)</label><input class="inp" id="s-dur" type="number" value="30"></div>
<div class="fg"><label>Categoria</label><input class="inp" id="s-cat" placeholder="es. salute" value="generale"></div>
<div class="fg"><label>Descrizione</label><textarea class="inp" id="s-desc" rows="3"></textarea></div>
<div class="f-actions"><button class="btn btn-g" onclick="closeM('m-svc')">Annulla</button><button class="btn btn-p" onclick="saveSvc()">Salva</button></div>
</div></div>
<!-- MODAL: Operatore -->
<div class="mo" id="m-prov"><div class="mo__box">
<h3>Nuovo operatore</h3>
<div class="fg"><label>Nome e cognome</label><input class="inp" id="p-name" placeholder="es. Dott.ssa Maria Rossi"></div>
<div class="fg"><label>Email</label><input class="inp" id="p-email" type="email" placeholder="email@farmaciaianni.it"></div>
<div class="fg"><label>Telefono</label><input class="inp" id="p-phone" placeholder="+39 ..."></div>
<div class="f-actions"><button class="btn btn-g" onclick="closeM('m-prov')">Annulla</button><button class="btn btn-p" onclick="saveProv()">Salva</button></div>
</div></div>
<!-- MODAL: Dettaglio cliente -->
<div class="mo" id="m-cust"><div class="mo__box" style="max-width:640px;max-height:80vh;overflow-y:auto"><div id="m-cust-body"></div><div class="f-actions"><button class="btn btn-g" onclick="closeM('m-cust')">Chiudi</button></div></div></div>
<!-- MODAL: Assegna servizio -->
<div class="mo" id="m-assign"><div class="mo__box">
<h3 id="m-assign-t">Assegna servizio</h3>
<div class="fg"><label>Servizio</label><select class="inp" id="a-svc"></select></div>
<div class="fg"><label>Giorni e orari</label>
<div id="a-days" style="display:grid;grid-template-columns:auto 1fr 1fr;gap:.5rem;align-items:center"></div>
</div>
<div class="f-actions"><button class="btn btn-g" onclick="closeM('m-assign')">Annulla</button><button class="btn btn-p" onclick="saveAssign()">Assegna</button></div>
</div></div>
<div class="toast" id="toast"></div>
<script>
const API_KEY='ianni-booking-2026';
function getHeaders(){const h={'Content-Type':'application/json','x-api-key':API_KEY};const t=localStorage.getItem('booking_token');if(t)h['Authorization']='Bearer '+t;return h}
let user=null,editSvcId=null,provs=[];
const SL={confirmed:'Confermata',completed:'Completata',cancelled:'Cancellata',no_show:'No-show'};
const TT={dashboard:'Dashboard',bookings:'Prenotazioni',calendar:'Calendario',services:'Servizi',providers:'Operatori',customers:'Clienti',settings:'Impostazioni'};
const $=id=>document.getElementById(id);
async function api(url,opt){const r=await fetch(url,{headers:getHeaders(),...(opt||{})});if(!r.ok)throw new Error((await r.json()).detail||r.statusText);return r.json()}
/* Auth */
async function checkAuth(){
const params=new URLSearchParams(location.search);
if(params.get('token')){localStorage.setItem('booking_token',params.get('token'));history.replaceState(null,'','/admin')}
if(params.get('logout')){localStorage.removeItem('booking_token');history.replaceState(null,'','/admin')}
const token=localStorage.getItem('booking_token');
if(!token)return showLogin();
try{const r=await fetch('/auth/me',{headers:{'Authorization':'Bearer '+token}});
if(!r.ok){localStorage.removeItem('booking_token');return showLogin()}
user=await r.json();$('u-name').textContent=user.name||user.email;$('u-email').textContent=user.email;showApp()
}catch(e){showLogin()}}
function showLogin(){$('login-screen').style.display='flex';$('app').style.display='none'}
function showApp(){$('login-screen').style.display='none';$('app').style.display='block';loadAll()}
function logout(){localStorage.removeItem('booking_token');location.href='/auth/logout'}
/* Nav */
function go(t){document.querySelectorAll('.tab').forEach(e=>e.classList.remove('active'));document.querySelectorAll('.sb-item').forEach(e=>e.classList.remove('active'));$('t-'+t).classList.add('active');document.querySelector(`[data-tab="${t}"]`).classList.add('active');$('tb-title').textContent=TT[t]}
/* Dashboard */
async function loadStats(){try{
const d=await api('/api/admin/dashboard');
const k=d.kpi;
$('k-today').textContent=k.today;$('k-tomorrow').textContent=k.tomorrow||0;$('k-week').textContent=k.week;
$('k-noshow').textContent=k.no_shows;$('k-customers').textContent=k.customers;
$('tb-pill').textContent=k.today>0?k.today+' oggi':'';
// Week chart
const maxC=Math.max(...d.week_chart.map(x=>x.count),1);
$('d-weekchart').innerHTML=d.week_chart.map(x=>{
const pct=Math.round(x.count/maxC*100);
const bg=x.is_today?'var(--green-400)':'var(--navy-200)';
return '<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:.25rem">'
+'<span style="font-size:.8125rem;font-weight:700;color:var(--navy-500)">'+x.count+'</span>'
+'<div style="width:100%;height:'+Math.max(pct,4)+'%;background:'+bg+';border-radius:4px 4px 0 0;min-height:4px;transition:height .3s"></div>'
+'<span style="font-size:.6875rem;font-weight:600;color:'+(x.is_today?'var(--green-600)':'var(--n400)')+'">'+x.day+'</span></div>';
}).join('');
// Top services
const maxS=Math.max(...d.top_services.map(x=>x.count),1);
$('d-topsvcs').innerHTML=d.top_services.map(x=>{
const pct=Math.round(x.count/maxS*100);
return '<div style="margin-bottom:.75rem"><div style="display:flex;justify-content:space-between;font-size:.8125rem;margin-bottom:.25rem"><span style="font-weight:600">'+x.name+'</span><span style="color:var(--n400)">'+x.count+'</span></div>'
+'<div style="height:6px;background:var(--n100);border-radius:3px;overflow:hidden"><div style="width:'+pct+'%;height:100%;background:var(--green-400);border-radius:3px"></div></div></div>';
}).join('')||'<div class="empty" style="padding:1rem">Nessun dato</div>';
// Upcoming
if(d.upcoming.length){
let h='<table><tbody>';
d.upcoming.forEach(u=>{
const dt=new Date(u.start_at);const ts=dt.toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'});
const ds=dt.toLocaleDateString('it-IT',{day:'2-digit',month:'short'});
h+='<tr><td><strong>'+ts+'</strong><br><span style="font-size:.75rem;color:var(--n400)">'+ds+'</span></td><td>'+u.customer_name+'</td><td style="color:var(--n500)">'+u.service+'</td><td style="font-size:.8125rem;color:var(--n400)">'+u.provider+'</td></tr>';
});
$('d-upcoming').innerHTML=h+'</tbody></table>';
}else{$('d-upcoming').innerHTML='<div class="empty">Nessun appuntamento imminente</div>'}
// Recent
if(d.recent.length){
let h='<table><tbody>';
d.recent.forEach(r=>{
const dt=new Date(r.created_at);const ds=dt.toLocaleDateString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'});
h+='<tr><td style="font-size:.8125rem;color:var(--n400)">'+ds+'</td><td>'+r.customer_name+'</td><td style="color:var(--n500)">'+(r.service||'')+'</td><td><span class="bs bs-'+r.status+'">'+(SL[r.status]||r.status)+'</span></td></tr>';
});
$('d-recent').innerHTML=h+'</tbody></table>';
}else{$('d-recent').innerHTML='<div class="empty">Nessuna attività</div>'}
}catch(e){console.error(e)}}
async function loadToday(){}
/* Bookings */
let bkPage=1;
async function loadBookings(pg){if(pg)bkPage=pg;try{let u='/api/admin/bookings?page='+bkPage+'&per_page=15&';const d=$('f-date').value,s=$('f-status').value,p=$('f-prov').value;if(d)u+='date='+d+'&';if(s)u+='status='+s+'&';if(p)u+='provider_id='+p+'&';const res=await api(u);if(!res.items.length){$('d-bookings').innerHTML='<div class="empty">Nessuna prenotazione trovata</div>';return}$('d-bookings').innerHTML=bTable(res.items,false)+pgHtml(res.page,res.pages,res.total,'loadBookings')}catch(e){console.error(e)}}
function bTable(data,short){
let h='<table><thead><tr>';if(!short)h+='<th>Data</th>';
h+='<th>Ora</th><th>Cliente</th><th>Servizio</th><th>Operatore</th><th>Stato</th>';if(!short)h+='<th>Note</th>';h+='<th></th></tr></thead><tbody>';
data.forEach(b=>{const dt=new Date(b.start_at);const ds=dt.toLocaleDateString('it-IT',{day:'2-digit',month:'short'});const ts=dt.toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'});
h+='<tr>';if(!short)h+=`<td><strong>${ds}</strong></td>`;
h+=`<td><strong>${ts}</strong></td><td>${b.customer_name}<br><span style="font-size:.8125rem;color:var(--n400)">${b.customer_phone}</span></td><td>${b.service?.name||'—'}</td><td>${b.provider?.name||'—'}</td><td><span class="bs bs-${b.status}">${SL[b.status]||b.status}</span></td>`;
if(!short)h+=`<td style="font-size:.875rem;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${b.notes||''}</td>`;
h+='<td>';if(b.status==='confirmed')h+=`<button class="btn btn-s btn-g" onclick="updB(${b.id},'completed')" title="Completata">✓</button><button class="btn btn-s btn-g btn-d" onclick="updB(${b.id},'no_show')" title="No-show">✗</button>${short?'':`<button class="btn btn-s btn-g btn-d" onclick="updB(${b.id},'cancelled')" title="Cancella">✕</button>`}`;
h+='</td></tr>'});
return h+'</tbody></table>';
}
async function updB(id,status){if(!confirm('Cambiare stato a "'+SL[status]+'"?'))return;try{await api('/api/admin/bookings/'+id,{method:'PUT',body:JSON.stringify({status})});toast('Prenotazione aggiornata');loadAll()}catch(e){toast('Errore: '+e.message)}}
/* Services */
async function loadServices(){try{const data=await api('/api/admin/services');if(!data.length){$('d-services').innerHTML='<div class="empty">Nessun servizio configurato</div>';return}
let h='<table><thead><tr><th>Nome</th><th>Slug</th><th>Durata</th><th>Categoria</th><th>Descrizione</th><th></th></tr></thead><tbody>';
data.forEach(s=>{h+=`<tr><td><strong>${s.name}</strong></td><td style="color:var(--n400)">${s.slug}</td><td>${s.duration_min} min</td><td>${s.category}</td><td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.description||'—'}</td><td><button class="btn btn-s btn-g" onclick="editSvc(${s.id},'${esc(s.name)}','${s.slug}',${s.duration_min},'${s.category}','${esc(s.description||'')}')">✎</button><button class="btn btn-s btn-g btn-d" onclick="delSvc(${s.id},'${esc(s.name)}')">🗑</button></td></tr>`});
$('d-services').innerHTML=h+'</tbody></table>'}catch(e){console.error(e)}}
function openSvcModal(){editSvcId=null;$('m-svc-t').textContent='Nuovo servizio';$('s-name').value='';$('s-slug').value='';$('s-dur').value=30;$('s-cat').value='generale';$('s-desc').value='';openM('m-svc')}
function editSvc(id,name,slug,dur,cat,desc){editSvcId=id;$('m-svc-t').textContent='Modifica servizio';$('s-name').value=name;$('s-slug').value=slug;$('s-dur').value=dur;$('s-cat').value=cat;$('s-desc').value=desc;openM('m-svc')}
async function saveSvc(){const body={name:$('s-name').value,slug:$('s-slug').value,duration_min:parseInt($('s-dur').value),category:$('s-cat').value,description:$('s-desc').value};if(!body.name||!body.slug){toast('Nome e slug obbligatori');return}try{const url=editSvcId?'/api/admin/services/'+editSvcId:'/api/admin/services';await api(url,{method:editSvcId?'PUT':'POST',body:JSON.stringify(body)});closeM('m-svc');toast(editSvcId?'Servizio aggiornato':'Servizio creato');loadServices()}catch(e){toast('Errore: '+e.message)}}
async function delSvc(id,name){if(!confirm('Eliminare "'+name+'"?'))return;try{await fetch('/api/admin/services/'+id,{method:'DELETE',headers:getHeaders()});toast('Servizio eliminato');loadServices()}catch(e){toast('Errore')}}
const DAYS=['Lun','Mar','Mer','Gio','Ven','Sab','Dom'];
let provDetail=[];
async function loadProviders(){
try{
provs=await api('/api/admin/providers');
// Populate filters
const sel=$('f-prov');const v=sel.value;
sel.innerHTML='<option value="">Tutti gli operatori</option>';
provs.forEach(p=>{sel.innerHTML+=`<option value="${p.id}">${p.name}</option>`});sel.value=v;
const csel=$('cal-prov-filter');if(csel){const cv=csel.value;csel.innerHTML='<option value="">Tutti gli operatori</option>';provs.forEach(p=>{csel.innerHTML+=`<option value="${p.id}">${p.name}</option>`});csel.value=cv}
// Load detail view
provDetail=await api('/api/admin/providers/detail');
renderProviders();
}catch(e){console.error(e)}
}
function renderProviders(){
const el=$('d-providers-detail');
if(!provDetail.length){el.innerHTML='<div class="empty">Nessun operatore configurato</div>';return}
let h='';
provDetail.forEach(p=>{
h+=`<div class="prov-card"><div class="prov-card__hd"><div><span class="prov-card__name">${p.name}</span><div class="prov-card__meta">${p.email||''} ${p.phone?'· '+p.phone:''}</div></div><button class="btn btn-s btn-g" onclick="openAssignModal(${p.id},'${esc(p.name)}')">+ Assegna servizio</button></div>`;
if(p.services.length){
p.services.forEach(s=>{
const days=s.availability_rules.map(r=>'<span class="svc-day">'+DAYS[r.weekday]+' '+r.start+'-'+r.end+'</span>').join('');
h+=`<div class="svc-row"><span class="svc-name">${s.service_name}</span><div class="svc-days">${days}</div><span class="svc-time">${s.duration_min} min</span><button class="btn btn-s btn-g btn-d" onclick="removeAssign(${p.id},${s.service_id},'${esc(s.service_name)}')">✕</button></div>`;
});
}else{h+='<div style="padding:1.25rem 1.75rem;color:var(--n400);font-size:.9375rem">Nessun servizio assegnato</div>'}
h+='</div>';
});
el.innerHTML=h;
}
async function removeAssign(pid,sid,name){if(!confirm('Rimuovere "'+name+'" da questo operatore?'))return;try{await fetch('/api/admin/providers/'+pid+'/services/'+sid,{method:'DELETE',headers:getHeaders()});toast('Assegnazione rimossa');loadProviders()}catch(e){toast('Errore')}}
/* Calendar */
let calStart=null;
function initCal(){const d=new Date();calStart=new Date(d);calStart.setDate(d.getDate()-d.getDay()+1);loadCalendar()}
function calNav(days){calStart.setDate(calStart.getDate()+days);loadCalendar()}
async function loadCalendar(){
const from=fmt(calStart);const toD=new Date(calStart);toD.setDate(toD.getDate()+6);const to=fmt(toD);
$('cal-range').textContent=calStart.toLocaleDateString('it-IT',{day:'numeric',month:'long'})+' — '+toD.toLocaleDateString('it-IT',{day:'numeric',month:'long',year:'numeric'});
const pf=$('cal-prov-filter').value;
let url='/api/admin/calendar?from_date='+from+'&to_date='+to;
if(pf)url+='&provider_id='+pf;
try{
const cal=await api(url);
renderCal(cal,calStart);
}catch(e){console.error(e)}
}
function renderCal(cal,weekStart){
const today=new Date().toISOString().split('T')[0];
const hours=[];for(let h=7;h<=20;h++)hours.push(h);
let g='<div class="cal-grid">';
// Header row
g+='<div class="cal-hdr"></div>';
for(let d=0;d<7;d++){
const day=new Date(weekStart);day.setDate(day.getDate()+d);
const ds=fmt(day);const isToday=ds===today;
g+=`<div class="cal-hdr${isToday?' today':''}">${DAYS[d]} ${day.getDate()}</div>`;
}
// Time rows
hours.forEach(h=>{
g+=`<div class="cal-time">${String(h).padStart(2,'0')}:00</div>`;
for(let d=0;d<7;d++){
const day=new Date(weekStart);day.setDate(day.getDate()+d);
const ds=fmt(day);
const evts=(cal[ds]||[]).filter(e=>{const eh=parseInt(e.start.split(':')[0]);return eh===h});
let cells='';
evts.forEach(e=>{
const startMin=parseInt(e.start.split(':')[1]);
const endH=parseInt(e.end.split(':')[0]);const endMin=parseInt(e.end.split(':')[1]);
const durMin=(endH*60+endMin)-(h*60+startMin);
const top=startMin/60*40;const height=Math.max(durMin/60*40,18);
const cat=e.category||'generale';
cells+=`<div class="cal-ev cat-${cat}" style="top:${top}px;height:${height}px" title="${e.start}-${e.end} ${e.service}\n${e.customer}\n${e.provider}">${e.start} ${e.customer.split(' ')[0]}</div>`;
});
g+=`<div class="cal-cell">${cells}</div>`;
}
});
g+='</div>';
$('d-calendar').innerHTML=g;
}
function fmt(d){return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0')}
function openProvModal(){openM('m-prov');$('p-name').value='';$('p-email').value='';$('p-phone').value=''}
async function saveProv(){const body={name:$('p-name').value,email:$('p-email').value||null,phone:$('p-phone').value||null};if(!body.name){toast('Nome obbligatorio');return}try{await api('/api/admin/providers',{method:'POST',body:JSON.stringify(body)});closeM('m-prov');toast('Operatore creato');loadProviders()}catch(e){toast('Errore: '+e.message)}}
/* Helpers */
function esc(s){return(s||'').replace(/'/g,"\\'")}
function openM(id){$(id).classList.add('open')}
function closeM(id){$(id).classList.remove('open')}
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2500)}
document.querySelectorAll('.mo').forEach(el=>{el.addEventListener('click',e=>{if(e.target===el)el.classList.remove('open')})});
/* Customers */
let custPage=1;let custQuery='';
async function loadCustomers(pg){if(pg)custPage=pg;try{const q=$('cust-search').value;custQuery=q;const res=await api('/api/admin/customers?page='+custPage+'&per_page=15'+(q?'&q='+encodeURIComponent(q):''));$('cust-count').textContent=res.total+' clienti';renderCustomers(res.items,res)}catch(e){console.error(e)}}
function searchCustomers(){custPage=1;loadCustomers()}
function renderCustomers(list,res){
if(!list.length){$('d-customers').innerHTML='<div class="empty">Nessun cliente trovato</div>';return}
let h='<table><thead><tr><th>Nome</th><th>Telefono</th><th>Email</th><th>Visite</th><th>Ultima visita</th><th>Note</th></tr></thead><tbody>';
list.forEach(c=>{
const lv=c.last_visit_at?new Date(c.last_visit_at).toLocaleDateString('it-IT',{day:'2-digit',month:'short',year:'numeric'}):'—';
h+=`<tr style="cursor:pointer" onclick="openCustomerDetail(${c.id})"><td><strong>${c.name}</strong></td><td style="font-size:.875rem">${c.phone}</td><td style="font-size:.875rem;color:var(--n400)">${c.email||'—'}</td><td style="text-align:center"><strong>${c.total_visits}</strong></td><td style="font-size:.875rem;color:var(--n400)">${lv}</td><td style="font-size:.8125rem;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--n500)">${c.notes||''}</td></tr>`;
});
$('d-customers').innerHTML=h+'</tbody></table>'+(res?pgHtml(res.page,res.pages,res.total,'loadCustomers'):'');
}
async function openCustomerDetail(id){
try{
const c=await api('/api/admin/customers/'+id);
let h='<h3 style="margin-bottom:.5rem">'+c.name+'</h3>';
h+='<div style="font-size:.875rem;color:var(--n500);margin-bottom:1rem">'+c.phone+(c.email?' · '+c.email:'')+'</div>';
h+='<div class="fg"><label>Note</label><textarea class="inp" id="cust-notes" rows="2" style="width:100%">'+(c.notes||'')+'</textarea></div>';
h+='<button class="btn btn-p btn-s" onclick="saveCustNotes('+c.id+')">Salva note</button>';
if(c.top_services.length){h+='<div style="margin-top:1.25rem;font-size:.875rem;font-weight:600;color:var(--navy-500)">Servizi più frequenti</div>';c.top_services.forEach(s=>{h+='<span class="bs" style="background:var(--navy-50);color:var(--navy-500);margin:.25rem .25rem 0 0">'+s.name+' ('+s.count+')</span>'})}
if(c.bookings.length){h+='<div style="margin-top:1.25rem;font-size:.875rem;font-weight:600;color:var(--navy-500);margin-bottom:.5rem">Storico prenotazioni</div>';
h+='<table><thead><tr><th>Data</th><th>Servizio</th><th>Operatore</th><th>Stato</th></tr></thead><tbody>';
c.bookings.forEach(b=>{const dt=new Date(b.start_at);const ds=dt.toLocaleDateString('it-IT',{day:'2-digit',month:'short',year:'numeric'});const ts=dt.toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'});
h+='<tr><td>'+ds+' '+ts+'</td><td>'+b.service+'</td><td>'+(b.provider||'—')+'</td><td><span class="bs bs-'+b.status+'">'+(SL[b.status]||b.status)+'</span></td></tr>'});
h+='</tbody></table>'}
$('m-cust-body').innerHTML=h;openM('m-cust');
}catch(e){toast('Errore: '+e.message)}
}
async function saveCustNotes(id){try{await api('/api/admin/customers/'+id,{method:'PUT',body:JSON.stringify({notes:$('cust-notes').value})});toast('Note salvate');loadCustomers()}catch(e){toast('Errore')}}
/* Settings */
let cfg={};
async function loadSettings(){try{cfg=await api('/api/admin/settings');const keys=['pharmacy_name','pharmacy_address','pharmacy_phone','pharmacy_wa','smtp_host','smtp_port','smtp_user','smtp_pass','smtp_from'];keys.forEach(k=>{const el=$('cfg-'+k);if(el)el.value=cfg[k]||''});$('cfg-wa_enabled').checked=cfg.wa_enabled==='true';$('cfg-smtp_enabled').checked=cfg.smtp_enabled==='true';$('smtp-badge').textContent=cfg.smtp_enabled==='true'?'Attivo':'Disattivo';$('smtp-badge').className='bs '+(cfg.smtp_enabled==='true'?'bs-confirmed':'bs-cancelled');checkWaStatus()}catch(e){console.error(e)}}
async function saveSettings(keys){const body={};keys.forEach(k=>{const el=$('cfg-'+k);if(el)body[k]=el.value});try{cfg=await api('/api/admin/settings',{method:'PUT',body:JSON.stringify(body)});toast('Impostazioni salvate')}catch(e){toast('Errore: '+e.message)}}
function toggleSetting(key,val){saveSettings([]);const body={};body[key]=val?'true':'false';api('/api/admin/settings',{method:'PUT',body:JSON.stringify(body)}).then(d=>{cfg=d;if(key==='smtp_enabled'){$('smtp-badge').textContent=val?'Attivo':'Disattivo';$('smtp-badge').className='bs '+(val?'bs-confirmed':'bs-cancelled')}if(key==='wa_enabled'){$('wa-badge').textContent=val?'Attivo':'Disattivo';$('wa-badge').className='bs '+(val?'bs-confirmed':'bs-cancelled')}toast(val?'Attivato':'Disattivato')}).catch(e=>toast('Errore'))}
let waTimer=null;let waPollInterval=null;
async function checkWaStatus(){
stopWaPoll();
try{
const d=await api('/api/admin/settings/wa-status');
if(d.connected){
$('wa-status-box').innerHTML='<div style="display:flex;align-items:center;gap:.75rem"><span class="bs bs-confirmed">Connesso</span><span style="font-size:.875rem;color:var(--n500)">'+(d.phone||'Numero collegato')+'</span></div>';
$('wa-badge').textContent='Connesso';$('wa-badge').className='bs bs-confirmed';
$('wa-qr-box').style.display='none';
}else{
$('wa-status-box').innerHTML='<div style="display:flex;align-items:center;gap:.75rem"><span class="bs bs-cancelled">Disconnesso</span><span style="font-size:.875rem;color:var(--n400)">'+(d.message||'Servizio non raggiungibile')+'</span></div>';
$('wa-badge').textContent='Disconnesso';$('wa-badge').className='bs bs-cancelled';
loadWaQr();
startWaPoll();
}
}catch(e){
$('wa-status-box').innerHTML='<p style="color:var(--n400);font-size:.9375rem">Servizio WhatsApp non disponibile. Verrà attivato con Baileys.</p>';
$('wa-badge').textContent='Non configurato';$('wa-badge').className='bs bs-no_show';
}
}
async function loadWaQr(){
try{
const d=await api('/api/admin/settings/wa-qr');
const box=$('wa-qr-box'),img=$('wa-qr-img'),timer=$('wa-qr-timer'),overlay=$('wa-qr-overlay');
if(d.qr){
box.style.display='block';img.src=d.qr;overlay.style.display='none';
startQrCountdown(d.expires_in||20,d.ttl||20);
}else if(d.expired){
box.style.display='block';overlay.style.display='flex';
timer.textContent='QR scaduto — clicca per rigenerare';
}else{
timer.textContent=d.message||'QR in arrivo...';
box.style.display='block';img.style.opacity='.3';overlay.style.display='none';
setTimeout(loadWaQr,3000);
}
}catch(e){$('wa-qr-box').style.display='none'}
}
function startQrCountdown(sec,ttl){
if(waTimer)clearInterval(waTimer);
let left=sec;
const timer=$('wa-qr-timer');
const img=$('wa-qr-img');
const overlay=$('wa-qr-overlay');
img.style.opacity='1';
timer.innerHTML=timerHtml(left,ttl);
waTimer=setInterval(()=>{
left--;
if(left<=0){
clearInterval(waTimer);waTimer=null;
overlay.style.display='flex';
timer.textContent='QR scaduto';
setTimeout(loadWaQr,1000);
}else{
timer.innerHTML=timerHtml(left,ttl);
if(left<=5)img.style.opacity='.4';
}
},1000);
}
function timerHtml(left,ttl){
const pct=Math.round(left/ttl*100);
const color=left>10?'var(--green-500)':left>5?'var(--warn)':'var(--err)';
return '<div style="display:flex;align-items:center;gap:.75rem;justify-content:center">'
+'<div style="flex:1;max-width:180px;height:4px;background:var(--n200);border-radius:2px;overflow:hidden">'
+'<div style="width:'+pct+'%;height:100%;background:'+color+';border-radius:2px;transition:width 1s linear"></div></div>'
+'<span style="font-size:.8125rem;font-weight:600;color:'+color+'">'+left+'s</span></div>';
}
function startWaPoll(){stopWaPoll();waPollInterval=setInterval(async()=>{
try{const d=await api('/api/admin/settings/wa-status');if(d.connected){checkWaStatus()}}catch(e){}
},5000)}
function stopWaPoll(){if(waPollInterval){clearInterval(waPollInterval);waPollInterval=null}}
async function testEmail(){try{const d=await api('/api/admin/settings/test-email',{method:'POST'});toast(d.message||'Email inviata')}catch(e){toast('Errore: '+e.message)}}
function loadAll(){loadStats();loadBookings();loadServices();loadProviders();loadCustomers();loadSettings();initCal()}
let assignProviderId=null;
function openAssignModal(pid,pname){
assignProviderId=pid;
$('m-assign-t').textContent='Assegna servizio a '+pname;
// Populate service select
const sel=$('a-svc');sel.innerHTML='';
api('/api/admin/services').then(svcs=>{
svcs.forEach(s=>{sel.innerHTML+=`<option value="${s.id}">${s.name} (${s.duration_min} min)</option>`});
});
// Render day toggles
const daysEl=$('a-days');daysEl.innerHTML='';
DAYS.forEach((d,i)=>{
daysEl.innerHTML+=`<label style="display:flex;align-items:center;gap:.375rem"><input type="checkbox" class="a-day-chk" data-day="${i}"> ${d}</label><input class="inp inp-s a-day-start" data-day="${i}" type="time" value="09:00"><input class="inp inp-s a-day-end" data-day="${i}" type="time" value="13:00">`;
});
openM('m-assign');
}
async function saveAssign(){
const sid=parseInt($('a-svc').value);
const rules=[];
document.querySelectorAll('.a-day-chk:checked').forEach(chk=>{
const d=parseInt(chk.dataset.day);
const start=document.querySelector(`.a-day-start[data-day="${d}"]`).value;
const end=document.querySelector(`.a-day-end[data-day="${d}"]`).value;
if(start&&end)rules.push({weekday:d,start,end});
});
if(!rules.length){toast('Seleziona almeno un giorno');return}
try{
await api('/api/admin/providers/'+assignProviderId+'/services/'+sid,{method:'POST',body:JSON.stringify(rules)});
closeM('m-assign');toast('Servizio assegnato');loadProviders();
}catch(e){toast('Errore: '+e.message)}
}
function pgHtml(page,pages,total,fn){
if(pages<=1)return '<div class="pgn"><span>'+total+' risultati</span></div>';
let h='<div class="pgn">';
h+='<button '+(page<=1?'disabled':'')+' onclick="'+fn+'('+(page-1)+')">←</button>';
const start=Math.max(1,page-2),end=Math.min(pages,page+2);
if(start>1)h+='<button onclick="'+fn+'(1)">1</button>';
if(start>2)h+='<span>…</span>';
for(let i=start;i<=end;i++)h+='<button class="'+(i===page?'active':'')+'" onclick="'+fn+'('+i+')">'+i+'</button>';
if(end<pages-1)h+='<span>…</span>';
if(end<pages)h+='<button onclick="'+fn+'('+pages+')">'+pages+'</button>';
h+='<button '+(page>=pages?'disabled':'')+' onclick="'+fn+'('+(page+1)+')">→</button>';
h+='<span style="margin-left:.5rem">'+total+' risultati</span></div>';
return h;
}
checkAuth();
</script>
</body>
</html>