File: /var/www/rspp/rspp_schema.sql
-- =====================================================================
-- GESTIONALE RSPP / CONSULENTI SICUREZZA — SCHEMA FONDAZIONALE
-- Stack: MariaDB 10.6+ / InnoDB / utf8mb4
-- Modello: SaaS multi-tenant (tenant scoping applicativo, NON RLS nativa)
-- Normativa formazione: Accordo Stato-Regioni 17/04/2025 (in vigore 24/05/2025)
--
-- NOTE MULTI-TENANT:
-- - Ogni tabella di dominio porta tenant_id.
-- - L'isolamento NON e' garantito dal DB (MariaDB non ha RLS): va imposto
-- nel layer dati applicativo, che deve iniettare sempre WHERE tenant_id=?.
-- - Centralizzare TUTTO l'accesso dati in un repository non bypassabile.
-- =====================================================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ---------------------------------------------------------------------
-- 1. TENANT, UTENTI, ABBONAMENTI
-- ---------------------------------------------------------------------
CREATE TABLE tenants (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ragione_sociale VARCHAR(255) NOT NULL,
partita_iva VARCHAR(16) NULL,
codice_fiscale VARCHAR(16) NULL,
email VARCHAR(255) NOT NULL,
piano ENUM('free','pro','enterprise') NOT NULL DEFAULT 'free',
stato ENUM('attivo','sospeso','trial','cessato') NOT NULL DEFAULT 'trial',
regione VARCHAR(64) NULL, -- per periodicita' aggiornamenti regione-dipendenti
trial_scade_il DATE NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_tenant_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
nome VARCHAR(120) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- password_hash() PHP, ARGON2ID
ruolo ENUM('owner','consulente','collaboratore','readonly') NOT NULL DEFAULT 'consulente',
attivo TINYINT(1) NOT NULL DEFAULT 1,
ultimo_login DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_user_email (email),
KEY idx_users_tenant (tenant_id),
CONSTRAINT fk_users_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Token OAuth per integrazioni cloud (Google Drive/Calendar/Gmail, Microsoft Graph/OneDrive)
CREATE TABLE integrazioni (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
provider ENUM('google','microsoft') NOT NULL,
account_email VARCHAR(255) NULL,
access_token TEXT NULL, -- cifrare a riposo (libsodium)
refresh_token TEXT NULL, -- cifrare a riposo
scope TEXT NULL,
expires_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_integr_tenant (tenant_id),
CONSTRAINT fk_integr_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 2. CLIENTI (aziende seguite dal consulente)
-- ---------------------------------------------------------------------
CREATE TABLE clienti (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
ragione_sociale VARCHAR(255) NOT NULL,
partita_iva VARCHAR(16) NULL,
codice_fiscale VARCHAR(16) NULL,
codice_ateco VARCHAR(16) NULL, -- determina classe di rischio
classe_rischio ENUM('basso','medio','alto') NULL,
indirizzo VARCHAR(255) NULL,
comune VARCHAR(120) NULL,
provincia CHAR(2) NULL,
cap VARCHAR(5) NULL,
pec VARCHAR(255) NULL,
note TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_clienti_tenant (tenant_id),
CONSTRAINT fk_clienti_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Figure della sicurezza ex D.Lgs 81/08 (DdL, RSPP, MC, RLS, Preposto, Addetto emergenza...)
CREATE TABLE figure_sicurezza (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
cliente_id BIGINT UNSIGNED NOT NULL,
tipo ENUM('datore_lavoro','rspp','aspp','medico_competente','rls',
'preposto','dirigente','addetto_antincendio','addetto_primo_soccorso',
'coordinatore') NOT NULL,
nominativo VARCHAR(160) NOT NULL,
email VARCHAR(255) NULL,
telefono VARCHAR(40) NULL,
data_nomina DATE NULL,
esterno TINYINT(1) NOT NULL DEFAULT 0, -- es. RSPP/MC esterno
attivo TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
KEY idx_figure_tenant (tenant_id),
KEY idx_figure_cliente (cliente_id),
CONSTRAINT fk_figure_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_figure_cliente FOREIGN KEY (cliente_id) REFERENCES clienti(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 3. APPALTI (N per cliente) e DIPENDENTI
-- ---------------------------------------------------------------------
CREATE TABLE appalti (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
cliente_id BIGINT UNSIGNED NOT NULL,
descrizione VARCHAR(255) NOT NULL,
committente VARCHAR(255) NULL,
sede_operativa VARCHAR(255) NULL,
data_inizio DATE NULL,
data_fine DATE NULL,
stato ENUM('attivo','sospeso','chiuso') NOT NULL DEFAULT 'attivo',
PRIMARY KEY (id),
KEY idx_appalti_tenant (tenant_id),
KEY idx_appalti_cliente (cliente_id),
CONSTRAINT fk_appalti_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_appalti_cliente FOREIGN KEY (cliente_id) REFERENCES clienti(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE dipendenti (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
appalto_id BIGINT UNSIGNED NOT NULL,
nome VARCHAR(120) NOT NULL,
cognome VARCHAR(120) NOT NULL,
codice_fiscale VARCHAR(16) NULL,
mansione VARCHAR(160) NULL,
data_assunzione DATE NULL,
data_cessazione DATE NULL,
-- regola Accordo 2025: niente piu' "60 giorni", formazione PRIMA della mansione
idoneo_alla_mansione TINYINT(1) NOT NULL DEFAULT 0, -- flag calcolato: ha formazione+idoneita'?
attivo TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id),
KEY idx_dip_tenant (tenant_id),
KEY idx_dip_appalto (appalto_id),
CONSTRAINT fk_dip_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_dip_appalto FOREIGN KEY (appalto_id) REFERENCES appalti(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 4. DOCUMENTI + DUAL STORAGE
-- Il DB e' la source of truth dei metadati; i provider sono blob storage.
-- ---------------------------------------------------------------------
CREATE TABLE documenti (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
-- polimorfico: un documento appartiene a cliente | appalto | dipendente | figura
owner_type ENUM('cliente','appalto','dipendente','figura') NOT NULL,
owner_id BIGINT UNSIGNED NOT NULL,
categoria VARCHAR(80) NOT NULL, -- es. 'DVR','contratto','idoneita_mc','attestato','DPI','DiCo'
titolo VARCHAR(255) NOT NULL,
data_emissione DATE NULL,
data_scadenza DATE NULL, -- NULL = senza scadenza
stato ENUM('valido','in_scadenza','scaduto','archiviato') NOT NULL DEFAULT 'valido',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_doc_tenant (tenant_id),
KEY idx_doc_owner (owner_type, owner_id),
KEY idx_doc_scadenza (tenant_id, data_scadenza),
CONSTRAINT fk_doc_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE documento_storage_refs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
documento_id BIGINT UNSIGNED NOT NULL,
provider ENUM('google_drive','onedrive','locale') NOT NULL,
file_id VARCHAR(255) NULL, -- id del file sul provider
path VARCHAR(1024) NULL,
url VARCHAR(1024) NULL,
checksum VARCHAR(64) NULL, -- sha256, per rilevare divergenze tra provider
sincronizzato_il DATETIME NULL,
PRIMARY KEY (id),
KEY idx_storage_doc (documento_id),
CONSTRAINT fk_storage_doc FOREIGN KEY (documento_id) REFERENCES documenti(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 5. FORMAZIONE — Catalogo corsi (Accordo Stato-Regioni 2025) + istanze
-- ---------------------------------------------------------------------
CREATE TABLE corso_tipi (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NULL, -- NULL = catalogo di sistema condiviso
codice VARCHAR(40) NOT NULL,
descrizione VARCHAR(255) NOT NULL,
durata_ore SMALLINT NULL,
-- periodicita' aggiornamento in mesi (NULL = senza aggiornamento periodico)
aggiornamento_mesi SMALLINT NULL,
durata_aggiornamento_ore SMALLINT NULL,
riferimento_norm VARCHAR(120) NULL,
PRIMARY KEY (id),
KEY idx_corsotipi_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Seed catalogo di sistema (Accordo Stato-Regioni 17/04/2025).
-- NB: l'aggiornamento del PREPOSTO varia per regione (2 o 5 anni): reso configurabile per tenant.
INSERT INTO corso_tipi
(tenant_id, codice, descrizione, durata_ore, aggiornamento_mesi, durata_aggiornamento_ore, riferimento_norm) VALUES
(NULL,'DDL_BASE', 'Datore di lavoro - formazione base', 16, 60, 6, 'ASR 2025'),
(NULL,'DDL_CANTIERI', 'Datore di lavoro - modulo cantieri', 6, 60, 6, 'ASR 2025'),
(NULL,'DIRIGENTE', 'Dirigenti', 12, 60, 6, 'ASR 2025'),
(NULL,'DIRIGENTE_CANTIERI','Dirigenti - modulo cantieri', 6, 60, 6, 'ASR 2025'),
(NULL,'PREPOSTO', 'Preposti (in presenza)', 12, 60, 6, 'ASR 2025'),
(NULL,'LAV_GENERALE', 'Lavoratori - formazione generale', 4, NULL, NULL, 'ASR 2025'),
(NULL,'LAV_SPEC_BASSO', 'Lavoratori - specifica rischio basso', 4, 60, 6, 'ASR 2025'),
(NULL,'LAV_SPEC_MEDIO', 'Lavoratori - specifica rischio medio', 8, 60, 6, 'ASR 2025'),
(NULL,'LAV_SPEC_ALTO', 'Lavoratori - specifica rischio alto', 12, 60, 6, 'ASR 2025'),
(NULL,'RLS', 'Rappresentante dei lavoratori (RLS)', 32, 12, 8, 'D.Lgs 81/08 art.37'),
(NULL,'DLSPP_BASSO', 'Datore di lavoro-RSPP rischio basso', 16, 60, 8, 'ASR 2025'),
(NULL,'DLSPP_MEDIO', 'Datore di lavoro-RSPP rischio medio', 32, 60, 8, 'ASR 2025'),
(NULL,'DLSPP_ALTO', 'Datore di lavoro-RSPP rischio alto', 48, 60, 8, 'ASR 2025'),
(NULL,'PRIMO_SOCCORSO_A', 'Addetto primo soccorso gruppo A', 16, 36, 6, 'DM 388/2003'),
(NULL,'PRIMO_SOCCORSO_BC', 'Addetto primo soccorso gruppo B/C', 12, 36, 4, 'DM 388/2003'),
(NULL,'ANTINCENDIO_1', 'Addetto antincendio livello 1 (basso)', 4, 60, 2, 'DM 02/09/2021'),
(NULL,'ANTINCENDIO_2', 'Addetto antincendio livello 2 (medio)', 8, 60, 5, 'DM 02/09/2021'),
(NULL,'ANTINCENDIO_3', 'Addetto antincendio livello 3 (alto)', 16, 60, 8, 'DM 02/09/2021');
-- Istanze formative: chi ha fatto cosa, quando, con scadenza calcolata
CREATE TABLE formazioni (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
corso_tipo_id BIGINT UNSIGNED NOT NULL,
-- soggetto: un dipendente OPPURE una figura della sicurezza
soggetto_type ENUM('dipendente','figura') NOT NULL,
soggetto_id BIGINT UNSIGNED NOT NULL,
data_corso DATE NOT NULL,
-- data_scadenza = data_corso + aggiornamento_mesi (calcolata in app o trigger)
data_scadenza DATE NULL,
ente_formatore VARCHAR(200) NULL,
attestato_doc_id BIGINT UNSIGNED NULL,
stato ENUM('valido','in_scadenza','scaduto') NOT NULL DEFAULT 'valido',
PRIMARY KEY (id),
KEY idx_form_tenant (tenant_id),
KEY idx_form_soggetto (soggetto_type, soggetto_id),
KEY idx_form_scadenza (tenant_id, data_scadenza),
CONSTRAINT fk_form_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_form_corso FOREIGN KEY (corso_tipo_id) REFERENCES corso_tipi(id),
CONSTRAINT fk_form_doc FOREIGN KEY (attestato_doc_id) REFERENCES documenti(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 6. MODULISTICA 81/08 compilata da mobile (nomine, schede DPI, verbali, checklist)
-- ---------------------------------------------------------------------
CREATE TABLE moduli_compilati (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
appalto_id BIGINT UNSIGNED NULL,
cliente_id BIGINT UNSIGNED NULL,
tipo ENUM('nomina','scheda_dpi','verbale_riunione','checklist','sopralluogo') NOT NULL,
dati JSON NOT NULL, -- payload del form (MariaDB: LONGTEXT + json_valid)
compilato_da BIGINT UNSIGNED NULL, -- users.id
-- firma (riuso pattern OTP-SMS gia' usato su Airpro/Skebby)
firmato TINYINT(1) NOT NULL DEFAULT 0,
firma_metodo ENUM('otp_sms','disegno','nessuna') NULL,
firmato_il DATETIME NULL,
pdf_doc_id BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_mod_tenant (tenant_id),
CONSTRAINT fk_mod_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_mod_pdf FOREIGN KEY (pdf_doc_id) REFERENCES documenti(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 7. SCADENZARIO unificato + sync Google Calendar
-- Vista materializzata "logica": le scadenze provengono da documenti,
-- formazioni e idoneita'. Questa tabella le aggrega per alert/calendar.
-- ---------------------------------------------------------------------
CREATE TABLE scadenze (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
origine_type ENUM('documento','formazione','idoneita','attivita') NOT NULL,
origine_id BIGINT UNSIGNED NOT NULL,
titolo VARCHAR(255) NOT NULL,
data_scadenza DATE NOT NULL,
giorni_preavviso SMALLINT NOT NULL DEFAULT 30,
stato ENUM('aperta','notificata','gestita','chiusa') NOT NULL DEFAULT 'aperta',
gcal_event_id VARCHAR(255) NULL, -- per sync bidirezionale Google Calendar
PRIMARY KEY (id),
KEY idx_scad_tenant (tenant_id),
KEY idx_scad_data (tenant_id, data_scadenza),
CONSTRAINT fk_scad_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------------
-- 8. AUDIT LOG immutabile (per ispezioni / contenzioso)
-- ---------------------------------------------------------------------
CREATE TABLE audit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NULL,
azione VARCHAR(80) NOT NULL, -- create|update|delete|download|firma|login
entita VARCHAR(80) NOT NULL,
entita_id BIGINT UNSIGNED NULL,
dettaglio JSON NULL,
ip VARCHAR(45) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_audit_tenant (tenant_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET FOREIGN_KEY_CHECKS = 1;