Iscrizioni fix, new graph and sharepoint page

This commit is contained in:
Luca Banfi
2026-05-14 12:34:57 +02:00
parent 34993725ce
commit 4ef2041b71
17 changed files with 342 additions and 82 deletions
+23
View File
@@ -1,3 +1,4 @@
import json as _json
from .config import _state, _access_log, DbType
from .renderer import _base, _card, _page_css, _page_js, _tmpl
from .queries.db import e as _e, query as _query
@@ -9,6 +10,7 @@ from .queries import documenti as q_documenti
from .queries import iscrizioni as q_iscrizioni
from .queries import api_iscrizioni as q_api_iscrizioni
from .queries import email as q_email
from .queries import sharepoint as q_sharepoint
def _load_data(db_path: str) -> dict:
@@ -187,6 +189,14 @@ def _page_api_iscrizioni(db_path: str) -> str:
return _base('Iscrizioni', content, 'nav_api_iscrizioni', db_path, _page_css('api_iscrizioni'), _page_js('api_iscrizioni'))
def _page_sharepoint(db_path: str) -> str:
rows = q_sharepoint.get_sharepoint(db_path)
content = _tmpl('sharepoint.html').substitute(
tbl_sharepoint=q_sharepoint.render_table(rows)
)
return _base('SharePoint', content, 'nav_sharepoint', db_path, _page_css('sharepoint'), _page_js('sharepoint'))
def _page_email(db_path: str) -> str:
rows = q_email.get_email(db_path)
content = _tmpl('email.html').substitute(
@@ -195,6 +205,19 @@ def _page_email(db_path: str) -> str:
return _base('Email', content, 'nav_email', db_path, _page_css('email'), _page_js('email'))
def _page_schema(db_path: str) -> str:
rows = q_processes.get_schema_stats(db_path)
labels = [r.get('process_name') or 'unknown' for r in rows]
counts = [r.get('run_count', 0) for r in rows]
minutes = [max(1, (r.get('total_seconds') or 0) // 60) for r in rows]
content = _tmpl('schema.html').substitute(
labels_json=_json.dumps(labels),
counts_json=_json.dumps(counts),
minutes_json=_json.dumps(minutes),
)
return _base('Schema', content, 'nav_schema', db_path, _page_css('schema'), _page_js('schema'))
def _page_server_logs(db_path: str) -> str:
rows = ''.join(
f'<tr>'
+35 -26
View File
@@ -29,6 +29,7 @@ def get_corsi(db_path: str) -> list:
json_extract(f.corso, '$.descrizione') AS corso_descrizione,
json_extract(f.corso, '$.quando_start') AS corso_data,
json_extract(f.corso, '$.n_iscritti') AS n_iscritti,
bc.odv_number,
s.step_ftp, s.err_ftp,
s.step_bc, s.err_bc,
s.step_iscrizione, s.err_iscrizione,
@@ -38,8 +39,9 @@ def get_corsi(db_path: str) -> list:
s.step_email, s.err_email,
f.created_at
FROM rpa_intra_ftp_json f
LEFT JOIN rpa_mapping_aziende m ON m.company_code = f.company_code
LEFT JOIN rpa_mapping_aziende m ON m.company_code = f.company_code
LEFT JOIN rpa_intra_steps_status s ON s.rpa_intra_ftp_json_id = f.id
LEFT JOIN rpa_intra_bc_check bc ON bc.rpa_intra_ftp_json_id = f.id
ORDER BY f.id DESC
""")
@@ -82,15 +84,16 @@ def _row_status(r: dict) -> str:
return 'done'
if any(r.get(s) == 'processing' for s in _ALL_STEPS):
return 'processing'
if any(r.get(s) == 'skipped' for s in _ALL_STEPS):
return 'skipped'
return 'pending'
if any(r.get(s) == 'pending' for s in _ALL_STEPS):
return 'pending'
return 'skipped'
def _fth(label: str, col: str) -> str:
return f'<th>{label}<button class="col-filter-btn" data-col="{col}" title="Filtra {label}">&#9881;</button></th>'
def _fth(label: str, col: str, step: bool = False, data: bool = False) -> str:
cls = ' class="step-th"' if step else (' class="data-th"' if data else '')
return f'<th{cls}>{label}<button class="col-filter-btn" data-col="{col}" title="Filtra {label}">&#9881;</button></th>'
def _clip(text: str, n: int = 20) -> str:
@@ -116,6 +119,7 @@ def render_table(rows: list) -> str:
f' data-azienda="{e(r.get("azienda_nome") or "")}"'
f' data-corso="{e(r.get("corso_descrizione") or "")}"'
f' data-data="{e(r.get("corso_data") or "")}"'
f' data-odv_number="{e(r.get("odv_number") or "")}"'
f' data-ftp="{e(r.get("step_ftp") or "pending")}"'
f' data-bc="{e(r.get("step_bc") or "pending")}"'
f' data-iscrizione="{e(r.get("step_iscrizione") or "pending")}"'
@@ -124,39 +128,44 @@ def render_table(rows: list) -> str:
f' data-report="{e(r.get("step_report") or "pending")}"'
f' data-email="{e(r.get("step_email") or "pending")}"'
f'>'
f'<td>{e(r.get("id"))}</td>'
f'<td>{e(r.get("sorgente"))}</td>'
f'<td>{e(r.get("azienda_nome"))}</td>'
f'<td>{e(r.get("sessione_id"))}</td>'
f'<td class="nc">{_clip(r.get("corso_descrizione") or "")}</td>'
f'<td>{e(r.get("corso_data"))}</td>'
f'<td>{e(r.get("n_iscritti"))}</td>'
f'<td class="td-id">{e(r.get("id"))}'
+ (f'<a href="/logs?run={e(r.get("rpa_process_id"))}" class="btn-log" title="Vedi log">&#9776;</a>'
if r.get('rpa_process_id') else '')
+ f'</td>'
f'<td class="step-td">{_step_cell(r.get("step_ftp"), r.get("err_ftp"))}</td>'
f'<td class="step-td">{_step_cell(r.get("step_bc"), r.get("err_bc"))}</td>'
f'<td class="step-td">{_step_cell(r.get("step_iscrizione"), r.get("err_iscrizione"))}</td>'
f'<td class="step-td">{e(r.get("odv_number") or "-")}</td>'
f'<td class="step-td">{_step_cell(r.get("step_odv"), r.get("err_odv"))}</td>'
f'<td class="step-td">{_step_cell(r.get("step_sharepoint"), r.get("err_sharepoint"))}</td>'
f'<td class="step-td">{_step_cell(r.get("step_report"), r.get("err_report"))}</td>'
f'<td class="step-td">{_step_cell(r.get("step_email"), r.get("err_email"))}</td>'
f'<td class="data-td">{e(r.get("sorgente"))}</td>'
f'<td class="data-td">{e(r.get("azienda_nome"))}</td>'
f'<td class="data-td">{e(r.get("sessione_id"))}</td>'
f'<td class="data-td nc">{_clip(r.get("corso_descrizione") or "")}</td>'
f'<td class="data-td">{e(r.get("corso_data"))}</td>'
f'<td class="data-td">{e(r.get("n_iscritti"))}</td>'
f'</tr>'
for r in rows
)
return (
'<table><thead><tr>'
f'<th>#</th>'
f'{_fth("Sorgente", "sorgente")}'
f'{_fth("Azienda", "azienda")}'
f'{_fth("Sessione", "sessione")}'
f'{_fth("Corso", "corso")}'
f'{_fth("Data", "data")}'
f'<th>N.</th>'
f'{_fth("FTP", "ftp")}'
f'{_fth("BC", "bc")}'
f'{_fth("Iscrizione", "iscrizione")}'
f'{_fth("ODV", "odv")}'
f'{_fth("SharePoint", "sharepoint")}'
f'{_fth("Report", "report")}'
f'{_fth("Email", "email")}'
f'{_fth("FTP", "ftp", step=True)}'
f'{_fth("BC", "bc", step=True)}'
f'{_fth("Iscrizione", "iscrizione", step=True)}'
f'{_fth("ODV Number", "odv_number", step=True)}'
f'{_fth("ODV Step", "odv", step=True)}'
f'{_fth("SharePoint", "sharepoint", step=True)}'
f'{_fth("Report", "report", step=True)}'
f'{_fth("Email", "email", step=True)}'
f'{_fth("Sorgente", "sorgente", data=True)}'
f'{_fth("Azienda", "azienda", data=True)}'
f'{_fth("Sessione", "sessione", data=True)}'
f'{_fth("Corso", "corso", data=True)}'
f'{_fth("Data", "data", data=True)}'
f'<th class="data-th">N. iscritti</th>'
f'</tr></thead><tbody>{trs}</tbody></table>'
)
+16 -2
View File
@@ -4,7 +4,7 @@ from .db import query, count, e, dt, dur, badge
def get_processes(db_path: str) -> list:
return query(db_path, "SELECT id, process_name, note, completed, start_run, finish_run FROM rpa_process ORDER BY start_run DESC LIMIT 50")
return query(db_path, "SELECT id, process_name, note, completed, start_run, finish_run FROM rpa_process ORDER BY id DESC LIMIT 50")
def get_stats(db_path: str) -> dict:
@@ -17,6 +17,19 @@ def get_stats(db_path: str) -> dict:
}
def get_schema_stats(db_path: str) -> list:
return query(db_path, """
SELECT process_name,
COUNT(*) as run_count,
SUM(CASE WHEN start_run IS NOT NULL AND finish_run IS NOT NULL
THEN CAST((julianday(finish_run) - julianday(start_run)) * 86400 AS INTEGER)
ELSE 0 END) as total_seconds
FROM rpa_process
GROUP BY process_name
ORDER BY run_count DESC
""")
def render_table(rows: list) -> str:
if not rows:
return '<p class="empty">Nessun processo trovato.</p>'
@@ -33,11 +46,12 @@ def render_table(rows: list) -> str:
f'<td>{dur(r.get("start_run"), r.get("finish_run"))}</td>'
f'<td>{_badge(r.get("completed"))}</td>'
f'<td class="nc">{e(r.get("note"))}</td>'
f'<td><a href="/logs?run={e(r.get("id"))}" class="btn-log">→ Log</a></td>'
f'</tr>'
for r in rows
)
return (
'<table><thead><tr>'
'<th>Processo</th><th>Inizio</th><th>Fine</th><th>Durata</th><th>Stato</th><th>Note</th>'
'<th>Processo</th><th>Inizio</th><th>Fine</th><th>Durata</th><th>Stato</th><th>Note</th><th></th>'
f'</tr></thead><tbody>{trs}</tbody></table>'
)
+61
View File
@@ -0,0 +1,61 @@
"""queries/sharepoint.py — rpa_intra_sharepoint (Intraz)"""
from .db import query, count, e, dt, badge
def get_sharepoint(db_path: str) -> list:
return query(db_path, """
SELECT
sp.id, sp.rpa_intra_ftp_json_id, sp.rpa_process_id,
sp.file_type, sp.remote_path, sp.stato, sp.note,
sp.created_at, sp.updated_at,
json_extract(f.corso, '$.IDsessione') AS sessione_id,
json_extract(f.corso, '$.descrizione') AS corso_descrizione,
json_extract(f.azienda, '$.ragione_sociale') AS azienda_nome
FROM rpa_intra_sharepoint sp
LEFT JOIN rpa_intra_ftp_json f ON f.id = sp.rpa_intra_ftp_json_id
ORDER BY sp.id DESC
LIMIT 500
""")
def get_stats(db_path: str) -> dict:
return {
'sp_total': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint"),
'sp_uploaded': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='uploaded'"),
'sp_errore': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='errore'"),
'sp_pending': count(db_path, "SELECT COUNT(*) as n FROM rpa_intra_sharepoint WHERE stato='pending'"),
}
def render_table(rows: list) -> str:
if not rows:
return '<p class="empty">Nessun file SharePoint trovato.</p>'
def _badge(stato):
if stato == 'uploaded': return badge('ok', 'Uploaded')
if stato == 'errore': return badge('err', 'Errore')
return badge('warn', 'Pending')
trs = ''.join(
f'<tr data-stato="{e(r.get("stato") or "pending")}">'
f'<td>{e(r.get("id"))}</td>'
f'<td>{e(r.get("rpa_intra_ftp_json_id"))}</td>'
f'<td>{e(r.get("sessione_id"))}</td>'
f'<td>{e(r.get("azienda_nome"))}</td>'
f'<td>{e(r.get("file_type"))}</td>'
f'<td class="nc">{e(r.get("remote_path"))}</td>'
f'<td>{_badge(r.get("stato"))}</td>'
f'<td class="nc">{e(r.get("note"))}</td>'
f'<td>{dt(r.get("created_at"))}</td>'
f'<td>{dt(r.get("updated_at"))}</td>'
f'</tr>'
for r in rows
)
return (
'<table><thead><tr>'
'<th>#</th><th>JSON ID</th><th>Sessione</th><th>Azienda</th>'
'<th>Tipo</th><th>Remote Path</th><th>Stato</th><th>Note</th>'
'<th>Creato</th><th>Aggiornato</th>'
f'</tr></thead><tbody>{trs}</tbody></table>'
)
+6 -2
View File
@@ -46,7 +46,7 @@ def _card(label, value, color='#2563eb') -> str:
def _base(title: str, content: str, active: str, db_path: str, page_css: str = '', page_js: str = '') -> str:
now = datetime.now().strftime('%d/%m/%Y %H:%M:%S')
nav = {'nav_dashboard': '', 'nav_runs': '', 'nav_logs': '', 'nav_report': ''}
nav = {'nav_dashboard': '', 'nav_runs': '', 'nav_logs': '', 'nav_report': '', 'nav_schema': ''}
if active in nav:
nav[active] = 'active'
@@ -58,6 +58,8 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
nav_step_link = f'<a href="/steps" class="{cls}">RPA Steps</a>'
cls_api_isc = 'active' if active == 'nav_api_iscrizioni' else ''
nav_report_link = f'<a href="/iscrizioni-api" class="{cls_api_isc}">Iscrizioni</a>'
cls_sp = 'active' if active == 'nav_sharepoint' else ''
nav_sharepoint_link = f'<a href="/sharepoint" class="{cls_sp}">SharePoint</a>'
cls_email = 'active' if active == 'nav_email' else ''
nav_pec_link = f'<a href="/email" class="{cls_email}">Email</a>'
else:
@@ -67,6 +69,7 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
nav_pec_link = f'<a href="/pec" class="{cls_pec}">PEC</a>'
cls_rep = nav.get('nav_report', '')
nav_report_link = f'<a href="/report" class="{cls_rep}">Report</a>'
nav_sharepoint_link = ''
return _tmpl('_base.html').substitute(
css=_css(), page_css=page_css, page_js=page_js, title=title, now=now,
@@ -74,7 +77,8 @@ def _base(title: str, content: str, active: str, db_path: str, page_css: str = '
h1_title=h1_title,
content=content,
nav_step_link=nav_step_link,
nav_pec_link=nav_pec_link,
nav_report_link=nav_report_link,
nav_sharepoint_link=nav_sharepoint_link,
nav_pec_link=nav_pec_link,
**nav,
)
+6 -1
View File
@@ -8,7 +8,8 @@ from .config import _state, _access_log, DbType
from .db_finder import _find_db, _pick_db_file
from .pages import (
_page_dashboard, _page_runs, _page_pec, _page_logs,
_page_report, _page_documenti, _page_iscrizioni, _page_api_iscrizioni, _page_email, _page_server_logs,
_page_report, _page_documenti, _page_iscrizioni, _page_api_iscrizioni, _page_sharepoint, _page_email, _page_server_logs,
_page_schema,
)
from .queries.db import e as _e, query as _query, detect_db as _detect_db
from .queries import documenti as q_documenti
@@ -110,11 +111,13 @@ def make_handler(db_path: str):
'/': lambda: _page_dashboard(db_path),
'/runs': lambda: _page_runs(db_path),
'/logs': _logs_page,
'/schema': lambda: _page_schema(db_path),
'/server-logs': lambda: _page_server_logs(db_path),
}
if db_type == DbType.INTRAZ:
routes['/steps'] = lambda: _page_iscrizioni(db_path)
routes['/iscrizioni-api'] = lambda: _page_api_iscrizioni(db_path)
routes['/sharepoint'] = lambda: _page_sharepoint(db_path)
routes['/email'] = lambda: _page_email(db_path)
else:
routes['/report'] = lambda: _page_report(db_path)
@@ -146,11 +149,13 @@ def run_server(db_path: str, host: str = 'localhost', port: int = 8080):
if db_type == DbType.INTRAZ:
print(f" RPA Steps : http://localhost:{port}/steps")
print(f" Iscr. API : http://localhost:{port}/iscrizioni-api")
print(f" SharePoint : http://localhost:{port}/sharepoint")
print(f" Email : http://localhost:{port}/email")
else:
print(f" PEC : http://localhost:{port}/pec")
print(f" Documenti : http://localhost:{port}/documenti")
print(f" Report : http://localhost:{port}/report")
print(f" Schema : http://localhost:{port}/schema")
print(f" Log DB : http://localhost:{port}/logs")
print(f" Server log : http://localhost:{port}/server-logs")
print(f" DB : {db_path}")
+13 -27
View File
@@ -1,5 +1,17 @@
(function () {
// ── Details toggle (show/hide data columns) ──────────────────────────────
var btnDetails = document.getElementById('btn-toggle-details');
var twMain = document.getElementById('tw-main');
if (btnDetails && twMain) {
btnDetails.classList.add('active');
btnDetails.addEventListener('click', function () {
var hidden = twMain.classList.toggle('data-hidden');
btnDetails.classList.toggle('active', !hidden);
requestAnimationFrame(syncMirrorWidth);
});
}
var COLS = ['sorgente', 'azienda', 'sessione', 'corso', 'data',
'ftp', 'bc', 'iscrizione', 'odv', 'sharepoint', 'report', 'email'];
var STEP_COLS = ['ftp', 'bc', 'iscrizione', 'odv', 'sharepoint', 'report', 'email'];
@@ -205,32 +217,6 @@
});
});
// ── Sticky mirror scrollbar ───────────────────────────────────────────────
(function () {
var tw = document.getElementById('tw-main');
var mirror = document.getElementById('tw-mirror');
var inner = document.getElementById('tw-inner');
if (!tw || !mirror || !inner) return;
function syncInnerWidth() {
inner.style.width = tw.scrollWidth + 'px';
}
syncInnerWidth();
new ResizeObserver(syncInnerWidth).observe(tw);
var syncing = false;
mirror.addEventListener('scroll', function () {
if (syncing) return;
syncing = true;
tw.scrollLeft = mirror.scrollLeft;
syncing = false;
});
tw.addEventListener('scroll', function () {
if (syncing) return;
syncing = true;
mirror.scrollLeft = tw.scrollLeft;
syncing = false;
});
})();
function syncMirrorWidth() {} // no-op, mirror replaced by native scrollbar
})();
+74
View File
@@ -0,0 +1,74 @@
(function () {
if (typeof Chart === 'undefined' || !_schemaLabels || !_schemaLabels.length) return;
var COLORS = [
'#2563eb','#16a34a','#d97706','#dc2626','#0284c7',
'#6d28d9','#db2777','#059669','#ea580c','#0891b2',
'#0f766e','#b45309','#7c3aed','#be185d','#0369a1'
];
function palette(n) {
var out = [];
for (var i = 0; i < n; i++) out.push(COLORS[i % COLORS.length]);
return out;
}
// --- bar chart: most used processes ---
var ctxBar = document.getElementById('chartBar');
if (ctxBar) {
new Chart(ctxBar, {
type: 'bar',
data: {
labels: _schemaLabels,
datasets: [{
label: 'Esecuzioni',
data: _schemaCounts,
backgroundColor: palette(_schemaLabels.length),
borderRadius: 4,
borderSkipped: false
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, ticks: { precision: 0 } }
}
}
});
}
// --- pie chart: time per process ---
var ctxPie = document.getElementById('chartPie');
if (ctxPie) {
new Chart(ctxPie, {
type: 'pie',
data: {
labels: _schemaLabels,
datasets: [{
data: _schemaMinutes,
backgroundColor: palette(_schemaLabels.length)
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'right', labels: { font: { size: 12 } } },
tooltip: {
callbacks: {
label: function (ctx) {
var min = ctx.parsed;
var h = Math.floor(min / 60);
var m = min % 60;
var dur = h > 0 ? h + 'h ' + m + 'min' : m + 'min';
return ' ' + ctx.label + ': ' + dur;
}
}
}
}
}
});
}
}());
+16
View File
@@ -0,0 +1,16 @@
(function(){
function applyFilter() {
var active = {};
document.querySelectorAll('.email-filter input[type="checkbox"]').forEach(function(chk) {
active[chk.getAttribute('data-stato')] = chk.checked;
});
document.querySelectorAll('tbody tr[data-stato]').forEach(function(tr) {
var stato = tr.getAttribute('data-stato');
var show = (stato in active) ? active[stato] : true;
tr.style.display = show ? '' : 'none';
});
}
document.querySelectorAll('.email-filter input[type="checkbox"]').forEach(function(chk) {
chk.addEventListener('change', applyFilter);
});
})();
+35 -20
View File
@@ -145,28 +145,16 @@
.cfp-all { font-weight: 600 !important; }
.cfp-sep { height: 1px; background: var(--brd, #e2e8f0); margin: 3px 0; }
/* Horizontal scroll — let table grow beyond container */
.tw { overflow-x: hidden; }
/* Horizontal scroll with blue native scrollbar */
.tw { overflow-x: auto; scrollbar-width: thin; scrollbar-color: #2563eb #dbeafe; }
.tw::-webkit-scrollbar { height: 14px; }
.tw::-webkit-scrollbar-track { background: #dbeafe; border-radius: 99px; }
.tw::-webkit-scrollbar-thumb { background: #2563eb; border-radius: 99px; border: 2px solid #dbeafe; }
.tw::-webkit-scrollbar-thumb:hover { background: #1d4ed8; }
.tw table { width: max-content; min-width: 100%; }
/* Sticky mirror scrollbar — always visible at viewport bottom */
.tw-scroll-mirror {
position: sticky;
bottom: 0;
overflow-x: scroll;
overflow-y: hidden;
height: 14px;
background: var(--surf);
border-top: 1px solid var(--brd);
z-index: 40;
}
.tw-scroll-inner { height: 1px; }
.tw-scroll-mirror { scrollbar-color: #2563eb #dbeafe; scrollbar-width: thin; }
.tw-scroll-mirror::-webkit-scrollbar { height: 14px; }
.tw-scroll-mirror::-webkit-scrollbar-track { background: #dbeafe; border-radius: 99px; }
.tw-scroll-mirror::-webkit-scrollbar-thumb { background: #2563eb; border-radius: 99px; border: 2px solid #dbeafe; }
.tw-scroll-mirror::-webkit-scrollbar-thumb:hover { background: #1d4ed8; }
/* Mirror kept for layout, hidden */
.tw-scroll-mirror { display: none; }
/* Step cells with inline error */
.step-td { vertical-align: top; min-width: 80px; }
@@ -201,3 +189,30 @@
background: #eff6ff;
border-color: var(--pri, #2563eb);
}
/* Details toggle button */
.btn-toggle-details {
background: none;
border: 1px solid var(--brd, #e2e8f0);
border-radius: 5px;
padding: .28rem .75rem;
font-size: .82rem;
cursor: pointer;
color: var(--mut, #64748b);
font-family: inherit;
transition: background .12s, border-color .12s, color .12s;
}
.btn-toggle-details.active {
background: #eff6ff;
border-color: var(--pri, #2563eb);
color: var(--pri, #2563eb);
}
.btn-toggle-details:hover { border-color: var(--pri, #2563eb); color: var(--pri, #2563eb); }
/* Hide data columns when Details is off */
.data-hidden .data-th,
.data-hidden .data-td { display: none; }
/* Log link button inside # column */
.td-id { white-space: nowrap; }
.td-id .btn-log { margin-left: 5px; vertical-align: middle; }
+5
View File
@@ -0,0 +1,5 @@
.schema-charts{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:8px}
.chart-box{background:var(--surf);border:1px solid var(--brd);border-radius:8px;padding:20px 24px}
.chart-box h3{font-size:.75rem;font-weight:600;color:var(--mut);margin-bottom:16px;
text-transform:uppercase;letter-spacing:.05em}
@media(max-width:800px){.schema-charts{grid-template-columns:1fr}}
+3
View File
@@ -73,3 +73,6 @@ th.sort-desc::after{content:' \25BC';font-size:.65rem;opacity:.7}
.modal-box li{background:var(--bg);border:1px solid var(--brd);border-radius:6px;padding:10px 14px;font-size:.875rem;font-weight:500}
.modal-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:1.2rem;cursor:pointer;color:var(--mut);line-height:1;padding:2px 6px;border-radius:4px}
.modal-close:hover{color:var(--txt);background:var(--bg)}
/* Log link button (shared across pages) */
.btn-log{display:inline-block;padding:1px 7px;border:1px solid var(--brd,#e2e8f0);border-radius:4px;font-size:.78rem;line-height:1.6;color:var(--pri,#2563eb);text-decoration:none;white-space:nowrap;transition:background .12s,border-color .12s}
.btn-log:hover{background:#eff6ff;border-color:var(--pri,#2563eb)}
+5 -3
View File
@@ -10,9 +10,10 @@
<body>
<header>
<div class="hdr-left">
<a href="/" class="hdr-nav ${nav_dashboard}">Dashboard</a>
<a href="/runs" class="hdr-nav ${nav_runs}">Processi RPA</a>
<a href="/logs" class="hdr-nav ${nav_logs}">Log DB</a>
<a href="/" class="hdr-nav ${nav_dashboard}">Dashboard</a>
<a href="/schema" class="hdr-nav ${nav_schema}">Schema</a>
<a href="/runs" class="hdr-nav ${nav_runs}">Processi RPA</a>
<a href="/logs" class="hdr-nav ${nav_logs}">Log DB</a>
</div>
<h1>${h1_title}</h1>
<div class="hdr-right">
@@ -30,6 +31,7 @@
<nav>
${nav_step_link}
${nav_report_link}
${nav_sharepoint_link}
${nav_pec_link}
</nav>
<main>
+1
View File
@@ -2,6 +2,7 @@
<div class="page-filter-wrap">
<div class="isc-header">
<h2>&#128203; Corsi Intraziendali</h2>
<button id="btn-toggle-details" class="btn-toggle-details" title="Mostra/nascondi BC e Iscrizione">Details</button>
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
<span class="filters-btn-text">Filtri</span>
+1 -1
View File
@@ -1,7 +1,7 @@
<section class="page-header">
<div class="page-filter-wrap">
<div class="runs-header">
<h2>Processi RPA (ultimi 50)</h2>
<h2>Processi RPA</h2>
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
<span class="filters-btn-text">Filtri</span>
+19
View File
@@ -0,0 +1,19 @@
<section>
<h2>&#128202; Schema Processi</h2>
<div class="schema-charts">
<div class="chart-box">
<h3>Processi pi&ugrave; usati</h3>
<canvas id="chartBar"></canvas>
</div>
<div class="chart-box">
<h3>Tempo per processo (minuti)</h3>
<canvas id="chartPie"></canvas>
</div>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
var _schemaLabels = ${labels_json};
var _schemaCounts = ${counts_json};
var _schemaMinutes = ${minutes_json};
</script>
+23
View File
@@ -0,0 +1,23 @@
<section class="page-header">
<div class="page-filter-wrap">
<div class="email-header">
<h2>SharePoint (ultimi 500)</h2>
<button id="btn-toggle-filters" class="btn-toggle-filters" title="Mostra/nascondi filtri">
<svg id="filters-arrow" class="filters-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>
<span class="filters-btn-text">Filtri</span>
</button>
</div>
<div id="filters-collapsible" class="filters-collapsible">
<div class="filters-collapsible-inner">
<div class="email-filter">
<label><input type="checkbox" data-stato="uploaded" checked> Uploaded</label>
<label><input type="checkbox" data-stato="pending" checked> Pending</label>
<label><input type="checkbox" data-stato="errore" checked> Errore</label>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="tw">${tbl_sharepoint}</div>
</section>