Password dimenticata? · Non hai un account? Registrati
Inserisci l'email associata al tuo account. Riceverai un link per impostare una nuova password.
Per accedere a tutte le funzionalità scegli come configurare il tuo profilo.
Seleziona il tuo nome dall'elenco:
La tua registrazione è stata ricevuta. Un amministratore approverà il tuo account a breve. Riceverai accesso completo alla piattaforma non appena approvato.
Seleziona un modulo dalla barra laterale
Scegli il periodo da stampare:
Il file .ics è compatibile con Google Calendar, iCloud, Outlook e qualsiasi app calendario standard.
TurniPneumo è il gestionale dei turni per la Scuola di Specializzazione in Pneumologia dell'Università di Salerno. Permette di pianificare le presenze settimanali degli specializzandi su ambulatori, reparti e altre attività, con suddivisione mattina/pomeriggio.
La dashboard è la schermata principale della piattaforma. Mostra i widget con i riepiloghi più importanti: consegne in sospeso, turni della settimana, avvisi non letti dalla bacheca e contatti rapidi dalla rubrica.
Il modulo turni gestisce il calendario settimanale degli specializzandi.
Le consegne sono note operative che uno specializzando lascia al turno successivo.
La bacheca raccoglie comunicazioni e avvisi pubblicati dal personale docente e dagli amministratori.
La rubrica raccoglie i contatti di tutti i membri della scuola: docenti e specializzandi.
Ogni volta che accedi, la piattaforma mostra in cima alla dashboard un pannello riepilogativo con tutte le modifiche effettuate dagli altri utenti dall'ultima tua sessione: nuovi turni, consegne, avvisi ecc.
Guide specifiche dei moduli installati e attivi sulla piattaforma.
Caricamento…
Cronologia delle versioni e delle novità introdotte sulla piattaforma.
Prima versione stabile di PneumoUnisa Hub (già nota internamente come "v2.0"), comprendente:
.zip del modulo dall'interfaccia.PneumoUnisa Hub è un'applicazione SPA (Single Page Application) costruita su stack PHP 8.4 + MySQL/PDO, senza framework frontend. Il backend è un singolo file api.php con routing action-based; il frontend è HTML statico + JS modulare caricato in sequenza.
tid() — non ci sono dati condivisi tra installazioni diverse.requireActive() verifica la sessione; requireAdmin() verifica il ruolo.api.php?action=X (o /api/X via .htaccess). Ogni endpoint è un blocco if ($action === 'X' && $method === 'GET/POST/...').migrateSchema() in api.php aggiunge colonne/tabelle mancanti via ALTER TABLE ad ogni richiesta (idempotente). I nuovi moduli devono usare lo stesso pattern.public/
├── api.php ← Backend unico (routing, auth, core endpoints)
├── config.php ← Connessione DB, costanti TENANT_ID
├── vapid.php ← Libreria notifiche push VAPID
├── migration_helper.php ← Restore backup (v1+v2)
├── setup-guidato.php ← Setup iniziale tenant
├── index.html ← SPA shell (HTML + modal)
├── js/
│ ├── core.js ← State globale, save/load, widget drag&drop
│ ├── auth.js ← Login, profilo, utenti, backup
│ ├── theme.js ← Branding, moduli, modalità turnazione
│ ├── turni.js ← Logica modulo turni
│ └── modules-admin.js ← Gestione addon
├── css/
│ └── main.css ← Tutti gli stili
├── modules/
│ ├── rubrica/ ← Addon rubrica
│ │ ├── manifest.json
│ │ ├── module.php ← Endpoint API del modulo
│ │ └── module.js ← IIFE frontend del modulo
│ └── bacheca/ ← Addon bacheca
│ ├── manifest.json
│ ├── module.php
│ └── module.js
└── backups/
└── {tenant_id}/ ← Backup automatici (protetti da .htaccess)
Un modulo addon è composto da 3 file in modules/{id}/:
1. manifest.json
{
"id": "miomodulo",
"name": "Mio Modulo",
"icon": "🔧",
"color": "#6366f1",
"version": "1.0.0",
"desc": "Descrizione breve del modulo."
}
2. module.php — Backend
<?php
// Schema migration (idempotente)
$tables = db()->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
if (!in_array('mio_modulo_items', $tables)) {
db()->exec("CREATE TABLE mio_modulo_items (
id VARCHAR(32) NOT NULL PRIMARY KEY,
tenant_id VARCHAR(32) NOT NULL,
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
// Endpoint GET /api/miomodulo-items
if ($action === 'miomodulo-items' && $method === 'GET') {
requireActive();
$rows = q('SELECT * FROM mio_modulo_items WHERE tenant_id=? ORDER BY created_at DESC', [tid()])->fetchAll();
respond($rows);
}
// Endpoint POST /api/miomodulo-items
if ($action === 'miomodulo-items' && $method === 'POST') {
requireActive();
$body = jsonBody();
$id = bin2hex(random_bytes(8));
q('INSERT INTO mio_modulo_items (id,tenant_id,title) VALUES (?,?,?)',
[$id, tid(), trim($body['title'] ?? '')]);
respond(['ok' => true, 'id' => $id]);
}
// Registrazione hook backup (opzionale ma consigliato)
if (function_exists('registerBackupModule')) {
registerBackupModule('miomodulo', 'Mio Modulo',
fn() => ['items' => q('SELECT * FROM mio_modulo_items WHERE tenant_id=?', [tid()])->fetchAll()],
function(array $data): void {
q('DELETE FROM mio_modulo_items WHERE tenant_id=?', [tid()]);
$s = db()->prepare('INSERT IGNORE INTO mio_modulo_items (id,tenant_id,title) VALUES (?,?,?)');
foreach ($data['items'] ?? [] as $r) $s->execute([$r['id'], tid(), $r['title']]);
}
);
}
3. module.js — Frontend (IIFE)
(function () {
const MODULE_HTML = `
<div id="module-miomodulo" style="display:none">
<div style="max-width:720px;margin:0 auto;padding:20px 16px">
<h2>Mio Modulo</h2>
<div id="miomodulo-list"></div>
</div>
</div>`;
// (opzionale) HTML per la sezione nel pannello Personalizzazione
const BRANDING_HTML = `
<div class="branding-section">
<div class="branding-section-title">Mio Modulo</div>
<!-- Opzioni di configurazione -->
</div>`;
// Registra l'addon (appare in Gestione Moduli)
registerAddon({
id: 'miomodulo', name: 'Mio Modulo', icon: '🔧',
desc: 'Descrizione breve.', core: false,
});
// Registra il modulo (aggiunge voce in nav, inietta HTML, etc.)
registerModule({
id: 'miomodulo',
name: 'Mio Modulo',
icon: '🔧',
color: '#6366f1',
html: MODULE_HTML,
brandingHtml: BRANDING_HTML, // ometti se non hai opzioni
brandingLoad: () => _loadConfig(), // chiamata all'apertura del pannello
brandingSave: () => _saveConfig(), // chiamata al salvataggio
onOpen: () => _loadItems(), // chiamata all'apertura del modulo
});
async function _loadItems() {
const el = document.getElementById('miomodulo-list');
const res = await fetch(apiUrl('miomodulo-items'), { credentials: 'include' });
const items = res.ok ? await res.json() : [];
el.innerHTML = items.map(i => `<div>${escapeHtml(i.title)}</div>`).join('');
}
})();
apiUrl(action) — restituisce l'URL corretto per l'endpoint (gestisce dev/prod).escapeHtml(str) — sanitizza stringhe per l'inserimento nel DOM.toast(msg) — mostra una notifica temporanea in basso a destra.openModal(id) / closeModal(id) — apre/chiude un modal overlay.currentUser — oggetto utente corrente: {id, username, displayName, role, residentId, teacherId}.state — stato globale turni: {residents, locations, schedule, scheduleOre, weekOffset}._currentTheme — impostazioni tenant correnti (colori, nome app, modalità turni, ecc.).registerModule({id, name, icon, color, html, brandingHtml, brandingLoad, brandingSave, onOpen}) — registra il modulo nel sistema di navigazione.registerAddon({id, name, icon, desc, core}) — registra il modulo nel pannello Gestione Moduli.tid() — restituisce il tenant ID corrente.db() — restituisce la connessione PDO.q(string $sql, array $params) — esegue una query parametrizzata, restituisce PDOStatement.respond(array $data) — invia risposta JSON 200 ed esce.respondErr(int $code, string $msg) — invia risposta JSON di errore ed esce.jsonBody() — legge e decodifica il corpo JSON della richiesta.requireActive() — verifica sessione attiva, restituisce $uid. Risponde 401 se non autenticato.requireAdmin() — come sopra ma verifica anche ruolo admin.userById(string $id) — restituisce i dati di un utente per ID.registerBackupModule(id, label, export, import) — registra i dati del modulo nel sistema di backup.vapidSendToAll(PDO $db, string $tid, string $pub, string $pem) — invia notifica push a tutti gli utenti del tenant con sottoscrizione attiva.bacheca_, miomodulo_) per evitare conflitti.CREATE TABLE IF NOT EXISTS e ALTER TABLE ADD COLUMN IF NOT EXISTS (o controlla con SHOW COLUMNS).escapeHtml() su tutti i dati utente inseriti nel DOM per prevenire XSS._bachecaOpenDetail) e vanno assegnate a window se chiamate da attributi HTML inline..widget-card con data-widget="{id}" e collegati agli event handler drag&drop globali (wDragStart, wDragOver, wDrop, wDragEnd, wDragLeave).registerBackupModule() se vuoi che i tuoi dati vengano inclusi nei backup della piattaforma.tenants — id, name, created_at
tenant_settings — tenant_id, key_name, value (impostazioni e chiavi VAPID)
users — id, tenant_id, username, display_name, email, role,
salt, hash, resident_id, teacher_id, status
residents — id, tenant_id, name, year, color, phone, birth_date,
manual_only, active_days, sort_order
locations — id, tenant_id, name, type, color, min_per_day, active_days, sort_order
schedule — tenant_id, resident_id, location_id, date,
start_time, end_time ← null in modalità classica
teachers — id, tenant_id, name, position, phone, email, color
consegne — id, tenant_id, title, body, priority, category,
author_id, author_name, created_at, completed_*
consegne_categories— id, tenant_id, name, sort_order
audit_log — tenant_id, user_id, display_name, detail, created_at
push_subscriptions — tenant_id, user_id, endpoint, p256dh, auth
pending_registrations — tenant_id, token, username, ...
bacheca_categories — id, tenant_id, name, color, icon, sort_order
bacheca_posts — id, tenant_id, author_id, category_id, title, body,
pinned, created_at, updated_at
bacheca_reads — tenant_id, post_id, user_id, read_at
glob('modules/*/module.php') e glob('modules/*/module.js') — non è necessario registrarli manualmente nel codice core.
L'operazione verrà registrata con il tuo nome e l'orario attuale.
.zip contenente il modulo nella struttura standardCaricamento…