diff --git a/db/migrations/20260605095127_alter_scad_functions_add_contact_fields.php b/db/migrations/20260605095127_alter_scad_functions_add_contact_fields.php
new file mode 100644
index 0000000..8a57493
--- /dev/null
+++ b/db/migrations/20260605095127_alter_scad_functions_add_contact_fields.php
@@ -0,0 +1,138 @@
+hasTable('scad_functions')) {
+ throw new RuntimeException('Table scad_functions does not exist.');
+ }
+
+ $table = $this->table('scad_functions');
+
+ if (!$table->hasColumn('person_full_name')) {
+ $table->addColumn('person_full_name', 'string', [
+ 'limit' => 200,
+ 'null' => true,
+ 'after' => 'description',
+ 'comment' => 'Full name and surname of the person assigned to the function',
+ ]);
+ }
+
+ if (!$table->hasColumn('phone')) {
+ $table->addColumn('phone', 'string', [
+ 'limit' => 80,
+ 'null' => true,
+ 'after' => 'person_full_name',
+ ]);
+ }
+
+ if (!$table->hasColumn('email')) {
+ $table->addColumn('email', 'string', [
+ 'limit' => 190,
+ 'null' => true,
+ 'after' => 'phone',
+ ]);
+ }
+
+ if (!$table->hasColumn('notes')) {
+ $table->addColumn('notes', 'text', [
+ 'null' => true,
+ 'after' => 'email',
+ ]);
+ }
+
+ if (!$table->hasColumn('sort_order')) {
+ $table->addColumn('sort_order', 'integer', [
+ 'signed' => false,
+ 'null' => false,
+ 'default' => 0,
+ 'after' => 'status',
+ ]);
+ }
+
+ if (!$table->hasIndexByName('idx_scad_functions_name')) {
+ $table->addIndex(['name'], [
+ 'name' => 'idx_scad_functions_name',
+ ]);
+ }
+
+ if (!$table->hasIndexByName('idx_scad_functions_person_full_name')) {
+ $table->addIndex(['person_full_name'], [
+ 'name' => 'idx_scad_functions_person_full_name',
+ ]);
+ }
+
+ if (!$table->hasIndexByName('idx_scad_functions_email')) {
+ $table->addIndex(['email'], [
+ 'name' => 'idx_scad_functions_email',
+ ]);
+ }
+
+ if (!$table->hasIndexByName('idx_scad_functions_status_sort')) {
+ $table->addIndex(['status', 'sort_order'], [
+ 'name' => 'idx_scad_functions_status_sort',
+ ]);
+ }
+
+ $table->update();
+
+ // Set a default order for existing rows without changing their names.
+ $this->execute("
+ UPDATE scad_functions
+ SET sort_order = id * 10
+ WHERE sort_order = 0
+ ");
+ }
+
+ public function down(): void
+ {
+ if (!$this->hasTable('scad_functions')) {
+ return;
+ }
+
+ $table = $this->table('scad_functions');
+
+ if ($table->hasIndexByName('idx_scad_functions_status_sort')) {
+ $table->removeIndexByName('idx_scad_functions_status_sort');
+ }
+
+ if ($table->hasIndexByName('idx_scad_functions_email')) {
+ $table->removeIndexByName('idx_scad_functions_email');
+ }
+
+ if ($table->hasIndexByName('idx_scad_functions_person_full_name')) {
+ $table->removeIndexByName('idx_scad_functions_person_full_name');
+ }
+
+ if ($table->hasIndexByName('idx_scad_functions_name')) {
+ $table->removeIndexByName('idx_scad_functions_name');
+ }
+
+ if ($table->hasColumn('sort_order')) {
+ $table->removeColumn('sort_order');
+ }
+
+ if ($table->hasColumn('notes')) {
+ $table->removeColumn('notes');
+ }
+
+ if ($table->hasColumn('email')) {
+ $table->removeColumn('email');
+ }
+
+ if ($table->hasColumn('phone')) {
+ $table->removeColumn('phone');
+ }
+
+ if ($table->hasColumn('person_full_name')) {
+ $table->removeColumn('person_full_name');
+ }
+
+ $table->update();
+ }
+}
diff --git a/public/userarea/company-functions.php b/public/userarea/company-functions.php
new file mode 100644
index 0000000..49443af
--- /dev/null
+++ b/public/userarea/company-functions.php
@@ -0,0 +1,617 @@
+getConnection();
+
+function jsonResponse(array $data): void
+{
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data);
+ exit;
+}
+
+function normalizeNullableInt($value): ?int
+{
+ return (isset($value) && $value !== '') ? (int)$value : null;
+}
+
+function normalizeBoolValue($value): int
+{
+ return ((string)$value === '0') ? 0 : 1;
+}
+
+function cleanString(?string $value): string
+{
+ return trim((string)$value);
+}
+
+/* ==========================================
+ AJAX HANDLERS
+ ========================================== */
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
+ $action = $_POST['action'] ?? '';
+
+ try {
+ if ($action === 'add') {
+ $functionName = cleanString($_POST['function_name'] ?? '');
+ $personFullName = cleanString($_POST['person_full_name'] ?? '');
+ $phone = cleanString($_POST['phone'] ?? '');
+ $email = cleanString($_POST['email'] ?? '');
+ $notes = cleanString($_POST['notes'] ?? '');
+ $sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
+ $isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
+
+ if ($functionName === '') {
+ jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
+ }
+
+ if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ jsonResponse(['success' => false, 'message' => 'Email non valida.']);
+ }
+
+ $stmt = $pdo->prepare("\n INSERT INTO company_functions\n (function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)\n VALUES\n (:function_name, :person_full_name, :phone, :email, :notes, :sort_order, :is_active, NOW(), NOW())\n ");
+
+ $stmt->execute([
+ 'function_name' => $functionName,
+ 'person_full_name' => $personFullName !== '' ? $personFullName : '',
+ 'phone' => $phone !== '' ? $phone : null,
+ 'email' => $email !== '' ? $email : null,
+ 'notes' => $notes !== '' ? $notes : null,
+ 'sort_order' => $sortOrder,
+ 'is_active' => $isActive,
+ ]);
+
+ jsonResponse(['success' => true, 'message' => 'Funzione salvata correttamente.']);
+ }
+
+ if ($action === 'edit') {
+ $id = (int)($_POST['id'] ?? 0);
+ $functionName = cleanString($_POST['function_name'] ?? '');
+ $personFullName = cleanString($_POST['person_full_name'] ?? '');
+ $phone = cleanString($_POST['phone'] ?? '');
+ $email = cleanString($_POST['email'] ?? '');
+ $notes = cleanString($_POST['notes'] ?? '');
+ $sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
+ $isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
+
+ if ($id <= 0) {
+ jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
+ }
+
+ if ($functionName === '') {
+ jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
+ }
+
+ if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ jsonResponse(['success' => false, 'message' => 'Email non valida.']);
+ }
+
+ $stmt = $pdo->prepare("\n UPDATE company_functions\n SET function_name = :function_name,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n is_active = :is_active,\n updated_at = NOW()\n WHERE id = :id\n ");
+
+ $stmt->execute([
+ 'function_name' => $functionName,
+ 'person_full_name' => $personFullName !== '' ? $personFullName : '',
+ 'phone' => $phone !== '' ? $phone : null,
+ 'email' => $email !== '' ? $email : null,
+ 'notes' => $notes !== '' ? $notes : null,
+ 'sort_order' => $sortOrder,
+ 'is_active' => $isActive,
+ 'id' => $id,
+ ]);
+
+ jsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
+ }
+
+ if ($action === 'delete') {
+ $id = (int)($_POST['id'] ?? 0);
+
+ if ($id <= 0) {
+ jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
+ }
+
+ $stmt = $pdo->prepare("DELETE FROM company_functions WHERE id = :id");
+ $stmt->execute(['id' => $id]);
+
+ jsonResponse(['success' => true, 'message' => 'Funzione cancellata correttamente.']);
+ }
+
+ jsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
+ } catch (Exception $e) {
+ jsonResponse(['success' => false, 'message' => $e->getMessage()]);
+ }
+}
+
+/* ==========================================
+ PAGE DATA
+ ========================================== */
+$stmtFunctions = $pdo->query("\n SELECT id, function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at\n FROM company_functions\n ORDER BY is_active DESC, sort_order ASC, function_name ASC, person_full_name ASC\n");
+$functions = $stmtFunctions->fetchAll(PDO::FETCH_ASSOC);
+?>
+
+
+
+
+
+
+
+
+
+ Funzioni Aziendali - = htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Elenco Funzioni
+
Gestione di RSPP, medico del lavoro, RLS e altre funzioni aziendali.
+
+
+
+
+
+
+
+
+ | Funzione |
+ Nominativo |
+ Contatti |
+ Note |
+ Ordine |
+ Stato |
+ Azioni |
+
+
+
+
+
+
+ |
+ = htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>
+ |
+
+
+ = htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?>
+
+ Da definire
+
+ |
+
+
+
+ 📞 = htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+
+ ✉️ = htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+ Nessun contatto
+
+ |
+
+
+
+ = htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>
+
+
+ —
+
+ |
+ = $sortOrder ?> |
+
+
+ Attiva
+
+ Non attiva
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/userarea/include/navbar.php b/public/userarea/include/navbar.php
index 7c2e5af..ba0b24b 100644
--- a/public/userarea/include/navbar.php
+++ b/public/userarea/include/navbar.php
@@ -359,6 +359,11 @@
Calendario
+
+
+ Funzioni Aziendali
+
+
diff --git a/public/userarea/scadenzario/functions/index.php b/public/userarea/scadenzario/functions/index.php
index d1da875..8b48ff1 100644
--- a/public/userarea/scadenzario/functions/index.php
+++ b/public/userarea/scadenzario/functions/index.php
@@ -1,15 +1,122 @@
getConnection();
-$functions = $pdo->query("
- SELECT f.*,
- (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
- (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
- FROM scad_functions f
- ORDER BY f.name ASC
-")->fetchAll(PDO::FETCH_ASSOC);
+function scadJsonResponse(array $data): void
+{
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($data);
+ exit;
+}
+
+function scadNullableString($value): ?string
+{
+ $value = trim((string)($value ?? ''));
+ return $value !== '' ? $value : null;
+}
+
+function scadNormalizeStatus(string $status): string
+{
+ return in_array($status, ['active', 'inactive'], true) ? $status : 'active';
+}
+
+function scadValidateEmail($email): ?string
+{
+ $email = scadNullableString($email);
+
+ if ($email !== null && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ throw new Exception('Email non valida.');
+ }
+
+ return $email;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] === '1') {
+ try {
+ $action = $_POST['action'] ?? '';
+
+ if ($action === 'save') {
+ $id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : 0;
+ $name = trim($_POST['name'] ?? '');
+ $description = scadNullableString($_POST['description'] ?? null);
+ $personFullName = scadNullableString($_POST['person_full_name'] ?? null);
+ $phone = scadNullableString($_POST['phone'] ?? null);
+ $email = scadValidateEmail($_POST['email'] ?? null);
+ $notes = scadNullableString($_POST['notes'] ?? null);
+ $sortOrder = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 0;
+ $status = scadNormalizeStatus(trim($_POST['status'] ?? 'active'));
+
+ if ($name === '') {
+ scadJsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
+ }
+
+ if ($id > 0) {
+ $stmt = $pdo->prepare("\n UPDATE scad_functions\n SET name = :name,\n description = :description,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n status = :status,\n updated_at = NOW()\n WHERE id = :id\n ");
+ $stmt->execute([
+ 'name' => $name,
+ 'description' => $description,
+ 'person_full_name' => $personFullName,
+ 'phone' => $phone,
+ 'email' => $email,
+ 'notes' => $notes,
+ 'sort_order' => $sortOrder,
+ 'status' => $status,
+ 'id' => $id,
+ ]);
+
+ scadJsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
+ }
+
+ $stmt = $pdo->prepare("\n INSERT INTO scad_functions\n (name, description, person_full_name, phone, email, notes, sort_order, status, created_at, updated_at)\n VALUES\n (:name, :description, :person_full_name, :phone, :email, :notes, :sort_order, :status, NOW(), NOW())\n ");
+ $stmt->execute([
+ 'name' => $name,
+ 'description' => $description,
+ 'person_full_name' => $personFullName,
+ 'phone' => $phone,
+ 'email' => $email,
+ 'notes' => $notes,
+ 'sort_order' => $sortOrder,
+ 'status' => $status,
+ ]);
+
+ scadJsonResponse(['success' => true, 'message' => 'Funzione creata correttamente.']);
+ }
+
+ if ($action === 'delete') {
+ $id = (int)($_POST['id'] ?? 0);
+
+ if ($id <= 0) {
+ scadJsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
+ }
+
+ $stmtUse = $pdo->prepare('SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?');
+ $stmtUse->execute([$id]);
+ $inUse = (int)$stmtUse->fetchColumn();
+
+ if ($inUse > 0) {
+ scadJsonResponse([
+ 'success' => false,
+ 'message' => 'Impossibile eliminare: la funzione è utilizzata in ' . $inUse . ' scadenza/e.'
+ ]);
+ }
+
+ $stmt = $pdo->prepare('DELETE FROM scad_functions WHERE id = :id');
+ $stmt->execute(['id' => $id]);
+
+ scadJsonResponse(['success' => true, 'message' => 'Funzione eliminata correttamente.']);
+ }
+
+ scadJsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
+ } catch (Exception $e) {
+ scadJsonResponse(['success' => false, 'message' => $e->getMessage()]);
+ }
+}
+
+$functions = $pdo->query("\n SELECT f.*,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count\n FROM scad_functions f\n ORDER BY COALESCE(f.sort_order, 0) ASC, f.name ASC\n")->fetchAll(PDO::FETCH_ASSOC);
?>
@@ -24,11 +131,13 @@ $functions = $pdo->query("
+
Scadenzario - Funzioni
@@ -39,6 +148,7 @@ $functions = $pdo->query("
--scad-heading: #2c3e6b;
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
--scad-card-border: #dde4f0;
+ --scad-muted: #6c757d;
}
.scad-card {
@@ -127,6 +237,63 @@ $functions = $pdo->query("
color: #fff;
}
+ .function-table th {
+ font-size: 0.82rem;
+ color: var(--scad-heading);
+ background: #f8fafc;
+ border-bottom: 1px solid var(--scad-card-border);
+ white-space: nowrap;
+ }
+
+ .function-table td {
+ font-size: 0.9rem;
+ vertical-align: middle;
+ }
+
+ .function-name {
+ font-weight: 700;
+ color: var(--scad-heading);
+ }
+
+ .function-description,
+ .function-notes {
+ color: var(--scad-muted);
+ font-size: 0.82rem;
+ margin-top: 2px;
+ max-width: 280px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .contact-line {
+ font-size: 0.85rem;
+ line-height: 1.45;
+ }
+
+ .contact-line a {
+ text-decoration: none;
+ }
+
+ .badge-function-status {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 999px;
+ padding: 4px 10px;
+ font-size: 0.78rem;
+ font-weight: 700;
+ }
+
+ .badge-function-status.active {
+ background: #d1fae5;
+ color: #065f46;
+ }
+
+ .badge-function-status.inactive {
+ background: #e5e7eb;
+ color: #374151;
+ }
+
.function-card {
background: #fff;
border: 1px solid var(--scad-card-border);
@@ -141,12 +308,19 @@ $functions = $pdo->query("
font-size: 0.95rem;
}
+ .function-card .fc-meta {
+ font-size: 0.82rem;
+ color: var(--scad-muted);
+ margin-top: 0.35rem;
+ }
+
.function-card .fc-stats {
display: flex;
gap: 0.75rem;
font-size: 0.8rem;
- color: #6c757d;
+ color: var(--scad-muted);
margin: 0.5rem 0;
+ flex-wrap: wrap;
}
.function-card .fc-stats strong {
@@ -156,7 +330,7 @@ $functions = $pdo->query("
.empty-state {
text-align: center;
padding: 3rem 1rem;
- color: #6c757d;
+ color: var(--scad-muted);
}
.empty-state i {
@@ -165,6 +339,15 @@ $functions = $pdo->query("
margin-bottom: 1rem;
}
+ .modal-content {
+ border-radius: 0.75rem;
+ }
+
+ .modal-header {
+ background: var(--scad-card-bg);
+ border-bottom: 1px solid var(--scad-card-border);
+ }
+
@media (max-width: 575.98px) {
.scad-card .card-header {
flex-direction: column;
@@ -223,25 +406,48 @@ $functions = $pdo->query("
-
+
-
= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
+
+
= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
+
= $statusLabel ?>
+
-
= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>
+
= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
Referente: = htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+ 📞 = htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?>
+ = !empty($f['phone']) ? '
' : '' ?>✉️ = htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?>
+
Scadenze: = (int)$f['deadline_count'] ?>
Aperte: = (int)$f['open_count'] ?>
+ Ordine: = (int)($f['sort_order'] ?? 0) ?>
@@ -258,30 +464,63 @@ $functions = $pdo->query("
-
+
- | Nome |
- Descrizione |
- Scadenze |
- Aperte |
- Azioni |
+ Ord. |
+ Funzione |
+ Referente |
+ Contatti |
+ Stato |
+ Scadenze |
+ Aperte |
+ Azioni |
+
- |
- = htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
+ | = (int)($f['sort_order'] ?? 0) ?> |
+
+ = htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
+
+
+ = htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+ Note: = htmlspecialchars($f['notes'], ENT_QUOTES, 'UTF-8') ?>
+
+
|
-
- = htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
+ | = !empty($f['person_full_name']) ? htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') : '—' ?> |
+
+
+
+
+
+
+
+
+ —
+
|
+ = $statusLabel ?> |
= (int)$f['deadline_count'] ?> |
= (int)$f['open_count'] ?> |
@@ -300,9 +539,7 @@ $functions = $pdo->query("
|
-
-
@@ -314,7 +551,7 @@ $functions = $pdo->query("