v1.3.0 — pannello admin completo, auth localStorage, Baileys WA, customers, calendario, paginazione, dashboard 7gg

This commit is contained in:
2026-04-12 17:46:08 +00:00
commit c33ec8450e
31 changed files with 3072 additions and 0 deletions

773
frontend/admin.html Normal file
View File

@@ -0,0 +1,773 @@
<!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>

5
frontend/favicon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'>
<rect width='32' height='32' rx='6' fill='#002c50'/>
<rect x='13' y='7' width='6' height='18' rx='1.5' fill='#80ba27'/>
<rect x='7' y='13' width='18' height='6' rx='1.5' fill='#80ba27'/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

339
frontend/index.html Normal file
View File

@@ -0,0 +1,339 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<title>Prenota — Farmacia Ianni</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
colors: {
teal: {
50: '#f0faf4',
100: '#d1f0df',
500: '#3a9d6a',
600: '#2d8a5e',
700: '#1f6d48',
}
}
}
}
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
.bg-blob-1 { background: radial-gradient(ellipse at 20% 50%, rgba(167, 225, 195, 0.4) 0%, transparent 60%); }
.bg-blob-2 { background: radial-gradient(ellipse at 80% 70%, rgba(120, 200, 170, 0.35) 0%, transparent 55%); }
.bg-blob-3 { background: radial-gradient(ellipse at 90% 30%, rgba(167, 225, 195, 0.3) 0%, transparent 50%); }
.time-slot { transition: all 0.15s ease; }
.time-slot:hover { border-color: #2d8a5e; background-color: #f0faf4; }
.time-slot.selected { background-color: #2d8a5e; color: white; border-color: #2d8a5e; }
.svc-card { transition: all 0.2s ease; cursor: pointer; }
.svc-card:hover { border-color: #2d8a5e; background: rgba(255,255,255,0.85); }
.fade { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
input:focus { outline:none; border-color:#2d8a5e; box-shadow:0 0 0 3px rgba(45,138,94,0.1); }
</style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center p-6" style="background-color: #ffffff;">
<!-- Background blobs -->
<div class="fixed inset-0 bg-blob-1 pointer-events-none"></div>
<div class="fixed inset-0 bg-blob-2 pointer-events-none"></div>
<div class="fixed inset-0 bg-blob-3 pointer-events-none"></div>
<!-- Logo centrato sopra la card -->
<div class="mb-6">
<img src="/static/logo.jpg" alt="Farmacia Ianni" class="h-auto w-auto max-h-[80px]">
</div>
<!-- Main Card -->
<div class="relative w-full max-w-[1020px] bg-white/60 backdrop-blur-sm rounded-3xl shadow-lg overflow-hidden flex min-h-[580px]">
<!-- Left Sidebar -->
<div class="w-[280px] flex-shrink-0 px-10 py-10 flex flex-col justify-between border-r border-white/40" style="background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, rgba(200,230,215,0.25) 100%);">
<div>
<!-- Logo reale Farmacia Ianni -->
<!-- Stepper dinamico -->
<div class="flex flex-col" id="stepper"></div>
</div>
<!-- Help -->
<div class="flex items-center gap-2 text-gray-500 text-sm mt-6">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><circle cx="12" cy="17" r="0.5" fill="currentColor"/></svg>
<a href="https://wa.me/393930579002" target="_blank" style="color:inherit;text-decoration:none">Serve aiuto?</a>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 px-10 py-10 flex flex-col relative overflow-hidden">
<!-- Decorative gradient blobs in content area -->
<div class="absolute top-0 right-0 w-64 h-64 rounded-full opacity-30 pointer-events-none" style="background: radial-gradient(circle, rgba(120,200,170,0.5) 0%, transparent 70%); transform: translate(30%, -20%);"></div>
<div class="absolute bottom-0 right-0 w-80 h-80 rounded-full opacity-25 pointer-events-none" style="background: radial-gradient(circle, rgba(100,190,160,0.6) 0%, transparent 65%); transform: translate(20%, 30%);"></div>
<div id="main" class="relative z-10 flex-1 flex flex-col"></div>
</div>
</div>
<script>
const API = location.origin;
const STEPS = ['Servizio', 'Data e ora', 'I tuoi dati', 'Conferma'];
const MO = ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'];
const DL = ['Domenica','Lunedì','Martedì','Mercoledì','Giovedì','Venerdì','Sabato'];
const DS = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'];
let step=0, svcs=[], sel=null, weekStart, selDate=null, slots=[], selSlot=null, lastPhone='';
const fd = d => d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');
const fl = d => DL[d.getDay()]+' '+d.getDate()+' '+MO[d.getMonth()];
function dayLabel(d) {
const t=new Date(); t.setHours(0,0,0,0);
const tm=new Date(t); tm.setDate(tm.getDate()+1);
if(d.toDateString()===t.toDateString()) return 'Oggi';
if(d.toDateString()===tm.toDateString()) return 'Domani';
return DS[d.getDay()===0?6:d.getDay()-1];
}
const CHK = '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>';
const CHKBIG = '<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>';
// ── Stepper ──
function renderStepper() {
document.getElementById('stepper').innerHTML = STEPS.map((s,i) => {
const done=i<step, cur=i===step, fut=i>step;
let dot;
if(done) dot = '<div class="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center flex-shrink-0">'+CHK+'</div>';
else if(cur) dot = '<div class="w-8 h-8 rounded-full bg-teal-600 flex items-center justify-center flex-shrink-0"><span class="text-white font-semibold text-sm">'+(i+1)+'</span></div>';
else dot = '<div class="w-8 h-8 rounded-full border-2 border-gray-300 flex items-center justify-center flex-shrink-0 bg-white/50"><span class="text-gray-400 font-medium text-sm">'+(i+1)+'</span></div>';
let label = '<span class="'+(cur?'text-gray-900 font-semibold':done?'text-gray-700 font-medium':'text-gray-400 font-medium')+' text-[15px]">'+s+'</span>';
let line = i<3 ? '<div class="ml-[15px] w-[2px] h-8 '+(done?'bg-teal-500':'bg-gray-300')+'"></div>' : '';
return '<div class="flex items-center gap-4">'+dot+label+'</div>'+line;
}).join('');
}
// ── Init ──
async function init() {
weekStart = new Date(); weekStart.setHours(0,0,0,0);
try { svcs = await (await fetch(API+'/api/services')).json(); } catch(e) { svcs = []; }
go(0);
}
function go(n) { step=n; renderStepper(); render(); }
function render() {
const m = document.getElementById('main');
if(step===0) renderServices(m);
else if(step===1) renderDatetime(m);
else if(step===2) renderForm(m);
else renderConfirm(m);
}
// ── STEP 0: Servizi ──
function renderServices(m) {
const g = {};
svcs.forEach(s => (g[s.category] = g[s.category]||[]).push(s));
let html = '<div class="fade">';
html += '<h1 class="text-2xl font-bold text-gray-900 mb-6">Scegli il servizio</h1>';
for (const [cat, list] of Object.entries(g)) {
html += '<div class="text-xs font-bold uppercase tracking-widest text-teal-600 mb-2 mt-5">'+cat+'</div>';
for (const s of list) {
html += '<div class="svc-card flex items-center justify-between p-4 mb-2 rounded-2xl bg-white/50 border-2 border-transparent" onclick="pickSvc('+s.id+')">';
html += '<div><div class="font-semibold text-[15px] text-gray-900">'+s.name+'</div>';
if(s.description) html += '<div class="text-[13px] text-gray-500 mt-0.5">'+s.description+'</div>';
html += '</div>';
html += '<span class="text-xs font-semibold text-teal-600 bg-teal-50 px-3 py-1.5 rounded-full ml-4 whitespace-nowrap">'+s.duration_min+' min</span>';
html += '</div>';
}
}
html += '</div>';
m.innerHTML = html;
}
function pickSvc(id) { sel=svcs.find(s=>s.id===id); selDate=null; selSlot=null; slots=[]; go(1); }
// ── STEP 1: Data e Ora (Data e Ora) ──
function renderDatetime(m) {
const wk = [];
for(let i=0; i<7; i++) { const d=new Date(weekStart); d.setDate(d.getDate()+i); wk.push(d); }
const today = new Date(); today.setHours(0,0,0,0);
const monthLabel = MO[wk[3].getMonth()]+', '+wk[3].getFullYear();
// Slot HTML
let slotsHtml = '';
if(selDate && slots.length > 0) {
// Righe da 7 7 per riga
const rows = [];
for(let i=0; i<slots.length; i+=7) rows.push(slots.slice(i, i+7));
slotsHtml = rows.map(row =>
'<div class="flex flex-wrap gap-2.5 mb-2.5">' +
row.map(s => '<button class="time-slot px-4 py-2 rounded-full border '+(selSlot&&selSlot.start===s.start ? 'border-teal-600 bg-teal-600 text-white selected' : 'border-gray-200 bg-white text-gray-700')+' text-sm font-medium" onclick="pickSlot(\''+s.start+'\',\''+s.end+'\','+s.provider_id+')">'+s.start+'</button>').join('') +
'</div>'
).join('');
} else if(selDate && slots.length === 0) {
slotsHtml = '<div class="text-center py-8 text-gray-500"><div class="text-3xl mb-2">😔</div><div class="font-medium text-gray-700">Nessun orario disponibile</div><div class="text-sm mt-1">Prova un altro giorno</div></div>';
}
// Selected box
let selectedBox = '';
if(selSlot) {
selectedBox = '<div class="bg-amber-50/80 border border-amber-200/60 rounded-xl px-5 py-4 mb-8">' +
'<p class="text-sm text-gray-600 mb-1">Hai scelto:</p>' +
'<div class="flex items-center gap-2">' +
'<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>' +
'<span class="text-sm font-medium text-gray-800">'+fl(selDate)+', '+selSlot.start+' - '+selSlot.end+'</span>' +
'</div></div>';
}
// Day strip
let dayStrip = wk.map(d => {
const dis = d < today || d.getDay() === 0;
const act = selDate && d.toDateString() === selDate.toDateString();
return '<div class="flex flex-col items-center px-3 py-2 '+(dis?'opacity-30 cursor-default':act?'cursor-pointer rounded-xl border-b-2 border-gray-800':'cursor-pointer rounded-xl hover:bg-white/60')+'" '+(dis?'':'onclick="pickDate(\''+fd(d)+'\')"')+'>' +
'<span class="text-xs '+(act?'font-semibold text-gray-800':'text-gray-500')+' mb-1">'+dayLabel(d)+'</span>' +
'<span class="text-lg '+(act?'font-bold text-gray-900':'font-medium text-gray-600')+'">'+d.getDate()+'</span>' +
'</div>';
}).join('');
m.innerHTML = '<div class="fade flex-1 flex flex-col">' +
// Header con nav mese
'<div class="flex items-center justify-between mb-8">' +
'<h1 class="text-2xl font-bold text-gray-900">Scegli data e ora</h1>' +
'<div class="flex items-center gap-3 bg-white rounded-full px-4 py-2 shadow-sm border border-gray-100">' +
'<button class="text-gray-500 hover:text-gray-700 text-sm" onclick="navWeek(-1)"><i class="fas fa-chevron-left"></i></button>' +
'<span class="text-sm font-medium text-gray-800">'+monthLabel+'</span>' +
'<button class="text-gray-500 hover:text-gray-700 text-sm" onclick="navWeek(1)"><i class="fas fa-chevron-right"></i></button>' +
'</div>' +
'</div>' +
// Date selector
'<div class="flex items-center gap-1 mb-8">' +
'<button class="text-gray-400 hover:text-gray-600 px-1" onclick="navWeek(-1)"><i class="fas fa-chevron-left text-xs"></i></button>' +
'<div class="flex-1 flex justify-between px-2">'+dayStrip+'</div>' +
'<button class="text-gray-400 hover:text-gray-600 px-1" onclick="navWeek(1)"><i class="fas fa-chevron-right text-xs"></i></button>' +
'</div>' +
// Time slots
'<div class="relative z-10 mb-3">'+slotsHtml+'</div>' +
// Currently selected
selectedBox +
// Navigation buttons
'<div class="flex justify-end gap-4 mt-auto">' +
'<button class="px-10 py-3 border border-gray-300 rounded-xl text-gray-700 font-medium bg-white hover:bg-gray-50 transition-colors" onclick="go(0)">Indietro</button>' +
'<button class="px-10 py-3 '+(selSlot?'bg-teal-600 text-white hover:bg-teal-700 cursor-pointer':'bg-gray-200 text-white cursor-default')+' rounded-xl font-medium transition-colors" onclick="'+(selSlot?'go(2)':'')+'">Avanti</button>' +
'</div>' +
'</div>';
}
function navWeek(dir) {
const d = new Date(weekStart);
d.setDate(d.getDate() + dir*7);
const t = new Date(); t.setHours(0,0,0,0);
if(d < t && dir < 0) return;
weekStart = d;
render();
}
async function pickDate(ds) {
selDate = new Date(ds+'T00:00:00');
selSlot = null;
slots = [];
render();
try {
const r = await fetch(API+'/api/services/'+sel.id+'/slots?date='+ds);
const d = await r.json();
slots = d.slots || [];
} catch(e) { slots = []; }
render();
}
function pickSlot(start, end, pid) {
selSlot = {start: start, end: end, provider_id: pid};
render();
}
// ── STEP 2: Form dati ──
function renderForm(m) {
m.innerHTML = '<div class="fade flex-1 flex flex-col">' +
'<h1 class="text-2xl font-bold text-gray-900 mb-1">I tuoi dati</h1>' +
'<p class="text-sm text-gray-500 mb-6">'+sel.name+' · '+fl(selDate)+' alle '+selSlot.start+'</p>' +
'<div class="space-y-4 mb-6">' +
'<div><label class="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">Nome e cognome *</label>' +
'<input id="fn" type="text" placeholder="Mario Rossi" autocomplete="name" class="w-full px-4 py-3 rounded-xl border border-gray-200 bg-white/80 text-[15px]"></div>' +
'<div><label class="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wide">Cellulare *</label>' +
'<input id="fp" type="tel" placeholder="333 1234567" autocomplete="tel" class="w-full px-4 py-3 rounded-xl border border-gray-200 bg-white/80 text-[15px]">' +
'<p class="text-xs text-gray-500 mt-1.5">📱 Riceverai conferma e promemoria su WhatsApp</p></div>' +
'<div><label class="block text-xs font-semibold text-gray-500 mb-1.5 uppercase tracking-wide">Email (opzionale)</label>' +
'<input id="fe" type="email" placeholder="mario@email.it" autocomplete="email" class="w-full px-4 py-3 rounded-xl border border-gray-200 bg-white/80 text-[15px]"></div>' +
'<div><label class="block text-xs font-semibold text-gray-500 mb-1.5 uppercase tracking-wide">Note (opzionale)</label>' +
'<input id="fno" type="text" placeholder="Richieste particolari..." class="w-full px-4 py-3 rounded-xl border border-gray-200 bg-white/80 text-[15px]"></div>' +
'</div>' +
'<div id="err"></div>' +
'<div class="flex justify-end gap-4 mt-auto">' +
'<button class="px-10 py-3 border border-gray-300 rounded-xl text-gray-700 font-medium bg-white hover:bg-gray-50 transition-colors" onclick="go(1)">Indietro</button>' +
'<button class="px-10 py-3 bg-teal-600 text-white rounded-xl font-medium hover:bg-teal-700 transition-colors" onclick="submitBooking()" id="bsub">Conferma</button>' +
'</div>' +
'</div>';
}
async function submitBooking() {
const n = document.getElementById('fn').value.trim();
const p = document.getElementById('fp').value.trim();
if(!n || !p) {
document.getElementById('err').innerHTML = '<div class="text-red-600 text-sm mb-4 p-3 bg-red-50 rounded-xl">Inserisci nome e cellulare</div>';
return;
}
lastPhone = p;
const btn = document.getElementById('bsub');
btn.textContent = 'Invio in corso...';
btn.disabled = true;
try {
const r = await fetch(API+'/api/bookings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
service_id: sel.id,
provider_id: selSlot.provider_id,
start_at: fd(selDate)+'T'+selSlot.start+':00+02:00',
customer_name: n,
customer_phone: p,
customer_email: document.getElementById('fe').value.trim() || null,
notes: document.getElementById('fno').value.trim() || null
})
});
if(!r.ok) { const e = await r.json(); throw new Error(e.detail || 'Errore'); }
go(3);
} catch(e) {
document.getElementById('err').innerHTML = '<div class="text-red-600 text-sm mb-4 p-3 bg-red-50 rounded-xl">'+e.message+'</div>';
btn.textContent = 'Conferma';
btn.disabled = false;
}
}
// ── STEP 3: Conferma ──
function renderConfirm(m) {
m.innerHTML = '<div class="fade text-center pt-16">' +
'<div class="w-16 h-16 rounded-full bg-teal-600 flex items-center justify-center mx-auto mb-6" style="box-shadow:0 8px 24px rgba(45,138,94,0.3)">'+CHKBIG+'</div>' +
'<h1 class="text-2xl font-bold text-gray-900 mb-3">Prenotazione confermata!</h1>' +
'<p class="text-sm text-gray-500 leading-relaxed">' +
'<strong class="text-gray-900">'+sel.name+'</strong><br>' +
fl(selDate)+' alle <strong class="text-gray-900">'+selSlot.start+'</strong><br><br>' +
'Riceverai conferma su WhatsApp<br>al numero <strong class="text-gray-900">'+lastPhone+'</strong>' +
'</p>' +
'<a href="https://wa.me/393930579002" target="_blank" class="inline-flex items-center gap-2 mt-6 px-6 py-3 rounded-xl text-white font-semibold text-sm" style="background:#25d366;text-decoration:none">💬 Contattaci su WhatsApp</a>' +
'<div class="mt-5"><button onclick="location.reload()" class="text-teal-600 text-sm font-semibold hover:underline cursor-pointer" style="background:none;border:none">Prenota un altro servizio</button></div>' +
'</div>';
}
init();
</script>
</body>
</html>

150
frontend/logo-white.svg Normal file
View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 440.56 107.85">
<defs>
<style>
.cls-1 {
fill: url(#linear-gradient);
}
.cls-2, .cls-3 {
fill: #ffffff;
}
.cls-4 {
fill: #6a971f;
}
.cls-5 {
fill: url(#radial-gradient-3);
}
.cls-6 {
fill: url(#radial-gradient-2);
}
.cls-7 {
fill: url(#radial-gradient);
}
.cls-3 {
fill-rule: evenodd;
}
</style>
<radialGradient id="radial-gradient" cx="273.62" cy="61.21" fx="273.62" fy="61.21" r="61.75" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f1f6ec"/>
<stop offset=".06" stop-color="#d5e2c2"/>
<stop offset=".12" stop-color="#bcd19c"/>
<stop offset=".18" stop-color="#a6c17a"/>
<stop offset=".25" stop-color="#93b45d"/>
<stop offset=".33" stop-color="#84a946"/>
<stop offset=".42" stop-color="#78a134"/>
<stop offset=".52" stop-color="#709b28"/>
<stop offset=".67" stop-color="#6b9721"/>
<stop offset="1" stop-color="#6a971f"/>
</radialGradient>
<linearGradient id="linear-gradient" x1="248.35" y1="21.74" x2="298.89" y2="21.74" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f4f9f1"/>
<stop offset=".05" stop-color="#e2efd1"/>
<stop offset=".11" stop-color="#cbe2a9"/>
<stop offset=".19" stop-color="#b6d786"/>
<stop offset=".26" stop-color="#a5ce68"/>
<stop offset=".35" stop-color="#97c650"/>
<stop offset=".44" stop-color="#8dc13d"/>
<stop offset=".56" stop-color="#85bd30"/>
<stop offset=".7" stop-color="#81ba29"/>
<stop offset="1" stop-color="#80ba27"/>
</linearGradient>
<radialGradient id="radial-gradient-2" cx="243.27" cy="-1778.99" fx="243.27" fy="-1778.99" r="31.75" gradientTransform="translate(59.23 297.58) scale(.88 .13)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#002c50"/>
<stop offset="0" stop-color="#012d50"/>
<stop offset=".09" stop-color="#365875"/>
<stop offset=".17" stop-color="#647f95"/>
<stop offset=".26" stop-color="#8ea1b1"/>
<stop offset=".36" stop-color="#b1bec9"/>
<stop offset=".46" stop-color="#cdd6dd"/>
<stop offset=".56" stop-color="#e3e8ec"/>
<stop offset=".68" stop-color="#f3f5f6"/>
<stop offset=".81" stop-color="#fcfcfd"/>
<stop offset="1" stop-color="#fff"/>
</radialGradient>
<radialGradient id="radial-gradient-3" cx="273.62" cy="30.14" fx="273.62" fy="30.14" r="15.94" gradientUnits="userSpaceOnUse">
<stop offset=".03" stop-color="#f4f9f1"/>
<stop offset=".36" stop-color="#f2f8ee"/>
<stop offset=".5" stop-color="#eef6e7"/>
<stop offset=".61" stop-color="#e7f2db"/>
<stop offset=".69" stop-color="#ddecc9"/>
<stop offset=".77" stop-color="#d0e5b2"/>
<stop offset=".84" stop-color="#bfdc96"/>
<stop offset=".9" stop-color="#acd274"/>
<stop offset=".95" stop-color="#96c64e"/>
<stop offset="1" stop-color="#80ba27"/>
</radialGradient>
</defs>
<g id="Livello_2-2" data-name="Livello 2">
<g>
<g>
<path class="cls-7" d="M304.23,30.6c0,16.9-13.7,30.6-30.6,30.6s-30.6-13.7-30.6-30.6S256.72,0,273.62,0s30.6,13.7,30.6,30.6Z"/>
<path class="cls-1" d="M298.89,21.74c0,11.53-11.31,20.87-25.27,20.87s-25.27-9.34-25.27-20.87S259.66,.87,273.62,.87s25.27,9.35,25.27,20.87Z"/>
<path class="cls-6" d="M301.52,67.55c0,2.27-12.52,4.11-27.97,4.11s-27.97-1.84-27.97-4.11,12.53-4.1,27.97-4.1,27.97,1.84,27.97,4.1Z"/>
<path class="cls-4" d="M286.32,24.45h-8.55V15.9c0-1.37-1.11-2.47-2.47-2.47h-4.89c-1.37,0-2.48,1.11-2.48,2.47v8.55h-8.55c-1.36,0-2.47,1.11-2.47,2.47v4.89c0,1.36,1.11,2.47,2.47,2.47h8.55v8.55c0,1.37,1.11,2.47,2.48,2.47h4.89c1.36,0,2.47-1.11,2.47-2.47v-8.55h8.55c1.37,0,2.47-1.11,2.47-2.47v-4.89c0-1.37-1.11-2.47-2.47-2.47Z"/>
<path class="cls-5" d="M287.09,25.22h-8.55v-8.55c0-1.37-1.11-2.47-2.47-2.47h-4.9c-1.36,0-2.47,1.11-2.47,2.47v8.55h-8.55c-1.36,0-2.47,1.11-2.47,2.47v4.89c0,1.37,1.11,2.47,2.47,2.47h8.55v8.55c0,1.37,1.11,2.47,2.47,2.47h4.9c1.36,0,2.47-1.11,2.47-2.47v-8.55h8.55c1.37,0,2.47-1.11,2.47-2.47v-4.89c0-1.37-1.11-2.47-2.47-2.47Z"/>
</g>
<g>
<path class="cls-3" d="M322.06,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
<path class="cls-3" d="M346.59,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.92,1.92-1.92,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.46-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06s4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.5-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M386.82,37.77c2.09,1.7,3.13,4.53,3.13,8.5v21.28h-8.47v-19.22c0-1.66-.22-2.94-.67-3.83-.81-1.62-2.36-2.44-4.64-2.44-2.8,0-4.73,1.19-5.77,3.57-.54,1.26-.81,2.86-.81,4.81v17.11h-8.23v-31.54h7.97v4.61c1.06-1.62,2.07-2.79,3.01-3.51,1.7-1.28,3.85-1.91,6.47-1.91,3.27,0,5.93,.86,8,2.58Z"/>
<path class="cls-3" d="M422.2,37.77c2.09,1.7,3.13,4.53,3.13,8.5v21.28h-8.47v-19.22c0-1.66-.22-2.94-.67-3.83-.81-1.62-2.36-2.44-4.64-2.44-2.8,0-4.73,1.19-5.77,3.57-.54,1.26-.81,2.86-.81,4.81v17.11h-8.23v-31.54h7.97v4.61c1.06-1.62,2.07-2.79,3.02-3.51,1.7-1.28,3.86-1.91,6.46-1.91,3.27,0,5.93,.86,8,2.58Z"/>
<path class="cls-3" d="M440.56,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
</g>
<g>
<path class="cls-3" d="M16.32,24.43c.43,.02,1.01,.06,1.74,.12v6.73c-.46-.06-1.25-.1-2.35-.12-1.08-.04-1.84,.2-2.26,.73-.41,.5-.61,1.06-.61,1.68v2.67h5.42v5.83h-5.42v25.49H4.61v-25.49H0v-5.83H4.52v-2.03c0-3.38,.57-5.71,1.71-6.99,1.2-1.89,4.09-2.84,8.67-2.84,.52,0,.99,.02,1.42,.06Z"/>
<path class="cls-3" d="M39.56,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.85,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.45-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M71.81,35.22c.12,0,.36,0,.72,.03v8.47c-.52-.06-.99-.1-1.39-.12-.41-.02-.73-.03-.99-.03-3.33,0-5.56,1.08-6.7,3.25-.64,1.22-.96,3.09-.96,5.62v15.11h-8.32v-31.6h7.89v5.51c1.28-2.11,2.39-3.55,3.33-4.32,1.55-1.29,3.56-1.94,6.03-1.94,.15,0,.28,0,.38,.03Z"/>
<path class="cls-3" d="M115.91,36.06c1.35,.54,2.58,1.49,3.68,2.84,.89,1.1,1.49,2.46,1.8,4.06,.19,1.06,.29,2.62,.29,4.67l-.06,19.92h-8.47v-20.12c0-1.2-.19-2.18-.58-2.96-.73-1.47-2.09-2.2-4.06-2.2-2.28,0-3.86,.95-4.73,2.84-.45,1.01-.67,2.21-.67,3.62v18.82h-8.32v-18.82c0-1.87-.19-3.24-.58-4.09-.7-1.53-2.06-2.29-4.09-2.29-2.36,0-3.94,.76-4.75,2.29-.45,.87-.67,2.17-.67,3.89v19.02h-8.38v-31.54h8.03v4.61c1.02-1.64,1.99-2.81,2.9-3.51,1.6-1.24,3.68-1.85,6.23-1.86,2.42,0,4.37,.53,5.86,1.59,1.2,.99,2.11,2.25,2.73,3.8,1.08-1.86,2.43-3.22,4.03-4.09,1.7-.87,3.59-1.3,5.68-1.3,1.39,0,2.76,.27,4.12,.81Z"/>
<path class="cls-3" d="M145.81,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.46-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M179.4,47.28c-.15-1.18-.55-2.24-1.19-3.19-.93-1.27-2.37-1.91-4.32-1.91-2.78,0-4.69,1.38-5.71,4.15-.54,1.47-.81,3.42-.81,5.86s.27,4.18,.81,5.6c.99,2.63,2.84,3.94,5.57,3.94,1.93,0,3.3-.52,4.12-1.56,.81-1.04,1.31-2.4,1.48-4.06h8.44c-.19,2.51-1.1,4.89-2.73,7.13-2.59,3.62-6.43,5.42-11.51,5.42-5.08,0-8.82-1.51-11.22-4.52-2.4-3.01-3.6-6.93-3.6-11.74,0-5.43,1.32-9.65,3.97-12.67,2.65-3.02,6.3-4.52,10.96-4.52,3.96,0,7.2,.89,9.71,2.67,2.53,1.78,4.03,4.92,4.5,9.42h-8.47Z"/>
<path class="cls-3" d="M201.15,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
<path class="cls-3" d="M225.69,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.18,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.45-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.34,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.46,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
</g>
<g>
<path class="cls-2" d="M28.16,87.82c0,.74-.51,1.32-1.37,1.32-.78,0-1.3-.59-1.3-1.32s.54-1.35,1.35-1.35,1.32,.59,1.32,1.35Zm-2.4,15.18v-11.85h2.16v11.85h-2.16Z"/>
<path class="cls-2" d="M31.24,85.61h2.16v17.39h-2.16v-17.39Z"/>
<path class="cls-2" d="M42.56,91.15l2.33,6.64c.39,1.1,.71,2.08,.96,3.06h.07c.27-.98,.61-1.96,1-3.06l2.3-6.64h2.25l-4.65,11.85h-2.06l-4.51-11.85h2.3Z"/>
<path class="cls-2" d="M63.74,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M65.93,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M78.27,88.31v2.84h3.09v1.64h-3.09v6.39c0,1.47,.42,2.3,1.62,2.3,.59,0,.93-.05,1.25-.15l.1,1.64c-.42,.15-1.08,.29-1.91,.29-1,0-1.81-.34-2.33-.91-.59-.66-.83-1.71-.83-3.11v-6.46h-1.84v-1.64h1.84v-2.2l2.11-.64Z"/>
<path class="cls-2" d="M83.54,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M101.78,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M109.2,85.61h2.13v7.45h.05c.76-1.32,2.13-2.16,4.04-2.16,2.96,0,5.02,2.45,5.02,6.03,0,4.24-2.69,6.34-5.34,6.34-1.71,0-3.09-.66-3.99-2.2h-.05l-.12,1.93h-1.84c.05-.81,.1-2.01,.1-3.06v-14.33Zm2.13,12.64c0,.27,.02,.54,.1,.78,.39,1.49,1.66,2.52,3.23,2.52,2.28,0,3.6-1.84,3.6-4.56,0-2.38-1.22-4.41-3.55-4.41-1.45,0-2.82,1.03-3.26,2.64-.07,.27-.12,.56-.12,.91v2.11Z"/>
<path class="cls-2" d="M124.1,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M134.92,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M149.47,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M160,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M169.46,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M180.4,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M191.23,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M199.95,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M214.82,96.58c-.02-1.42,1.15-2.57,2.57-2.57s2.57,1.13,2.57,2.57-1.15,2.57-2.6,2.57-2.55-1.13-2.55-2.57Z"/>
<path class="cls-2" d="M227.33,85.61h2.16v17.39h-2.16v-17.39Z"/>
<path class="cls-2" d="M241.2,100.16c0,1.03,.05,2.03,.17,2.84h-1.93l-.17-1.49h-.07c-.66,.93-1.93,1.76-3.62,1.76-2.4,0-3.62-1.69-3.62-3.4,0-2.87,2.55-4.43,7.13-4.41v-.25c0-.96-.27-2.74-2.69-2.72-1.13,0-2.28,.32-3.11,.88l-.49-1.45c.98-.61,2.42-1.03,3.92-1.03,3.62,0,4.51,2.47,4.51,4.83v4.43Zm-2.08-3.21c-2.35-.05-5.02,.37-5.02,2.67,0,1.42,.93,2.06,2.01,2.06,1.57,0,2.57-.98,2.91-1.98,.07-.25,.1-.49,.1-.69v-2.06Z"/>
<path class="cls-2" d="M249.38,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M273.45,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M275.63,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M287.98,88.31v2.84h3.09v1.64h-3.09v6.39c0,1.47,.42,2.3,1.62,2.3,.59,0,.93-.05,1.25-.15l.1,1.64c-.42,.15-1.08,.29-1.91,.29-1,0-1.81-.34-2.33-.91-.59-.66-.83-1.71-.83-3.11v-6.46h-1.84v-1.64h1.84v-2.2l2.11-.64Z"/>
<path class="cls-2" d="M293.24,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M309.21,100.16c0,1.03,.05,2.03,.17,2.84h-1.93l-.17-1.49h-.07c-.66,.93-1.93,1.76-3.62,1.76-2.4,0-3.62-1.69-3.62-3.4,0-2.87,2.55-4.43,7.13-4.41v-.25c0-.96-.27-2.74-2.69-2.72-1.13,0-2.28,.32-3.11,.88l-.49-1.45c.98-.61,2.42-1.03,3.92-1.03,3.62,0,4.51,2.47,4.51,4.83v4.43Zm-2.08-3.21c-2.35-.05-5.02,.37-5.02,2.67,0,1.42,.93,2.06,2.01,2.06,1.57,0,2.57-.98,2.91-1.98,.07-.25,.1-.49,.1-.69v-2.06Z"/>
<path class="cls-2" d="M317.39,95.02c0-1.52-.05-2.74-.1-3.87h1.91l.12,2.03h.05c.86-1.44,2.28-2.28,4.21-2.28,2.89,0,5.05,2.42,5.05,6,0,4.26-2.62,6.37-5.41,6.37-1.57,0-2.94-.69-3.65-1.86h-.05v6.44h-2.13v-12.83Zm2.13,3.16c0,.32,.02,.61,.1,.88,.39,1.49,1.69,2.52,3.23,2.52,2.28,0,3.6-1.86,3.6-4.58,0-2.35-1.25-4.38-3.53-4.38-1.47,0-2.87,1.03-3.26,2.64-.07,.27-.15,.59-.15,.86v2.06Z"/>
<path class="cls-2" d="M331.09,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M349.33,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M352.03,103v-10.21h-1.64v-1.64h1.64v-.56c0-1.66,.39-3.18,1.37-4.14,.81-.78,1.89-1.1,2.89-1.1,.78,0,1.42,.17,1.84,.34l-.29,1.66c-.32-.15-.73-.27-1.37-.27-1.84,0-2.3,1.59-2.3,3.43v.64h2.87v1.64h-2.87v10.21h-2.13Z"/>
<path class="cls-2" d="M359.59,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M370.13,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M379.58,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M391.73,87.82c0,.74-.51,1.32-1.37,1.32-.78,0-1.3-.59-1.3-1.32s.54-1.35,1.35-1.35,1.32,.59,1.32,1.35Zm-2.4,15.18v-11.85h2.16v11.85h-2.16Z"/>
<path class="cls-2" d="M405.54,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M408.02,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M422.56,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

BIN
frontend/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
frontend/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

150
frontend/logo.svg Normal file
View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 440.56 107.85">
<defs>
<style>
.cls-1 {
fill: url(#linear-gradient);
}
.cls-2, .cls-3 {
fill: #002c50;
}
.cls-4 {
fill: #6a971f;
}
.cls-5 {
fill: url(#radial-gradient-3);
}
.cls-6 {
fill: url(#radial-gradient-2);
}
.cls-7 {
fill: url(#radial-gradient);
}
.cls-3 {
fill-rule: evenodd;
}
</style>
<radialGradient id="radial-gradient" cx="273.62" cy="61.21" fx="273.62" fy="61.21" r="61.75" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f1f6ec"/>
<stop offset=".06" stop-color="#d5e2c2"/>
<stop offset=".12" stop-color="#bcd19c"/>
<stop offset=".18" stop-color="#a6c17a"/>
<stop offset=".25" stop-color="#93b45d"/>
<stop offset=".33" stop-color="#84a946"/>
<stop offset=".42" stop-color="#78a134"/>
<stop offset=".52" stop-color="#709b28"/>
<stop offset=".67" stop-color="#6b9721"/>
<stop offset="1" stop-color="#6a971f"/>
</radialGradient>
<linearGradient id="linear-gradient" x1="248.35" y1="21.74" x2="298.89" y2="21.74" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f4f9f1"/>
<stop offset=".05" stop-color="#e2efd1"/>
<stop offset=".11" stop-color="#cbe2a9"/>
<stop offset=".19" stop-color="#b6d786"/>
<stop offset=".26" stop-color="#a5ce68"/>
<stop offset=".35" stop-color="#97c650"/>
<stop offset=".44" stop-color="#8dc13d"/>
<stop offset=".56" stop-color="#85bd30"/>
<stop offset=".7" stop-color="#81ba29"/>
<stop offset="1" stop-color="#80ba27"/>
</linearGradient>
<radialGradient id="radial-gradient-2" cx="243.27" cy="-1778.99" fx="243.27" fy="-1778.99" r="31.75" gradientTransform="translate(59.23 297.58) scale(.88 .13)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#002c50"/>
<stop offset="0" stop-color="#012d50"/>
<stop offset=".09" stop-color="#365875"/>
<stop offset=".17" stop-color="#647f95"/>
<stop offset=".26" stop-color="#8ea1b1"/>
<stop offset=".36" stop-color="#b1bec9"/>
<stop offset=".46" stop-color="#cdd6dd"/>
<stop offset=".56" stop-color="#e3e8ec"/>
<stop offset=".68" stop-color="#f3f5f6"/>
<stop offset=".81" stop-color="#fcfcfd"/>
<stop offset="1" stop-color="#fff"/>
</radialGradient>
<radialGradient id="radial-gradient-3" cx="273.62" cy="30.14" fx="273.62" fy="30.14" r="15.94" gradientUnits="userSpaceOnUse">
<stop offset=".03" stop-color="#f4f9f1"/>
<stop offset=".36" stop-color="#f2f8ee"/>
<stop offset=".5" stop-color="#eef6e7"/>
<stop offset=".61" stop-color="#e7f2db"/>
<stop offset=".69" stop-color="#ddecc9"/>
<stop offset=".77" stop-color="#d0e5b2"/>
<stop offset=".84" stop-color="#bfdc96"/>
<stop offset=".9" stop-color="#acd274"/>
<stop offset=".95" stop-color="#96c64e"/>
<stop offset="1" stop-color="#80ba27"/>
</radialGradient>
</defs>
<g id="Livello_2-2" data-name="Livello 2">
<g>
<g>
<path class="cls-7" d="M304.23,30.6c0,16.9-13.7,30.6-30.6,30.6s-30.6-13.7-30.6-30.6S256.72,0,273.62,0s30.6,13.7,30.6,30.6Z"/>
<path class="cls-1" d="M298.89,21.74c0,11.53-11.31,20.87-25.27,20.87s-25.27-9.34-25.27-20.87S259.66,.87,273.62,.87s25.27,9.35,25.27,20.87Z"/>
<path class="cls-6" d="M301.52,67.55c0,2.27-12.52,4.11-27.97,4.11s-27.97-1.84-27.97-4.11,12.53-4.1,27.97-4.1,27.97,1.84,27.97,4.1Z"/>
<path class="cls-4" d="M286.32,24.45h-8.55V15.9c0-1.37-1.11-2.47-2.47-2.47h-4.89c-1.37,0-2.48,1.11-2.48,2.47v8.55h-8.55c-1.36,0-2.47,1.11-2.47,2.47v4.89c0,1.36,1.11,2.47,2.47,2.47h8.55v8.55c0,1.37,1.11,2.47,2.48,2.47h4.89c1.36,0,2.47-1.11,2.47-2.47v-8.55h8.55c1.37,0,2.47-1.11,2.47-2.47v-4.89c0-1.37-1.11-2.47-2.47-2.47Z"/>
<path class="cls-5" d="M287.09,25.22h-8.55v-8.55c0-1.37-1.11-2.47-2.47-2.47h-4.9c-1.36,0-2.47,1.11-2.47,2.47v8.55h-8.55c-1.36,0-2.47,1.11-2.47,2.47v4.89c0,1.37,1.11,2.47,2.47,2.47h8.55v8.55c0,1.37,1.11,2.47,2.47,2.47h4.9c1.36,0,2.47-1.11,2.47-2.47v-8.55h8.55c1.37,0,2.47-1.11,2.47-2.47v-4.89c0-1.37-1.11-2.47-2.47-2.47Z"/>
</g>
<g>
<path class="cls-3" d="M322.06,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
<path class="cls-3" d="M346.59,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.92,1.92-1.92,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.46-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06s4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.5-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M386.82,37.77c2.09,1.7,3.13,4.53,3.13,8.5v21.28h-8.47v-19.22c0-1.66-.22-2.94-.67-3.83-.81-1.62-2.36-2.44-4.64-2.44-2.8,0-4.73,1.19-5.77,3.57-.54,1.26-.81,2.86-.81,4.81v17.11h-8.23v-31.54h7.97v4.61c1.06-1.62,2.07-2.79,3.01-3.51,1.7-1.28,3.85-1.91,6.47-1.91,3.27,0,5.93,.86,8,2.58Z"/>
<path class="cls-3" d="M422.2,37.77c2.09,1.7,3.13,4.53,3.13,8.5v21.28h-8.47v-19.22c0-1.66-.22-2.94-.67-3.83-.81-1.62-2.36-2.44-4.64-2.44-2.8,0-4.73,1.19-5.77,3.57-.54,1.26-.81,2.86-.81,4.81v17.11h-8.23v-31.54h7.97v4.61c1.06-1.62,2.07-2.79,3.02-3.51,1.7-1.28,3.86-1.91,6.46-1.91,3.27,0,5.93,.86,8,2.58Z"/>
<path class="cls-3" d="M440.56,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
</g>
<g>
<path class="cls-3" d="M16.32,24.43c.43,.02,1.01,.06,1.74,.12v6.73c-.46-.06-1.25-.1-2.35-.12-1.08-.04-1.84,.2-2.26,.73-.41,.5-.61,1.06-.61,1.68v2.67h5.42v5.83h-5.42v25.49H4.61v-25.49H0v-5.83H4.52v-2.03c0-3.38,.57-5.71,1.71-6.99,1.2-1.89,4.09-2.84,8.67-2.84,.52,0,.99,.02,1.42,.06Z"/>
<path class="cls-3" d="M39.56,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.85,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.45-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M71.81,35.22c.12,0,.36,0,.72,.03v8.47c-.52-.06-.99-.1-1.39-.12-.41-.02-.73-.03-.99-.03-3.33,0-5.56,1.08-6.7,3.25-.64,1.22-.96,3.09-.96,5.62v15.11h-8.32v-31.6h7.89v5.51c1.28-2.11,2.39-3.55,3.33-4.32,1.55-1.29,3.56-1.94,6.03-1.94,.15,0,.28,0,.38,.03Z"/>
<path class="cls-3" d="M115.91,36.06c1.35,.54,2.58,1.49,3.68,2.84,.89,1.1,1.49,2.46,1.8,4.06,.19,1.06,.29,2.62,.29,4.67l-.06,19.92h-8.47v-20.12c0-1.2-.19-2.18-.58-2.96-.73-1.47-2.09-2.2-4.06-2.2-2.28,0-3.86,.95-4.73,2.84-.45,1.01-.67,2.21-.67,3.62v18.82h-8.32v-18.82c0-1.87-.19-3.24-.58-4.09-.7-1.53-2.06-2.29-4.09-2.29-2.36,0-3.94,.76-4.75,2.29-.45,.87-.67,2.17-.67,3.89v19.02h-8.38v-31.54h8.03v4.61c1.02-1.64,1.99-2.81,2.9-3.51,1.6-1.24,3.68-1.85,6.23-1.86,2.42,0,4.37,.53,5.86,1.59,1.2,.99,2.11,2.25,2.73,3.8,1.08-1.86,2.43-3.22,4.03-4.09,1.7-.87,3.59-1.3,5.68-1.3,1.39,0,2.76,.27,4.12,.81Z"/>
<path class="cls-3" d="M145.81,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.17,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.46-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.33,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.45,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
<path class="cls-3" d="M179.4,47.28c-.15-1.18-.55-2.24-1.19-3.19-.93-1.27-2.37-1.91-4.32-1.91-2.78,0-4.69,1.38-5.71,4.15-.54,1.47-.81,3.42-.81,5.86s.27,4.18,.81,5.6c.99,2.63,2.84,3.94,5.57,3.94,1.93,0,3.3-.52,4.12-1.56,.81-1.04,1.31-2.4,1.48-4.06h8.44c-.19,2.51-1.1,4.89-2.73,7.13-2.59,3.62-6.43,5.42-11.51,5.42-5.08,0-8.82-1.51-11.22-4.52-2.4-3.01-3.6-6.93-3.6-11.74,0-5.43,1.32-9.65,3.97-12.67,2.65-3.02,6.3-4.52,10.96-4.52,3.96,0,7.2,.89,9.71,2.67,2.53,1.78,4.03,4.92,4.5,9.42h-8.47Z"/>
<path class="cls-3" d="M201.15,32.18h-8.38v-7.63h8.38v7.63Zm-8.38,3.77h8.38v31.6h-8.38v-31.6Z"/>
<path class="cls-3" d="M225.69,52.18c-.52,.33-1.05,.6-1.59,.81-.52,.19-1.25,.38-2.18,.55l-1.86,.35c-1.74,.31-2.99,.69-3.74,1.13-1.28,.75-1.91,1.92-1.91,3.51,0,1.41,.39,2.44,1.16,3.07,.79,.62,1.75,.93,2.87,.93,1.78,0,3.41-.52,4.9-1.57,1.51-1.04,2.29-2.95,2.35-5.71v-3.07Zm-5.02-3.86c1.53-.19,2.62-.43,3.28-.73,1.18-.5,1.77-1.28,1.77-2.35,0-1.29-.45-2.18-1.36-2.67-.89-.5-2.2-.75-3.94-.75-1.95,0-3.34,.48-4.15,1.45-.58,.72-.97,1.68-1.16,2.9h-7.97c.17-2.76,.95-5.03,2.32-6.81,2.18-2.78,5.93-4.17,11.25-4.17,3.46,0,6.53,.69,9.22,2.06,2.69,1.37,4.03,3.96,4.03,7.77v14.5c0,1.01,.02,2.22,.06,3.65,.06,1.08,.22,1.82,.49,2.2,.27,.39,.68,.71,1.22,.96v1.22h-8.99c-.25-.64-.43-1.24-.52-1.8-.1-.56-.17-1.2-.23-1.91-1.14,1.24-2.46,2.29-3.94,3.16-1.78,1.02-3.79,1.54-6.03,1.54-2.86,0-5.23-.81-7.1-2.44-1.86-1.64-2.78-3.96-2.78-6.96,0-3.89,1.5-6.7,4.49-8.44,1.64-.95,4.06-1.62,7.25-2.03l2.81-.35Z"/>
</g>
<g>
<path class="cls-2" d="M28.16,87.82c0,.74-.51,1.32-1.37,1.32-.78,0-1.3-.59-1.3-1.32s.54-1.35,1.35-1.35,1.32,.59,1.32,1.35Zm-2.4,15.18v-11.85h2.16v11.85h-2.16Z"/>
<path class="cls-2" d="M31.24,85.61h2.16v17.39h-2.16v-17.39Z"/>
<path class="cls-2" d="M42.56,91.15l2.33,6.64c.39,1.1,.71,2.08,.96,3.06h.07c.27-.98,.61-1.96,1-3.06l2.3-6.64h2.25l-4.65,11.85h-2.06l-4.51-11.85h2.3Z"/>
<path class="cls-2" d="M63.74,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M65.93,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M78.27,88.31v2.84h3.09v1.64h-3.09v6.39c0,1.47,.42,2.3,1.62,2.3,.59,0,.93-.05,1.25-.15l.1,1.64c-.42,.15-1.08,.29-1.91,.29-1,0-1.81-.34-2.33-.91-.59-.66-.83-1.71-.83-3.11v-6.46h-1.84v-1.64h1.84v-2.2l2.11-.64Z"/>
<path class="cls-2" d="M83.54,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M101.78,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M109.2,85.61h2.13v7.45h.05c.76-1.32,2.13-2.16,4.04-2.16,2.96,0,5.02,2.45,5.02,6.03,0,4.24-2.69,6.34-5.34,6.34-1.71,0-3.09-.66-3.99-2.2h-.05l-.12,1.93h-1.84c.05-.81,.1-2.01,.1-3.06v-14.33Zm2.13,12.64c0,.27,.02,.54,.1,.78,.39,1.49,1.66,2.52,3.23,2.52,2.28,0,3.6-1.84,3.6-4.56,0-2.38-1.22-4.41-3.55-4.41-1.45,0-2.82,1.03-3.26,2.64-.07,.27-.12,.56-.12,.91v2.11Z"/>
<path class="cls-2" d="M124.1,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M134.92,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M149.47,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M160,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M169.46,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.15-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M180.4,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M191.23,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M199.95,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M214.82,96.58c-.02-1.42,1.15-2.57,2.57-2.57s2.57,1.13,2.57,2.57-1.15,2.57-2.6,2.57-2.55-1.13-2.55-2.57Z"/>
<path class="cls-2" d="M227.33,85.61h2.16v17.39h-2.16v-17.39Z"/>
<path class="cls-2" d="M241.2,100.16c0,1.03,.05,2.03,.17,2.84h-1.93l-.17-1.49h-.07c-.66,.93-1.93,1.76-3.62,1.76-2.4,0-3.62-1.69-3.62-3.4,0-2.87,2.55-4.43,7.13-4.41v-.25c0-.96-.27-2.74-2.69-2.72-1.13,0-2.28,.32-3.11,.88l-.49-1.45c.98-.61,2.42-1.03,3.92-1.03,3.62,0,4.51,2.47,4.51,4.83v4.43Zm-2.08-3.21c-2.35-.05-5.02,.37-5.02,2.67,0,1.42,.93,2.06,2.01,2.06,1.57,0,2.57-.98,2.91-1.98,.07-.25,.1-.49,.1-.69v-2.06Z"/>
<path class="cls-2" d="M249.38,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M273.45,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M275.63,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M287.98,88.31v2.84h3.09v1.64h-3.09v6.39c0,1.47,.42,2.3,1.62,2.3,.59,0,.93-.05,1.25-.15l.1,1.64c-.42,.15-1.08,.29-1.91,.29-1,0-1.81-.34-2.33-.91-.59-.66-.83-1.71-.83-3.11v-6.46h-1.84v-1.64h1.84v-2.2l2.11-.64Z"/>
<path class="cls-2" d="M293.24,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M309.21,100.16c0,1.03,.05,2.03,.17,2.84h-1.93l-.17-1.49h-.07c-.66,.93-1.93,1.76-3.62,1.76-2.4,0-3.62-1.69-3.62-3.4,0-2.87,2.55-4.43,7.13-4.41v-.25c0-.96-.27-2.74-2.69-2.72-1.13,0-2.28,.32-3.11,.88l-.49-1.45c.98-.61,2.42-1.03,3.92-1.03,3.62,0,4.51,2.47,4.51,4.83v4.43Zm-2.08-3.21c-2.35-.05-5.02,.37-5.02,2.67,0,1.42,.93,2.06,2.01,2.06,1.57,0,2.57-.98,2.91-1.98,.07-.25,.1-.49,.1-.69v-2.06Z"/>
<path class="cls-2" d="M317.39,95.02c0-1.52-.05-2.74-.1-3.87h1.91l.12,2.03h.05c.86-1.44,2.28-2.28,4.21-2.28,2.89,0,5.05,2.42,5.05,6,0,4.26-2.62,6.37-5.41,6.37-1.57,0-2.94-.69-3.65-1.86h-.05v6.44h-2.13v-12.83Zm2.13,3.16c0,.32,.02,.61,.1,.88,.39,1.49,1.69,2.52,3.23,2.52,2.28,0,3.6-1.86,3.6-4.58,0-2.35-1.25-4.38-3.53-4.38-1.47,0-2.87,1.03-3.26,2.64-.07,.27-.15,.59-.15,.86v2.06Z"/>
<path class="cls-2" d="M331.09,94.84c0-1.4-.02-2.6-.1-3.7h1.89l.1,2.35h.07c.54-1.59,1.86-2.6,3.31-2.6,.22,0,.39,.03,.59,.05v2.03c-.22-.05-.44-.05-.73-.05-1.52,0-2.6,1.13-2.89,2.74-.05,.29-.07,.66-.07,1v6.32h-2.16v-8.16Z"/>
<path class="cls-2" d="M349.33,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M352.03,103v-10.21h-1.64v-1.64h1.64v-.56c0-1.66,.39-3.18,1.37-4.14,.81-.78,1.89-1.1,2.89-1.1,.78,0,1.42,.17,1.84,.34l-.29,1.66c-.32-.15-.73-.27-1.37-.27-1.84,0-2.3,1.59-2.3,3.43v.64h2.87v1.64h-2.87v10.21h-2.13Z"/>
<path class="cls-2" d="M359.59,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
<path class="cls-2" d="M370.13,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M379.58,100.82c.66,.39,1.79,.83,2.87,.83,1.54,0,2.28-.76,2.28-1.76s-.61-1.57-2.18-2.16c-2.16-.78-3.16-1.93-3.16-3.35,0-1.91,1.57-3.48,4.09-3.48,1.2,0,2.25,.32,2.89,.73l-.51,1.57c-.47-.29-1.32-.71-2.42-.71-1.27,0-1.96,.74-1.96,1.62,0,.98,.69,1.42,2.23,2.03,2.03,.76,3.11,1.79,3.11,3.55,0,2.11-1.64,3.58-4.41,3.58-1.3,0-2.5-.34-3.33-.83l.51-1.62Z"/>
<path class="cls-2" d="M391.73,87.82c0,.74-.51,1.32-1.37,1.32-.78,0-1.3-.59-1.3-1.32s.54-1.35,1.35-1.35,1.32,.59,1.32,1.35Zm-2.4,15.18v-11.85h2.16v11.85h-2.16Z"/>
<path class="cls-2" d="M405.54,96.98c0,4.38-3.06,6.29-5.9,6.29-3.18,0-5.68-2.35-5.68-6.1,0-3.94,2.62-6.27,5.88-6.27s5.71,2.47,5.71,6.07Zm-9.38,.12c0,2.6,1.47,4.56,3.58,4.56s3.6-1.94,3.6-4.6c0-2.01-1-4.53-3.55-4.53s-3.62,2.35-3.62,4.58Z"/>
<path class="cls-2" d="M408.02,94.35c0-1.25-.02-2.23-.1-3.21h1.91l.12,1.96h.05c.59-1.1,1.96-2.2,3.92-2.2,1.64,0,4.19,.98,4.19,5.04v7.05h-2.16v-6.83c0-1.91-.71-3.5-2.74-3.5-1.4,0-2.5,1-2.89,2.2-.1,.27-.15,.64-.15,1v7.13h-2.16v-8.65Z"/>
<path class="cls-2" d="M422.56,97.47c.05,2.91,1.89,4.11,4.07,4.11,1.54,0,2.5-.27,3.28-.61l.39,1.54c-.76,.34-2.08,.76-3.97,.76-3.65,0-5.83-2.42-5.83-6s2.11-6.37,5.56-6.37c3.89,0,4.9,3.38,4.9,5.56,0,.44-.02,.76-.07,1h-8.33Zm6.32-1.54c.02-1.35-.56-3.48-2.99-3.48-2.2,0-3.13,1.98-3.31,3.48h6.29Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB