Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc3af01f3 | |||
| 3d54140280 | |||
| bfdbbbfc8f | |||
| 40a5771a4b | |||
| 9f5a585717 | |||
| 9ec5419a86 | |||
| c05091e020 | |||
| 0b470f290e | |||
| e74870c8d3 | |||
| 9001eff317 | |||
| 7cbd74111d | |||
| 650676037a | |||
| 2fc34c3cf4 | |||
| 955a7ed9e9 | |||
| cb221a8039 |
+4
-2
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
MANAGER_USER_ID=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
@@ -55,5 +57,5 @@ AZURE_REDIRECT_URI=https://your-app.com/auth/azure/callback
|
||||
AZURE_TENANT_ID=
|
||||
|
||||
MICROSOFT_CLIENT_ID=your_client_id_here
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
MICROSOFT_CLIENT_SECRET=your_client_secret_here
|
||||
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
|
||||
|
||||
@@ -111,6 +111,14 @@ class LoginController extends Controller
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('User')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('HR')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('SuperUser')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Management')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
} elseif ($user->hasRole('Quality')) {
|
||||
return redirect()->to('userarea/production_dashboard.php');
|
||||
}
|
||||
|
||||
// Se il ruolo non è specificato, reindirizza alla home predefinita
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddFunctionsToScadDeadlines extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('scad_functions', [
|
||||
'id' => false,
|
||||
'primary_key' => ['id'],
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'encoding' => 'utf8mb4',
|
||||
])
|
||||
->addColumn('id', 'integer', [
|
||||
'identity' => true,
|
||||
'signed' => false,
|
||||
])
|
||||
->addColumn('name', 'string', [
|
||||
'limit' => 255,
|
||||
'null' => false,
|
||||
])
|
||||
->addColumn('description', 'text', [
|
||||
'null' => true,
|
||||
])
|
||||
->addColumn('status', 'string', [
|
||||
'limit' => 20,
|
||||
'null' => false,
|
||||
'default' => 'active',
|
||||
])
|
||||
->addColumn('created_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addColumn('updated_at', 'timestamp', [
|
||||
'null' => false,
|
||||
'default' => 'CURRENT_TIMESTAMP',
|
||||
'update' => 'CURRENT_TIMESTAMP',
|
||||
])
|
||||
->addIndex(['name'], [
|
||||
'unique' => true,
|
||||
'name' => 'uniq_scad_functions_name',
|
||||
])
|
||||
->create();
|
||||
|
||||
$this->table('scad_deadlines')
|
||||
->addColumn('function_id', 'integer', [
|
||||
'signed' => false,
|
||||
'null' => true,
|
||||
'after' => 'subject_id',
|
||||
])
|
||||
->addIndex(['function_id'], [
|
||||
'name' => 'idx_scad_deadlines_function_id',
|
||||
])
|
||||
->addForeignKey('function_id', 'scad_functions', 'id', [
|
||||
'delete' => 'SET_NULL',
|
||||
'update' => 'CASCADE',
|
||||
'constraint' => 'fk_scad_deadlines_function',
|
||||
])
|
||||
->update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* Bulk "renew": set a common completed_date on the selected training records
|
||||
*/
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// $pdo and $currentUserId from hr_auth_check.php
|
||||
|
||||
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||
$ids = $_POST['training_ids'] ?? [];
|
||||
|
||||
if (!is_array($ids)) {
|
||||
$ids = [];
|
||||
}
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
|
||||
|
||||
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Load each record with its topic default frequency
|
||||
$rowStmt = $pdo->prepare("
|
||||
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||
et.update_frequency_months, tt.default_frequency_months
|
||||
FROM employee_trainings et
|
||||
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||
WHERE et.id = :id
|
||||
");
|
||||
$upd = $pdo->prepare("
|
||||
UPDATE employee_trainings
|
||||
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||
");
|
||||
|
||||
$updated = 0;
|
||||
foreach ($ids as $id) {
|
||||
$rowStmt->execute(['id' => $id]);
|
||||
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Effective frequency: per-record override, else topic default
|
||||
$effFreq = $row['update_frequency_months'] !== null
|
||||
? (int)$row['update_frequency_months']
|
||||
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
|
||||
|
||||
$nextDue = null;
|
||||
if ($effFreq !== null && $effFreq > 0) {
|
||||
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||
if ($d) {
|
||||
$d->modify('+' . $effFreq . ' months');
|
||||
$nextDue = $d->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
|
||||
|
||||
if ((string)$row['completed_date'] !== (string)$completedDate) {
|
||||
$logStmt->execute([
|
||||
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
|
||||
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
|
||||
]);
|
||||
}
|
||||
if ((string)$row['next_due_date'] !== (string)$nextDue) {
|
||||
$logStmt->execute([
|
||||
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
|
||||
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
|
||||
]);
|
||||
}
|
||||
$updated++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'updated' => $updated,
|
||||
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Calendar events for the training calendar (training_calendar.php).
|
||||
* Returns FullCalendar event objects for the *current* training record per
|
||||
* (employee, topic) that has a next_due_date, colored by computed status.
|
||||
* HR-only.
|
||||
*/
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// $pdo and $currentUserId provided by hr_auth_check.php
|
||||
|
||||
$start = $_GET['start'] ?? null;
|
||||
$end = $_GET['end'] ?? null;
|
||||
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
|
||||
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
|
||||
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
|
||||
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
// Deadlines only (one-time trainings have no next_due_date)
|
||||
$where[] = "et.next_due_date IS NOT NULL";
|
||||
|
||||
// Only the most recent record per (employee, topic)
|
||||
$where[] = "NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et2
|
||||
WHERE et2.employee_id = et.employee_id
|
||||
AND et2.training_topic_id = et.training_topic_id
|
||||
AND (et2.completed_date > et.completed_date
|
||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||
)";
|
||||
|
||||
if ($start && $end) {
|
||||
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
|
||||
$params['start'] = $start;
|
||||
$params['end'] = $end;
|
||||
}
|
||||
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
|
||||
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
|
||||
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
|
||||
|
||||
$whereSql = 'WHERE ' . implode(' AND ', $where);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
|
||||
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
||||
e.first_name, e.last_name
|
||||
FROM employee_trainings et
|
||||
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||
JOIN employees e ON e.id = et.employee_id
|
||||
$whereSql
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$today = new DateTime('today');
|
||||
$events = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$rem = $r['reminder_days'] !== null
|
||||
? (int)$r['reminder_days']
|
||||
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
||||
|
||||
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
|
||||
if (!$due) continue;
|
||||
$daysLeft = (int)$today->diff($due)->format('%r%a');
|
||||
|
||||
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
|
||||
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
|
||||
else { $code = 'compliant'; $color = '#198754'; }
|
||||
|
||||
if ($fStatus !== '' && $fStatus !== $code) continue;
|
||||
|
||||
$name = trim($r['first_name'] . ' ' . $r['last_name']);
|
||||
$events[] = [
|
||||
'id' => (int)$r['id'],
|
||||
'title' => $name . ' — ' . $r['topic_name'],
|
||||
'start' => $r['next_due_date'],
|
||||
'allDay' => true,
|
||||
'backgroundColor' => $color,
|
||||
'borderColor' => $color,
|
||||
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/**
|
||||
* Bulk-create training records: one employee_trainings row per selected employee,
|
||||
* all sharing the same course + parameters (a single training "session").
|
||||
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
|
||||
* HR-only.
|
||||
*/
|
||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// $pdo and $currentUserId from hr_auth_check.php
|
||||
|
||||
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||
$remRaw = $_POST['reminder_days'] ?? '';
|
||||
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||
|
||||
if (!is_array($employeeIds)) {
|
||||
$employeeIds = [];
|
||||
}
|
||||
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||
|
||||
if ($topicId <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||
exit;
|
||||
}
|
||||
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||
exit;
|
||||
}
|
||||
if (empty($employeeIds)) {
|
||||
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||
exit;
|
||||
}
|
||||
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||
$trainingType = 'initial';
|
||||
}
|
||||
|
||||
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||
$topicStmt->execute(['id' => $topicId]);
|
||||
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$topic) {
|
||||
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||
|
||||
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
|
||||
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||
$nextDue = null;
|
||||
if ($effFreq !== null && $effFreq > 0) {
|
||||
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||
if ($d) {
|
||||
$d->modify('+' . (int)$effFreq . ' months');
|
||||
$nextDue = $d->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||
$description = $description !== '' ? $description : null;
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Only insert for employees that actually exist
|
||||
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO employee_trainings
|
||||
(employee_id, training_topic_id, completed_date,
|
||||
delivered_by, description,
|
||||
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||
created_by, created_at, updated_at)
|
||||
VALUES
|
||||
(:eid, :tid, :completed_date,
|
||||
:delivered_by, :description,
|
||||
:training_type, :freq, :rem, :next_due,
|
||||
:cb, NOW(), NOW())
|
||||
");
|
||||
$logStmt = $pdo->prepare("
|
||||
INSERT INTO employee_training_log
|
||||
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||
VALUES
|
||||
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||
");
|
||||
|
||||
$created = 0;
|
||||
foreach ($employeeIds as $eid) {
|
||||
$checkEmp->execute(['id' => $eid]);
|
||||
if (!$checkEmp->fetchColumn()) {
|
||||
continue;
|
||||
}
|
||||
$ins->execute([
|
||||
'eid' => $eid,
|
||||
'tid' => $topicId,
|
||||
'completed_date' => $completedDate,
|
||||
'delivered_by' => $deliveredBy,
|
||||
'description' => $description,
|
||||
'training_type' => $trainingType,
|
||||
'freq' => $freq,
|
||||
'rem' => $rem,
|
||||
'next_due' => $nextDue,
|
||||
'cb' => $currentUserId,
|
||||
]);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'created' => $created,
|
||||
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -47,7 +47,8 @@ $sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
|
||||
/* Candidate trainings (with optional override reminder + topic default) */
|
||||
/* Candidate trainings (with optional override reminder + topic default).
|
||||
Only the most recent record per (employee, topic) — older history rows skipped. */
|
||||
$stmt = $pdo->query("
|
||||
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||
et.reminder_days, et.delivered_by,
|
||||
@@ -60,6 +61,13 @@ $stmt = $pdo->query("
|
||||
JOIN employees e ON e.id = et.employee_id
|
||||
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
||||
WHERE et.next_due_date IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et2
|
||||
WHERE et2.employee_id = et.employee_id
|
||||
AND et2.training_topic_id = et.training_topic_id
|
||||
AND (et2.completed_date > et.completed_date
|
||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||
)
|
||||
");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
+1281
-716
File diff suppressed because it is too large
Load Diff
@@ -321,6 +321,11 @@
|
||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="training_calendar.php">
|
||||
<i class='bx bx-radio-circle'></i>Calendario Formazione
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (userCan('hr.skills.view')) : ?>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex align-items-center" href="../users">
|
||||
<a class="dropdown-item d-flex align-items-center" href="user_settings.php">
|
||||
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -117,4 +117,4 @@
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
@@ -19,6 +19,7 @@ if (!$__trWidgetHr) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Only the most recent record per (employee, topic) — older history rows ignored. */
|
||||
$__trRows = $pdo->query("
|
||||
SELECT et.id,
|
||||
et.next_due_date,
|
||||
@@ -27,6 +28,13 @@ $__trRows = $pdo->query("
|
||||
FROM employee_trainings et
|
||||
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||
WHERE et.next_due_date IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et2
|
||||
WHERE et2.employee_id = et.employee_id
|
||||
AND et2.training_topic_id = et.training_topic_id
|
||||
AND (et2.completed_date > et.completed_date
|
||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||
)
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$__expiredCount = 0;
|
||||
|
||||
@@ -51,7 +51,7 @@ $dashboardSections = [
|
||||
'permission' => 'warehouse.dashboard.view',
|
||||
],
|
||||
[
|
||||
'label' => 'Scadenziario',
|
||||
'label' => 'Smart-Alert',
|
||||
'icon' => '⏰',
|
||||
'class' => 'btn-scadenziario',
|
||||
'url' => 'scadenzario/index.php',
|
||||
@@ -187,7 +187,7 @@ $dashboardSections = [
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Dashboard Produzione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
<title>Dashboard <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<!-- Bootstrap + jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
@@ -539,7 +539,7 @@ $dashboardSections = [
|
||||
<?php include(__DIR__ . '/include/training_widget.php'); ?>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-title">Dashboard Produzione</h3>
|
||||
<h3 class="dashboard-title">Dashboard</h3>
|
||||
|
||||
<!-- ===== STATISTICHE PRINCIPALI ===== -->
|
||||
<div class="stats-row">
|
||||
|
||||
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
$rawId = $_POST['id'] ?? $_GET['id'] ?? null;
|
||||
if ($rawId === null || !is_numeric($rawId)) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int)$_GET['id'];
|
||||
$id = (int)$rawId;
|
||||
|
||||
// Whether to create the next (recurring) deadline. Absent or '1' => create; '0' => complete only.
|
||||
$createNext = ($_POST['create_next'] ?? '1') !== '0';
|
||||
|
||||
// Whether to carry the attachment links over to the new deadline. Default ON ("default all activate").
|
||||
$copyAttachments = ($_POST['copy_attachments'] ?? '1') !== '0';
|
||||
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
@@ -34,11 +41,13 @@ try {
|
||||
->execute([$id, $currentUserId]);
|
||||
|
||||
$newId = null;
|
||||
$newDueDate = null;
|
||||
|
||||
// If recurring, create next deadline
|
||||
if ($deadline['recurrence_type'] !== 'once') {
|
||||
// If recurring AND the user asked for it, create the next deadline
|
||||
if ($deadline['recurrence_type'] !== 'once' && $createNext) {
|
||||
$dueDate = new DateTime($deadline['due_date']);
|
||||
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
||||
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
||||
|
||||
switch ($deadline['recurrence_type']) {
|
||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
||||
@@ -57,23 +66,25 @@ try {
|
||||
if ($interval) {
|
||||
$dueDate->add($interval);
|
||||
if ($checkDate) $checkDate->add($interval);
|
||||
if ($documentDate) $documentDate->add($interval);
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$ins->execute([
|
||||
$deadline['subject_id'], $deadline['topic'], $deadline['law_regulation'],
|
||||
$deadline['subject_id'], $deadline['function_id'], $deadline['topic'], $deadline['law_regulation'],
|
||||
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||
$deadline['document_date'],
|
||||
$documentDate ? $documentDate->format('Y-m-d') : null,
|
||||
$deadline['notification_days'], $deadline['storage_location'],
|
||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
$newDueDate = $dueDate;
|
||||
|
||||
// Copy employee assignments
|
||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
||||
@@ -87,6 +98,31 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Carry forward ALL attachment links from the source deadline (shared physical file, same stored_name).
|
||||
// Individual links can later be removed on the new deadline without deleting the file.
|
||||
if ($copyAttachments) {
|
||||
$attSel = $pdo->prepare("
|
||||
SELECT original_name, stored_name, mime_type, size
|
||||
FROM scad_deadline_attachments
|
||||
WHERE deadline_id = ?
|
||||
");
|
||||
$attSel->execute([$id]);
|
||||
$attRows = $attSel->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($attRows) {
|
||||
$attIns = $pdo->prepare("
|
||||
INSERT INTO scad_deadline_attachments
|
||||
(deadline_id, original_name, stored_name, mime_type, size, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$attHist = $pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_linked', ?)");
|
||||
foreach ($attRows as $a) {
|
||||
$attIns->execute([$newId, $a['original_name'], $a['stored_name'], $a['mime_type'], $a['size'], $currentUserId]);
|
||||
$attHist->execute([$newId, $currentUserId, $a['original_name']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// History for new
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
||||
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
||||
@@ -97,7 +133,7 @@ try {
|
||||
|
||||
$msg = 'Scadenza completata con successo.';
|
||||
if ($newId) {
|
||||
$msg .= ' Nuova scadenza creata con data ' . $dueDate->format('d/m/Y') . '.';
|
||||
$msg .= ' Nuova scadenza creata con data ' . $newDueDate->format('d/m/Y') . '.';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||
|
||||
@@ -23,20 +23,32 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete file
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
// Delete DB record
|
||||
// Remove this link (DB record) first
|
||||
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)")
|
||||
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]);
|
||||
// The same physical file may be shared with other deadlines (carried forward on completion).
|
||||
// Only unlink it when no other link references the same stored file.
|
||||
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
|
||||
$refStmt->execute([$att['stored_name']]);
|
||||
$stillReferenced = (int)$refStmt->fetchColumn() > 0;
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']);
|
||||
if ($stillReferenced) {
|
||||
$action = 'attachment_unlinked';
|
||||
$message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
|
||||
} else {
|
||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
$action = 'attachment_removed';
|
||||
$message = 'Allegato eliminato.';
|
||||
}
|
||||
|
||||
// History
|
||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
|
||||
->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $message]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
|
||||
@@ -13,10 +13,29 @@ try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
// Collect the physical files referenced by this deadline before the FK cascade removes its links
|
||||
$attStmt = $pdo->prepare("SELECT DISTINCT stored_name FROM scad_deadline_attachments WHERE deadline_id = ?");
|
||||
$attStmt->execute([$id]);
|
||||
$storedNames = $attStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Deleting the deadline cascades to its attachment/employee/history rows (FK ON DELETE CASCADE)
|
||||
$stmt = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Unlink physical files no longer referenced by any other deadline (shared-file safe)
|
||||
if (!empty($storedNames)) {
|
||||
$refStmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadline_attachments WHERE stored_name = ?");
|
||||
foreach ($storedNames as $storedName) {
|
||||
$refStmt->execute([$storedName]);
|
||||
if ((int)$refStmt->fetchColumn() === 0) {
|
||||
$filePath = __DIR__ . '/../attachments/' . $storedName;
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
||||
|
||||
@@ -9,6 +9,7 @@ try {
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
||||
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
||||
$topic = trim($_POST['topic'] ?? '');
|
||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||
@@ -51,16 +52,26 @@ try {
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_deadlines SET
|
||||
subject_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||
UPDATE scad_deadlines SET
|
||||
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||
storage_location = ?, notes = ?, departments = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id, $topic, $law_regulation, $recurrence_type,
|
||||
$due_date, $check_date, $document_date, $notification_days,
|
||||
$storage_location, $notes, $departmentsStr, $id
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$departmentsStr,
|
||||
$id
|
||||
]);
|
||||
|
||||
// Re-link employees
|
||||
@@ -75,14 +86,24 @@ try {
|
||||
// INSERT
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_deadlines
|
||||
(subject_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$subject_id, $topic, $law_regulation, $recurrence_type,
|
||||
$due_date, $check_date, $document_date, $notification_days,
|
||||
$storage_location, $notes, $currentUserId, $departmentsStr
|
||||
$subject_id,
|
||||
$function_id,
|
||||
$topic,
|
||||
$law_regulation,
|
||||
$recurrence_type,
|
||||
$due_date,
|
||||
$check_date,
|
||||
$document_date,
|
||||
$notification_days,
|
||||
$storage_location,
|
||||
$notes,
|
||||
$currentUserId,
|
||||
$departmentsStr
|
||||
]);
|
||||
|
||||
$deadlineId = $pdo->lastInsertId();
|
||||
@@ -107,7 +128,6 @@ try {
|
||||
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
||||
'id' => $deadlineId
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
|
||||
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
|
||||
$today = date('Y-m-d');
|
||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||
|
||||
// Manager email for Cc — taken from MANAGER_USER_ID → auth_users.email
|
||||
$managerCcEmail = null;
|
||||
if (!empty($_ENV['MANAGER_USER_ID']) && is_numeric($_ENV['MANAGER_USER_ID'])) {
|
||||
$mgrStmt = $pdo->prepare("SELECT email FROM auth_users WHERE id = ?");
|
||||
$mgrStmt->execute([(int)$_ENV['MANAGER_USER_ID']]);
|
||||
$mgrEmail = $mgrStmt->fetchColumn();
|
||||
if (!empty($mgrEmail)) {
|
||||
$managerCcEmail = $mgrEmail;
|
||||
}
|
||||
}
|
||||
|
||||
$sent = 0;
|
||||
$skipped = 0;
|
||||
$errors = 0;
|
||||
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
|
||||
);
|
||||
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
|
||||
|
||||
// Cc the manager (unless they are the direct recipient)
|
||||
if ($managerCcEmail && strcasecmp($managerCcEmail, $emp['email']) !== 0) {
|
||||
$mail->addCC($managerCcEmail);
|
||||
}
|
||||
|
||||
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
}
|
||||
|
||||
$recurrenceLabels = ['once' => 'Una tantum', 'monthly' => 'Mensile', 'quarterly' => 'Trimestrale', 'semiannual' => 'Semestrale', 'annual' => 'Annuale', 'biennial' => 'Biennale', 'triennial' => 'Triennale', 'quadriennial' => 'Quadriennale', 'quinquennial' => 'Quinquennale', 'decennial' => 'Decennale', 'quindecennial' => 'Quindicennale'];
|
||||
$actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'notification_sent' => 'Notifica inviata'];
|
||||
$actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'notification_sent' => '#adb5bd'];
|
||||
$actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'notification_sent' => 'fa-bell'];
|
||||
$actionLabels = ['created' => 'Creata', 'updated' => 'Modificata', 'completed' => 'Completata', 'attachment_added' => 'Allegato aggiunto', 'attachment_removed' => 'Allegato rimosso', 'attachment_linked' => 'Allegato collegato', 'attachment_unlinked' => 'Collegamento rimosso', 'notification_sent' => 'Notifica inviata'];
|
||||
$actionColors = ['created' => '#198754', 'updated' => '#5a8fd8', 'completed' => '#6f42c1', 'attachment_added' => '#e8930c', 'attachment_removed' => '#e8930c', 'attachment_linked' => '#0dcaf0', 'attachment_unlinked' => '#adb5bd', 'notification_sent' => '#adb5bd'];
|
||||
$actionIcons = ['created' => 'fa-plus', 'updated' => 'fa-pen', 'completed' => 'fa-check', 'attachment_added' => 'fa-paperclip', 'attachment_removed' => 'fa-trash', 'attachment_linked' => 'fa-link', 'attachment_unlinked' => 'fa-link-slash', 'notification_sent' => 'fa-bell'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -85,6 +85,14 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/i18n/it.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/it.js"></script>
|
||||
<?php include __DIR__ . '/include/deadline_modal_css.php'; ?>
|
||||
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
|
||||
<script>
|
||||
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -755,52 +763,114 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
</div>
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php if ($deadline && !$isCompleted): ?>
|
||||
<?php require __DIR__ . '/include/deadline_form_data.php'; ?>
|
||||
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<?php if ($deadline && !$isCompleted): ?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Used by the shared modal JS to auto-open edit on "#edit"
|
||||
window.SCAD_DETAIL_ID = <?= (int)$deadline['id'] ?>;
|
||||
|
||||
$('#btnModifica').on('click', function() {
|
||||
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>';
|
||||
window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
|
||||
});
|
||||
|
||||
function detailSubmitComplete(createNext, copyAttachments) {
|
||||
var fd = new FormData();
|
||||
fd.append('id', '<?= (int)$deadline['id'] ?>');
|
||||
fd.append('create_next', createNext ? '1' : '0');
|
||||
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||
|
||||
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
if (data.new_id) {
|
||||
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
|
||||
} else {
|
||||
window.location.href = 'scadenzario/index.php';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$('#btnCompleta').on('click', function() {
|
||||
var recurrence = <?= json_encode($deadline['recurrence_type'] ?? 'once') ?>;
|
||||
var attCount = <?= count($attachments) ?>;
|
||||
|
||||
if (recurrence === 'once') {
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
text: 'La scadenza verrà contrassegnata come completata.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
detailSubmitComplete(false, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var attCheckbox = attCount > 0 ?
|
||||
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
|
||||
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
|
||||
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
|
||||
'</div>' :
|
||||
'';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
showDenyButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
denyButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Completa e crea la prossima',
|
||||
denyButtonText: 'Completa senza nuova',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa'
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>')
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 2500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
window.location.href = 'scadenzario/index.php';
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
|
||||
detailSubmitComplete(true, copy);
|
||||
} else if (result.isDenied) {
|
||||
detailSubmitComplete(false, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$inUse = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($inUse > 0) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Impossibile eliminare: la funzione è utilizzata in $inUse scadenz" . ($inUse === 1 ? 'a' : 'e') . '.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->prepare("DELETE FROM scad_functions WHERE id = ?")->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => 'Funzione eliminata.']);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../ajax/auth_check.php');
|
||||
header('Content-Type: application/json');
|
||||
require_once(__DIR__ . '/../../../class/db-functions.php');
|
||||
|
||||
try {
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '') ?: null;
|
||||
|
||||
if ($name === '') {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome è obbligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (mb_strlen($name) > 255) {
|
||||
echo json_encode(['success' => false, 'message' => 'Il nome supera 255 caratteri.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ? AND id <> ?");
|
||||
$stmt->execute([$name, $id]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT id FROM scad_functions WHERE name = ?");
|
||||
$stmt->execute([$name]);
|
||||
}
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Esiste già una funzione con questo nome.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE scad_functions
|
||||
SET name = ?, description = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$name, $description, $id]);
|
||||
$savedId = $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO scad_functions (name, description, status)
|
||||
VALUES (?, ?, 'active')
|
||||
");
|
||||
$stmt->execute([$name, $description]);
|
||||
$savedId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $id ? 'Funzione aggiornata.' : 'Funzione creata.',
|
||||
'id' => $savedId,
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
<?php include('../../include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->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);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php
|
||||
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$baseHref = dirname(dirname($scriptDir)) . '/';
|
||||
?>
|
||||
<base href="<?= $baseHref ?>">
|
||||
<?php include('../../cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<title>Scadenzario - Funzioni</title>
|
||||
<script>
|
||||
if (window.innerWidth > 1024) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('appWrapper').classList.add('toggled');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--scad-primary: #5a8fd8;
|
||||
--scad-primary-hover: #4578c0;
|
||||
--scad-heading: #2c3e6b;
|
||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
--scad-card-border: #dde4f0;
|
||||
}
|
||||
|
||||
.scad-card {
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scad-card .card-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.scad-card .card-header h5 {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.scad-card .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-scad-primary {
|
||||
background: var(--scad-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-scad-primary:hover {
|
||||
background: var(--scad-primary-hover);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-scad-outline {
|
||||
background: transparent;
|
||||
border: 1.5px solid var(--scad-primary);
|
||||
color: var(--scad-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-scad-outline:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-action-edit {
|
||||
background: rgba(90, 143, 216, 0.12);
|
||||
color: var(--scad-primary);
|
||||
}
|
||||
|
||||
.btn-action-edit:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-action-delete {
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-action-delete:hover {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.function-card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--scad-card-border);
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.function-card .fc-name {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.function-card .fc-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.function-card .fc-stats strong {
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.scad-card .card-header {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('../../include/navbar.php'); ?>
|
||||
<?php include('../../include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb" style="background:transparent;padding:0;margin:0;font-size:0.85rem">
|
||||
<li class="breadcrumb-item"><a href="scadenzario/index.php">Scadenzario</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Funzioni</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card scad-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h5><i class="fa-solid fa-briefcase me-2"></i>Funzioni</h5>
|
||||
|
||||
<div class="header-actions d-flex gap-2 flex-wrap">
|
||||
<a href="scadenzario/index.php" class="btn btn-scad-outline d-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-arrow-left"></i><span>Scadenzario</span>
|
||||
</a>
|
||||
|
||||
<button class="btn btn-scad-primary d-inline-flex align-items-center gap-2" id="btnAddFunction">
|
||||
<i class="fa-solid fa-plus"></i><span>Nuova Funzione</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if (count($functions) === 0): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-briefcase"></i>
|
||||
<p>Nessuna funzione definita.<br>Clicca <strong>"Nuova Funzione"</strong> per aggiungere la prima.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div id="functionsList">
|
||||
|
||||
<div class="d-md-none">
|
||||
<?php foreach ($functions as $f): ?>
|
||||
<div class="function-card"
|
||||
data-id="<?= (int)$f['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||
|
||||
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
|
||||
<?php if (!empty($f['description'])): ?>
|
||||
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fc-stats">
|
||||
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
||||
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1 justify-content-end">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-block">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Descrizione</th>
|
||||
<th class="text-center" style="width:120px">Scadenze</th>
|
||||
<th class="text-center" style="width:120px">Aperte</th>
|
||||
<th class="text-center" style="width:120px">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($functions as $f): ?>
|
||||
<tr data-id="<?= (int)$f['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||
|
||||
<td class="fw-semibold" style="color:var(--scad-heading)">
|
||||
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
|
||||
</td>
|
||||
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
||||
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
||||
<td class="text-center">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button class="btn-action btn-action-edit btn-edit" title="Modifica">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-action btn-action-delete btn-delete" title="Elimina">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
|
||||
<form id="functionForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="functionId" name="id" value="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
||||
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('../../jsinclude.php'); ?>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
function openModal(data) {
|
||||
const isEdit = !!data;
|
||||
|
||||
$('#functionModalTitle').text(isEdit ? 'Modifica Funzione' : 'Nuova Funzione');
|
||||
$('#functionId').val(isEdit ? data.id : '');
|
||||
$('#functionName').val(isEdit ? data.name : '');
|
||||
$('#functionDescription').val(isEdit ? data.description : '');
|
||||
|
||||
new bootstrap.Modal('#functionModal').show();
|
||||
}
|
||||
|
||||
$('#btnAddFunction').on('click', function() {
|
||||
openModal(null);
|
||||
});
|
||||
|
||||
$('#functionsList').on('click', '.btn-edit', function() {
|
||||
const $row = $(this).closest('[data-id]');
|
||||
|
||||
openModal({
|
||||
id: $row.data('id'),
|
||||
name: $row.data('name'),
|
||||
description: $row.data('description')
|
||||
});
|
||||
});
|
||||
|
||||
$('#functionsList').on('click', '.btn-delete', function() {
|
||||
const $row = $(this).closest('[data-id]');
|
||||
const inUse = parseInt($row.data('in-use') || 0, 10);
|
||||
const name = $row.data('name');
|
||||
|
||||
if (inUse > 0) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Impossibile eliminare',
|
||||
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: `Eliminare "${name}"?`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Elimina',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then(function(result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.post('scadenzario/functions/ajax/delete_function.php', {
|
||||
id: $row.data('id')
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore',
|
||||
text: res.message
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore di rete'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#functionForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const payload = {
|
||||
id: $('#functionId').val(),
|
||||
name: $('#functionName').val().trim(),
|
||||
description: $('#functionDescription').val().trim()
|
||||
};
|
||||
|
||||
if (!payload.name) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Nome obbligatorio'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('scadenzario/functions/ajax/save_function.php', payload)
|
||||
.done(function(res) {
|
||||
if (res.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore',
|
||||
text: res.message
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Errore di rete'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared data for the deadline modal form (used by index.php and detail.php).
|
||||
* Populates $employees, $departments, $subjects. Safe to include more than once.
|
||||
*/
|
||||
if (!isset($pdo) || !$pdo) {
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
if (!isset($employees)) {
|
||||
$employees = $pdo->query("
|
||||
SELECT e.id, e.first_name, e.last_name, e.department_id, dep.name AS department_name
|
||||
FROM employees e
|
||||
LEFT JOIN departments dep ON dep.id = e.department_id
|
||||
WHERE e.status = 'active'
|
||||
ORDER BY e.first_name, e.last_name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($departments)) {
|
||||
$departments = $pdo->query("
|
||||
SELECT id, name, code, color
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($subjects)) {
|
||||
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!isset($functions)) {
|
||||
$functions = $pdo->query("
|
||||
SELECT id, name
|
||||
FROM scad_functions
|
||||
WHERE status = 'active'
|
||||
ORDER BY name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared "Nuova/Modifica Scadenza" modal markup (used by index.php and detail.php).
|
||||
* Requires $subjects, $departments, $employees in scope (see deadline_form_data.php).
|
||||
* The accompanying JS lives in deadline_modal_js.php.
|
||||
*/
|
||||
?>
|
||||
<!-- Deadline Modal -->
|
||||
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="deadlineForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="dlId" name="id" value="">
|
||||
|
||||
<!-- Group 1: Informazioni principali -->
|
||||
<div class="form-section-title">Informazioni principali</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
|
||||
<option value="">— Nessuno —</option>
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlFunction" class="form-label fw-semibold">Funzione</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="dlFunction" name="function_id" style="flex:1">
|
||||
<option value="">— Nessuna —</option>
|
||||
<?php foreach ($functions as $fn): ?>
|
||||
<option value="<?= (int)$fn['id'] ?>">
|
||||
<?= htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<a href="scadenzario/functions/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci funzioni">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: Date e frequenza -->
|
||||
<div class="form-section-title">Date e frequenza</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
|
||||
<select class="form-select" id="dlRecurrence" name="recurrence_type">
|
||||
<option value="once">Una tantum</option>
|
||||
<option value="monthly">Mensile</option>
|
||||
<option value="quarterly">Trimestrale</option>
|
||||
<option value="semiannual">Semestrale</option>
|
||||
<option value="annual">Annuale</option>
|
||||
<option value="biennial">Biennale</option>
|
||||
<option value="triennial">Triennale</option>
|
||||
<option value="quadriennial">Quadriennale</option>
|
||||
<option value="quinquennial">Quinquennale</option>
|
||||
<option value="decennial">Decennale</option>
|
||||
<option value="quindecennial">Quindicennale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
|
||||
<input type="text" class="form-control js-date-it" id="dlDocDate" name="document_date" placeholder="gg/mm/aaaa" autocomplete="off">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control js-date-it" id="dlDueDate" name="due_date" placeholder="gg/mm/aaaa" autocomplete="off" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
|
||||
<input type="text" class="form-control js-date-it" id="dlCheckDate" name="check_date" placeholder="gg/mm/aaaa" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: Responsabili -->
|
||||
<div class="form-section-title">Responsabili</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Tutto il reparto sarà responsabile</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
|
||||
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= (int)$emp['id'] ?>">
|
||||
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 4: Dettagli aggiuntivi -->
|
||||
<div class="form-section-title">Dettagli aggiuntivi</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
|
||||
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
|
||||
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 5: Allegati -->
|
||||
<div class="form-section-title mt-4">Allegati</div>
|
||||
<div id="attachmentsList" class="mb-3"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
|
||||
<input type="file" class="form-control" id="dlFiles" multiple>
|
||||
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">
|
||||
<i class="fa-solid fa-check me-1"></i> Salva
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Shared styles for the deadline modal (deadline_modal.php).
|
||||
* Relies on the --scad-* CSS variables defined on each page's :root.
|
||||
*/
|
||||
?>
|
||||
<style>
|
||||
.form-section-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid #e8eeff;
|
||||
}
|
||||
|
||||
#deadlineModal.modal {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#deadlineModal .modal-content,
|
||||
#deadlineModal .modal-body,
|
||||
#deadlineModal .modal-footer {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
#deadlineModal .modal-header {
|
||||
background: var(--scad-card-bg);
|
||||
border-bottom: 1px solid var(--scad-card-border);
|
||||
}
|
||||
|
||||
#deadlineModal .modal-title {
|
||||
font-weight: 700;
|
||||
color: var(--scad-heading);
|
||||
}
|
||||
|
||||
/* Attachment list in modal */
|
||||
.att-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.4rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.att-item .att-name {
|
||||
color: var(--scad-heading);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.att-item .att-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.att-item .att-actions a,
|
||||
.att-item .att-actions button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.3rem;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.att-item .att-download {
|
||||
background: #eef3ff;
|
||||
color: var(--scad-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.att-item .att-download:hover {
|
||||
background: var(--scad-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.att-item .att-remove {
|
||||
background: #fff0f0;
|
||||
color: var(--scad-red, #dc3545);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.att-item .att-remove:hover {
|
||||
background: var(--scad-red, #dc3545);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Self-contained JS for the deadline modal (deadline_modal.php).
|
||||
* Requires jQuery, Bootstrap, flatpickr, select2 and SweetAlert2 to be loaded first.
|
||||
*
|
||||
* Exposes:
|
||||
* window.openDeadlineCreate() — open the modal empty (new deadline)
|
||||
* window.openDeadlineEdit(id) — fetch a deadline and open the modal in edit mode
|
||||
*
|
||||
* Auto-open on load:
|
||||
* #edit=<id> → opens edit for that id
|
||||
* #edit → opens edit for window.SCAD_DETAIL_ID (used by detail.php)
|
||||
*/
|
||||
?>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
// --- Flatpickr date fields (visible dd/mm/yyyy, submitted yyyy-mm-dd) ---
|
||||
var fpOptsDate = {
|
||||
dateFormat: 'Y-m-d',
|
||||
altInput: true,
|
||||
altFormat: 'd/m/Y',
|
||||
locale: 'it',
|
||||
allowInput: true
|
||||
};
|
||||
var fpDocDate = flatpickr('#dlDocDate', fpOptsDate);
|
||||
var fpDueDate = flatpickr('#dlDueDate', fpOptsDate);
|
||||
var fpCheckDate = flatpickr('#dlCheckDate', fpOptsDate);
|
||||
|
||||
// --- Select2 ---
|
||||
var s2Opts = {
|
||||
theme: 'bootstrap-5',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
};
|
||||
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
|
||||
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
|
||||
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
|
||||
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
|
||||
|
||||
// --- Auto-calc due_date from document_date + recurrence ---
|
||||
var RECURRENCE_OFFSETS = {
|
||||
monthly: { months: 1 },
|
||||
quarterly: { months: 3 },
|
||||
semiannual: { months: 6 },
|
||||
annual: { years: 1 },
|
||||
biennial: { years: 2 },
|
||||
triennial: { years: 3 },
|
||||
quadriennial: { years: 4 },
|
||||
quinquennial: { years: 5 },
|
||||
decennial: { years: 10 },
|
||||
quindecennial: { years: 15 }
|
||||
};
|
||||
|
||||
function computeDueDate() {
|
||||
var docVal = document.getElementById('dlDocDate').value;
|
||||
var recurrence = document.getElementById('dlRecurrence').value;
|
||||
var offset = RECURRENCE_OFFSETS[recurrence];
|
||||
if (!docVal || !offset) return;
|
||||
var d = new Date(docVal + 'T00:00:00');
|
||||
if (isNaN(d.getTime())) return;
|
||||
if (offset.months) d.setMonth(d.getMonth() + offset.months);
|
||||
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
|
||||
var iso = d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
fpDueDate.setDate(iso, true, 'Y-m-d');
|
||||
}
|
||||
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
|
||||
|
||||
// --- Modal instance ---
|
||||
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
|
||||
var form = document.getElementById('deadlineForm');
|
||||
|
||||
// --- Render attachments list ---
|
||||
function renderAttachments(attachments) {
|
||||
var container = document.getElementById('attachmentsList');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(function(a) {
|
||||
return '<div class="att-item" data-att-id="' + a.id + '">' +
|
||||
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
|
||||
'<span class="att-actions">' +
|
||||
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
|
||||
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
|
||||
'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// --- Open modal (create) ---
|
||||
window.openDeadlineCreate = function() {
|
||||
form.reset();
|
||||
document.getElementById('dlId').value = '';
|
||||
document.getElementById('dlNotifDays').value = '7';
|
||||
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
fpDocDate.clear();
|
||||
fpDueDate.clear();
|
||||
fpCheckDate.clear();
|
||||
$('#dlSubject').val('').trigger('change');
|
||||
$('#dlDepartments').val(null).trigger('change');
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
$('#dlFunction').val('').trigger('change');
|
||||
renderAttachments([]);
|
||||
modal.show();
|
||||
};
|
||||
|
||||
// --- Open modal (edit) ---
|
||||
window.openDeadlineEdit = function(id) {
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data.success) {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
return;
|
||||
}
|
||||
var d = data.data;
|
||||
document.getElementById('dlId').value = d.id;
|
||||
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
$('#dlFunction').val(d.function_id || '').trigger('change');
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
|
||||
fpDueDate.setDate(d.due_date || null, false, 'Y-m-d');
|
||||
fpCheckDate.setDate(d.check_date || null, false, 'Y-m-d');
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
if (Array.isArray(d.employee_ids)) {
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
} else {
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
}
|
||||
renderAttachments(d.attachments || []);
|
||||
modal.show();
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// --- Save ---
|
||||
var isSaving = false;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (isSaving) return;
|
||||
isSaving = true;
|
||||
var saveBtn = form.querySelector('[type="submit"]');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
|
||||
var formData = new FormData(form);
|
||||
|
||||
fetch('scadenzario/ajax/save_deadline.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
var deadlineId = data.id;
|
||||
var fileInput = document.getElementById('dlFiles');
|
||||
if (fileInput.files.length > 0) {
|
||||
var fileData = new FormData();
|
||||
fileData.append('deadline_id', deadlineId);
|
||||
for (var i = 0; i < fileInput.files.length; i++) {
|
||||
fileData.append('files[]', fileInput.files[i]);
|
||||
}
|
||||
return fetch('scadenzario/ajax/upload_attachment.php', {
|
||||
method: 'POST',
|
||||
body: fileData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(upData) {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message + ' ' + upData.message,
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Delete attachment ---
|
||||
$(document).on('click', '.att-remove', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = $(this);
|
||||
var attId = btn.data('att-id');
|
||||
Swal.fire({
|
||||
title: 'Rimuovere l\'allegato?',
|
||||
text: 'Il collegamento verrà rimosso da questa scadenza. Il file resta disponibile se è usato da altre scadenze.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Rimuovi',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
btn.closest('.att-item').remove();
|
||||
if ($('#attachmentsList .att-item').length === 0) {
|
||||
renderAttachments([]);
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Fatto',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Auto-open from hash (#edit=ID or #edit for the current detail page) ---
|
||||
var hash = window.location.hash;
|
||||
var hashMatch = hash.match(/^#edit=(\d+)$/);
|
||||
var autoEditId = hashMatch ? hashMatch[1] :
|
||||
(hash === '#edit' && window.SCAD_DETAIL_ID ? window.SCAD_DETAIL_ID : null);
|
||||
if (autoEditId) {
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
window.openDeadlineEdit(autoEditId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Renders two status banners for the current user:
|
||||
* - red -> overdue deadlines (scaduta)
|
||||
@@ -44,63 +45,90 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
||||
?>
|
||||
<style>
|
||||
.my-deadlines-widgets {
|
||||
display: flex; flex-wrap: wrap; gap: 0.75rem;
|
||||
margin-bottom: 1rem; width: 100%;
|
||||
}
|
||||
.my-deadlines-widgets:empty { display: none; }
|
||||
/* When two widget containers are nested inside an outer .my-deadlines-widgets
|
||||
(e.g. on the production dashboard), let their children flow into the outer flex. */
|
||||
.my-deadlines-widgets .my-deadlines-widgets {
|
||||
display: contents;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw {
|
||||
flex: 1 1 0; min-width: 0;
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.8rem 0.9rem; border-radius: 0.6rem;
|
||||
text-decoration: none; color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
flex: 1 1 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.6rem;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.my-deadlines-widgets .mdw:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); color: #fff; }
|
||||
.my-deadlines-widgets .mdw-red { background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%); }
|
||||
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
|
||||
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
|
||||
|
||||
.my-deadlines-widgets .mdw:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-red {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-orange {
|
||||
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-icon {
|
||||
width: 38px; height: 38px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.my-deadlines-widgets .mdw-body { flex: 1; line-height: 1.2; min-width: 0; }
|
||||
.my-deadlines-widgets .mdw-count { font-size: 1.5rem; font-weight: 700; }
|
||||
.my-deadlines-widgets .mdw-label { font-size: 0.78rem; opacity: 0.95;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
|
||||
@media (max-width: 991.98px) {
|
||||
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
|
||||
|
||||
.my-deadlines-widgets .mdw-body {
|
||||
flex: 1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
|
||||
|
||||
.my-deadlines-widgets .mdw-count {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.my-deadlines-widgets .mdw-arrow {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
<div class="my-deadlines-widgets">
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></span>
|
||||
<span class="mdw-label d-block">Scadenz<?= $_overdue === 1 ? 'a' : 'e' ?> scadut<?= $_overdue === 1 ? 'a' : 'e' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_overdue > 0): ?>
|
||||
<a class="mdw mdw-red" href="scadenzario/index.php?filter_my=1&filter_status=scaduta">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_overdue ?></span>
|
||||
<span class="mdw-label d-block">Task<?= $_overdue === 1 ? '' : 's' ?> scadut<?= $_overdue === 1 ? 'o' : 'i' ?> — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($_approaching > 0): ?>
|
||||
<a class="mdw mdw-orange" href="scadenzario/index.php?filter_my=1&filter_status=in-scadenza">
|
||||
<span class="mdw-icon"><i class="fa-solid fa-clock"></i></span>
|
||||
<span class="mdw-body">
|
||||
<span class="mdw-count"><?= $_approaching ?></span>
|
||||
<span class="mdw-label d-block">In scadenza a breve — <?= $_dept !== '' ? htmlspecialchars($_dept, ENT_QUOTES, 'UTF-8') : 'personali' ?></span>
|
||||
</span>
|
||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -37,12 +37,14 @@ $sql = "
|
||||
SELECT d.*,
|
||||
s.name AS subject_name,
|
||||
s.color AS subject_color,
|
||||
f.name AS function_name,
|
||||
GROUP_CONCAT(DISTINCT CONCAT(e.first_name, ' ', e.last_name) ORDER BY e.first_name SEPARATOR ', ') as responsabili,
|
||||
GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone,
|
||||
d.departments as reparti_assegnati,
|
||||
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
|
||||
FROM scad_deadlines d
|
||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||
LEFT JOIN scad_functions f ON f.id = d.function_id
|
||||
LEFT JOIN scad_deadline_employee de ON de.deadline_id = d.id
|
||||
LEFT JOIN employees e ON e.id = de.employee_id
|
||||
LEFT JOIN departments dep ON dep.id = e.department_id
|
||||
@@ -69,27 +71,7 @@ $stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$employees = $pdo->query("
|
||||
SELECT
|
||||
e.id,
|
||||
e.first_name,
|
||||
e.last_name,
|
||||
e.department_id,
|
||||
dep.name AS department_name
|
||||
FROM employees e
|
||||
LEFT JOIN departments dep ON dep.id = e.department_id
|
||||
WHERE e.status = 'active'
|
||||
ORDER BY e.first_name, e.last_name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$departments = $pdo->query("
|
||||
SELECT id, name, code, color
|
||||
FROM departments
|
||||
WHERE is_active = 1
|
||||
ORDER BY sort_order ASC, name ASC
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$subjects = $pdo->query("SELECT id, name, color FROM scad_subjects ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
require __DIR__ . '/include/deadline_form_data.php';
|
||||
|
||||
$today = date('Y-m-d');
|
||||
|
||||
@@ -494,7 +476,8 @@ function getContrastTextColor($hexColor)
|
||||
}
|
||||
|
||||
#deadlinesTable td:first-child {
|
||||
max-width: 150px;
|
||||
max-width: 110px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
/* Attachment list in modal */
|
||||
@@ -824,6 +807,9 @@ function getContrastTextColor($hexColor)
|
||||
<a href="scadenzario/subjects/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-tags"></i><span>Argomenti</span>
|
||||
</a>
|
||||
<a href="scadenzario/functions/index.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-briefcase"></i><span>Funzioni</span>
|
||||
</a>
|
||||
<a href="scadenzario/calendar.php" class="btn btn-scad-outline d-none d-md-inline-flex align-items-center gap-2">
|
||||
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
|
||||
</a>
|
||||
@@ -842,6 +828,7 @@ function getContrastTextColor($hexColor)
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/subjects/index.php"><i class="fa-solid fa-tags"></i> Argomenti</a></li>
|
||||
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/functions/index.php"><i class="fa-solid fa-briefcase"></i> Funzioni</a></li>
|
||||
<li><a class="dropdown-item d-flex align-items-center gap-2" href="scadenzario/calendar.php"><i class="fa-solid fa-calendar-days"></i> Calendario</a></li>
|
||||
<li><button type="button" class="dropdown-item d-flex align-items-center gap-2" id="btnStampaMobile"><i class="fa-solid fa-print"></i> Stampa</button></li>
|
||||
</ul>
|
||||
@@ -923,7 +910,9 @@ function getContrastTextColor($hexColor)
|
||||
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-att-count="<?= (int)$row['attachment_count'] ?>">
|
||||
<?php if (!empty($row['subject_name'])): ?>
|
||||
<div class="mb-1"><?php
|
||||
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
|
||||
@@ -972,11 +961,12 @@ function getContrastTextColor($hexColor)
|
||||
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Argomento</th>
|
||||
<th style="width:110px">Argomento</th>
|
||||
<th>Dettaglio</th>
|
||||
<th class="d-none d-lg-table-cell">Legge/Art.</th>
|
||||
<th>Scadenza</th>
|
||||
<th class="d-none d-lg-table-cell">Verifica</th>
|
||||
<th>Funzione</th>
|
||||
<th>Responsabili</th>
|
||||
<th>Stato</th>
|
||||
<th class="text-center" style="width:120px">Azioni</th>
|
||||
@@ -1000,7 +990,9 @@ function getContrastTextColor($hexColor)
|
||||
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-due-date="<?= htmlspecialchars($row['due_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
|
||||
data-check-date="<?= htmlspecialchars($row['check_date'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-recurrence="<?= htmlspecialchars($row['recurrence_type'] ?? 'once', ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-att-count="<?= (int)$row['attachment_count'] ?>">
|
||||
<td>
|
||||
<?php if (!empty($row['subject_name'])): ?>
|
||||
<?php
|
||||
@@ -1014,6 +1006,7 @@ function getContrastTextColor($hexColor)
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="scadenzario/detail.php?id=<?= (int)$row['id'] ?>" class="fw-semibold text-decoration-none" style="color:var(--scad-heading)"><?= htmlspecialchars($row['topic'], ENT_QUOTES, 'UTF-8') ?></a>
|
||||
<?php if ((int)$row['attachment_count'] > 0): ?>
|
||||
@@ -1023,6 +1016,17 @@ function getContrastTextColor($hexColor)
|
||||
<td class="d-none d-lg-table-cell text-muted"><?= htmlspecialchars($row['law_regulation'] ?? '—', ENT_QUOTES, 'UTF-8') ?></td>
|
||||
<td><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
|
||||
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></td>
|
||||
|
||||
<td>
|
||||
<?php if (!empty($row['function_name'])): ?>
|
||||
<span class="text-muted">
|
||||
<i class="fa-solid fa-briefcase me-1"></i>
|
||||
<?= htmlspecialchars($row['function_name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($row['reparti']): ?><span class="text-muted"><i class="fa-regular fa-building me-1"></i><?= htmlspecialchars($row['reparti'], ENT_QUOTES, 'UTF-8') ?></span><?php endif; ?>
|
||||
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
|
||||
@@ -1055,143 +1059,7 @@ function getContrastTextColor($hexColor)
|
||||
<?php include('../include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Deadline Modal -->
|
||||
<div class="modal fade" id="deadlineModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-fullscreen-sm-down">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Nuova Scadenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="deadlineForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="dlId" name="id" value="">
|
||||
|
||||
<!-- Group 1: Informazioni principali -->
|
||||
<div class="form-section-title">Informazioni principali</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlSubject" class="form-label fw-semibold">Argomento</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="dlSubject" name="subject_id" style="flex:1">
|
||||
<option value="">— Nessuno —</option>
|
||||
<?php foreach ($subjects as $s): ?>
|
||||
<option value="<?= (int)$s['id'] ?>" data-color="<?= htmlspecialchars($s['color'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($s['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<a href="scadenzario/subjects/index.php" target="_blank" class="btn btn-scad-outline" title="Gestisci argomenti">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||
<input type="text" class="form-control" id="dlLaw" name="law_regulation" maxlength="500" placeholder="es. D.Lgs. 81/2008, D.M. 10.03.1998...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlTopic" class="form-label fw-semibold">Dettaglio <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="dlTopic" name="topic" required maxlength="500" rows="2" placeholder="es. Verifica estintori, Autorizzazione trasporto rifiuti..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 2: Date e frequenza -->
|
||||
<div class="form-section-title">Date e frequenza</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlRecurrence" class="form-label fw-semibold">Periodicità</label>
|
||||
<select class="form-select" id="dlRecurrence" name="recurrence_type">
|
||||
<option value="once">Una tantum</option>
|
||||
<option value="monthly">Mensile</option>
|
||||
<option value="quarterly">Trimestrale</option>
|
||||
<option value="semiannual">Semestrale</option>
|
||||
<option value="annual">Annuale</option>
|
||||
<option value="biennial">Biennale</option>
|
||||
<option value="triennial">Triennale</option>
|
||||
<option value="quadriennial">Quadriennale</option>
|
||||
<option value="quinquennial">Quinquennale</option>
|
||||
<option value="decennial">Decennale</option>
|
||||
<option value="quindecennial">Quindicennale</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDocDate" class="form-label fw-semibold">Data documento</label>
|
||||
<input type="date" class="form-control" id="dlDocDate" name="document_date">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlDueDate" class="form-label fw-semibold">Data scadenza <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="dlDueDate" name="due_date" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlCheckDate" class="form-label fw-semibold">Data ultimo controllo</label>
|
||||
<input type="date" class="form-control" id="dlCheckDate" name="check_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: Responsabili -->
|
||||
<div class="form-section-title">Responsabili</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||
<select class="form-select" id="dlDepartments" name="department_names[]" multiple>
|
||||
<?php foreach ($departments as $dept): ?>
|
||||
<option value="<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars($dept['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
<?= !empty($dept['code']) ? ' (' . htmlspecialchars($dept['code'], ENT_QUOTES, 'UTF-8') . ')' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Tutto il reparto sarà responsabile</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlEmployees" class="form-label fw-semibold">Singoli responsabili</label>
|
||||
<select class="form-select" id="dlEmployees" name="employee_ids[]" multiple>
|
||||
<?php foreach ($employees as $emp): ?>
|
||||
<option value="<?= (int)$emp['id'] ?>">
|
||||
<?= htmlspecialchars($emp['first_name'] . ' ' . $emp['last_name'], ENT_QUOTES, 'UTF-8') ?><?php if (!empty($emp['department_name'])): ?> (<?= htmlspecialchars($emp['department_name'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 4: Dettagli aggiuntivi -->
|
||||
<div class="form-section-title">Dettagli aggiuntivi</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="dlNotifDays" class="form-label fw-semibold">Giorni preavviso</label>
|
||||
<input type="number" class="form-control" id="dlNotifDays" name="notification_days" value="7" min="1" max="365">
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<label for="dlStorage" class="form-label fw-semibold">Luogo archiviazione</label>
|
||||
<input type="text" class="form-control" id="dlStorage" name="storage_location" maxlength="500" placeholder="es. Armadio A3, Server/Documenti/Sicurezza...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="dlNotes" class="form-label fw-semibold">Note</label>
|
||||
<textarea class="form-control" id="dlNotes" name="notes" rows="3" placeholder="es. Scadenza 09/06/2026, Attività in appalto a Ditta specializzata..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 5: Allegati -->
|
||||
<div class="form-section-title mt-4">Allegati</div>
|
||||
<div id="attachmentsList" class="mb-3"></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label for="dlFiles" class="form-label fw-semibold">Carica file</label>
|
||||
<input type="file" class="form-control" id="dlFiles" multiple>
|
||||
<div class="form-text">Puoi selezionare più file contemporaneamente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-scad-primary">
|
||||
<i class="fa-solid fa-check me-1"></i> Salva
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
|
||||
|
||||
<?php include('../jsinclude.php'); ?>
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
||||
@@ -1221,84 +1089,6 @@ function getContrastTextColor($hexColor)
|
||||
var fpDue = flatpickr('#filterDueRange', fpOpts);
|
||||
var fpCheck = flatpickr('#filterCheckRange', fpOpts);
|
||||
|
||||
// --- Select2 ---
|
||||
$('#dlSubject').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona argomento...',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
$('#dlDepartments').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona reparti...',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
$('#dlEmployees').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona persone...',
|
||||
allowClear: true,
|
||||
dropdownParent: $('#deadlineModal .modal-body'),
|
||||
language: 'it',
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
// --- Auto-calc due_date from document_date + recurrence ---
|
||||
var RECURRENCE_OFFSETS = {
|
||||
monthly: {
|
||||
months: 1
|
||||
},
|
||||
quarterly: {
|
||||
months: 3
|
||||
},
|
||||
semiannual: {
|
||||
months: 6
|
||||
},
|
||||
annual: {
|
||||
years: 1
|
||||
},
|
||||
biennial: {
|
||||
years: 2
|
||||
},
|
||||
triennial: {
|
||||
years: 3
|
||||
},
|
||||
quadriennial: {
|
||||
years: 4
|
||||
},
|
||||
quinquennial: {
|
||||
years: 5
|
||||
},
|
||||
decennial: {
|
||||
years: 10
|
||||
},
|
||||
quindecennial: {
|
||||
years: 15
|
||||
}
|
||||
};
|
||||
|
||||
function computeDueDate() {
|
||||
var docVal = document.getElementById('dlDocDate').value;
|
||||
var recurrence = document.getElementById('dlRecurrence').value;
|
||||
var offset = RECURRENCE_OFFSETS[recurrence];
|
||||
if (!docVal || !offset) return;
|
||||
var d = new Date(docVal + 'T00:00:00');
|
||||
if (isNaN(d.getTime())) return;
|
||||
if (offset.months) d.setMonth(d.getMonth() + offset.months);
|
||||
if (offset.years) d.setFullYear(d.getFullYear() + offset.years);
|
||||
var iso = d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
document.getElementById('dlDueDate').value = iso;
|
||||
}
|
||||
$('#dlDocDate, #dlRecurrence').on('change', computeDueDate);
|
||||
|
||||
// --- DataTables custom filters ---
|
||||
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
|
||||
if (settings.nTable.id !== 'deadlinesTable') return true;
|
||||
@@ -1460,148 +1250,8 @@ function getContrastTextColor($hexColor)
|
||||
// Apply default filter on load
|
||||
applyFiltersRefresh();
|
||||
|
||||
// --- Modal ---
|
||||
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
|
||||
var form = document.getElementById('deadlineForm');
|
||||
|
||||
// Add
|
||||
document.getElementById('btnAddDeadline').addEventListener('click', function() {
|
||||
form.reset();
|
||||
document.getElementById('dlId').value = '';
|
||||
document.getElementById('dlNotifDays').value = '7';
|
||||
document.getElementById('modalTitle').textContent = 'Nuova Scadenza';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
$('#dlSubject').val('').trigger('change');
|
||||
$('#dlDepartments').val(null).trigger('change');
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
renderAttachments([]);
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Save
|
||||
var isSaving = false;
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (isSaving) return;
|
||||
isSaving = true;
|
||||
var saveBtn = form.querySelector('[type="submit"]');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin me-1"></i> Salvataggio...';
|
||||
var formData = new FormData(form);
|
||||
|
||||
fetch('scadenzario/ajax/save_deadline.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
var deadlineId = data.id;
|
||||
var fileInput = document.getElementById('dlFiles');
|
||||
if (fileInput.files.length > 0) {
|
||||
// Upload files
|
||||
var fileData = new FormData();
|
||||
fileData.append('deadline_id', deadlineId);
|
||||
for (var i = 0; i < fileInput.files.length; i++) {
|
||||
fileData.append('files[]', fileInput.files[i]);
|
||||
}
|
||||
return fetch('scadenzario/ajax/upload_attachment.php', {
|
||||
method: 'POST',
|
||||
body: fileData
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(upData) {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message + ' ' + upData.message,
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
modal.hide();
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Salvato',
|
||||
text: data.message,
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
isSaving = false;
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fa-solid fa-check me-1"></i> Salva';
|
||||
});
|
||||
});
|
||||
|
||||
// Render attachments list
|
||||
function renderAttachments(attachments) {
|
||||
var container = document.getElementById('attachmentsList');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<div class="text-muted" style="font-size:0.85rem">Nessun allegato</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(function(a) {
|
||||
return '<div class="att-item" data-att-id="' + a.id + '">' +
|
||||
'<span class="att-name"><i class="fa-solid fa-paperclip me-1"></i>' + $('<span>').text(a.original_name).html() + '</span>' +
|
||||
'<span class="att-actions">' +
|
||||
'<a href="scadenzario/ajax/download_attachment.php?id=' + a.id + '" class="att-download" title="Scarica"><i class="fa-solid fa-download"></i></a>' +
|
||||
'<button type="button" class="att-remove" title="Elimina" data-att-id="' + a.id + '"><i class="fa-solid fa-xmark"></i></button>' +
|
||||
'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Delete attachment
|
||||
$(document).on('click', '.att-remove', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = $(this);
|
||||
var attId = btn.data('att-id');
|
||||
Swal.fire({
|
||||
title: 'Eliminare allegato?',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Elimina'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/delete_attachment.php?id=' + attId)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
btn.closest('.att-item').remove();
|
||||
if ($('#attachmentsList .att-item').length === 0) {
|
||||
renderAttachments([]);
|
||||
}
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (window.openDeadlineCreate) window.openDeadlineCreate();
|
||||
});
|
||||
|
||||
// Edit with confirmation
|
||||
@@ -1618,91 +1268,103 @@ function getContrastTextColor($hexColor)
|
||||
confirmButtonText: 'Sì, modifica',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (!result.isConfirmed) {
|
||||
return;
|
||||
if (result.isConfirmed && window.openDeadlineEdit) {
|
||||
window.openDeadlineEdit(id);
|
||||
}
|
||||
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + id)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data.success) {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var d = data.data;
|
||||
|
||||
document.getElementById('dlId').value = d.id;
|
||||
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
document.getElementById('dlDocDate').value = d.document_date || '';
|
||||
document.getElementById('dlDueDate').value = d.due_date || '';
|
||||
document.getElementById('dlCheckDate').value = d.check_date || '';
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
|
||||
if (Array.isArray(d.employee_ids)) {
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
} else {
|
||||
$('#dlEmployees').val(null).trigger('change');
|
||||
}
|
||||
|
||||
renderAttachments(d.attachments || []);
|
||||
|
||||
modal.show();
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Complete
|
||||
function submitComplete(id, createNext, copyAttachments) {
|
||||
var fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fd.append('create_next', createNext ? '1' : '0');
|
||||
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||
|
||||
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 1800,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
// Open the new deadline's detail page with the edit modal auto-opening
|
||||
if (data.new_id) {
|
||||
window.location = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.btn-complete', function() {
|
||||
var el = $(this).closest('[data-id]');
|
||||
var id = el.data('id');
|
||||
var recurrence = el.data('recurrence') || 'once';
|
||||
var attCount = parseInt(el.data('att-count'), 10) || 0;
|
||||
|
||||
// Non-recurring: simple confirm, no new deadline is created
|
||||
if (recurrence === 'once') {
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
text: 'La scadenza verrà contrassegnata come completata.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa',
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
submitComplete(id, false, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Recurring: ask whether to create the next deadline; optionally carry attachments over
|
||||
var attCheckbox = attCount > 0 ?
|
||||
'<div class="form-check d-flex align-items-center justify-content-center gap-2 mt-3">' +
|
||||
'<input class="form-check-input" type="checkbox" id="swCopyAtt" checked>' +
|
||||
'<label class="form-check-label" for="swCopyAtt">Copia gli allegati (' + attCount + ') sulla nuova scadenza</label>' +
|
||||
'</div>' :
|
||||
'';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Completare la scadenza?',
|
||||
html: 'Vuoi creare automaticamente la prossima scadenza ricorrente?' + attCheckbox,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
showDenyButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
denyButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Completa e crea la prossima',
|
||||
denyButtonText: 'Completa senza nuova',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonText: 'Completa'
|
||||
reverseButtons: true
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
fetch('scadenzario/ajax/complete_deadline.php?id=' + id)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Completata',
|
||||
text: data.message,
|
||||
timer: 2500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
.then(function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Errore', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
|
||||
submitComplete(id, true, copy);
|
||||
} else if (result.isDenied) {
|
||||
submitComplete(id, false, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1748,38 +1410,6 @@ function getContrastTextColor($hexColor)
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-open edit modal from ?edit=ID
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var editId = urlParams.get('edit');
|
||||
if (editId) {
|
||||
history.replaceState(null, '', 'scadenzario/index.php');
|
||||
fetch('scadenzario/ajax/get_deadline.php?id=' + editId)
|
||||
.then(function(r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data.success) return;
|
||||
var d = data.data;
|
||||
document.getElementById('dlId').value = d.id;
|
||||
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
||||
document.getElementById('dlTopic').value = d.topic || '';
|
||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||
document.getElementById('dlDocDate').value = d.document_date || '';
|
||||
document.getElementById('dlDueDate').value = d.due_date || '';
|
||||
document.getElementById('dlCheckDate').value = d.check_date || '';
|
||||
document.getElementById('dlNotifDays').value = d.notification_days || 7;
|
||||
document.getElementById('dlStorage').value = d.storage_location || '';
|
||||
document.getElementById('dlNotes').value = d.notes || '';
|
||||
document.getElementById('dlFiles').value = '';
|
||||
document.getElementById('modalTitle').textContent = 'Modifica Scadenza';
|
||||
$('#dlDepartments').val(d.department_names || []).trigger('change');
|
||||
$('#dlEmployees').val(d.employee_ids.map(String)).trigger('change');
|
||||
renderAttachments(d.attachments || []);
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Stampa
|
||||
function doStampa() {
|
||||
var params = [];
|
||||
@@ -1802,6 +1432,7 @@ function getContrastTextColor($hexColor)
|
||||
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
|
||||
});
|
||||
</script>
|
||||
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
include('include/headscript.php');
|
||||
|
||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||
|
||||
/* ==========================================
|
||||
PERMISSIONS (mirror trainings.php)
|
||||
========================================== */
|
||||
$isHrManager = Auth::user()->hasRole('Admin')
|
||||
|| Auth::user()->hasRole('Superuser')
|
||||
|| Auth::user()->hasRole('employee-hr')
|
||||
|| Auth::user()->hasRole('manager');
|
||||
|
||||
if (!$isHrManager) {
|
||||
header('Location: employee-profile.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Dropdown data */
|
||||
$employees = $pdo->query("
|
||||
SELECT id, first_name, last_name, employee_code
|
||||
FROM employees
|
||||
ORDER BY last_name, first_name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$topics = $pdo->query("
|
||||
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$departments = $pdo->query("
|
||||
SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
|
||||
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
body { font-size: 1.05rem; background: #f8fafc; }
|
||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
||||
.back-dashboard {
|
||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
||||
font-weight: 600; padding: 10px 18px;
|
||||
}
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
|
||||
.legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: #64748b; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
/* FullCalendar overrides */
|
||||
.fc { font-size: 0.95rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #2c3e6b; }
|
||||
.fc .fc-button-primary {
|
||||
background: #5a8fd8; border-color: #5a8fd8;
|
||||
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
|
||||
}
|
||||
.fc .fc-button-primary:hover { background: #4578c0; border-color: #4578c0; }
|
||||
.fc .fc-button-primary:disabled { background: #9bbce6; border-color: #9bbce6; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active { background: #2c3e6b; border-color: #2c3e6b; }
|
||||
.fc .fc-daygrid-day-number { color: #2c3e6b; font-weight: 500; }
|
||||
.fc .fc-daygrid-day.fc-day-today { background: #f0f4ff; }
|
||||
.fc .fc-event { border-radius: 0.3rem; padding: 2px 4px; font-weight: 600; cursor: pointer; }
|
||||
.fc .fc-event:hover { filter: brightness(0.92); }
|
||||
.fc .fc-list-event:hover td { background: #f0f4ff; }
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
||||
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper" id="appWrapper">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="mb-0">📅 Calendario Formazione</h5>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a href="trainings.php" class="btn btn-light border d-inline-flex align-items-center gap-2">
|
||||
📚 <span>Storico Formazione</span>
|
||||
</a>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- FILTERS -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Stato</label>
|
||||
<select id="filterStatus" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<option value="expired">Scaduti</option>
|
||||
<option value="due_soon">Da aggiornare</option>
|
||||
<option value="compliant">Conformi</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Corso</label>
|
||||
<select id="filterTopic" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($topics as $t): ?>
|
||||
<option value="<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Reparto</label>
|
||||
<select id="filterDepartment" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label small text-muted mb-1">Dipendente</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="filterEmployee" class="form-select">
|
||||
<option value="">Tutti</option>
|
||||
<?php foreach ($employees as $e): ?>
|
||||
<option value="<?= (int)$e['id'] ?>"><?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button id="btnResetFilters" type="button" class="btn btn-light border flex-shrink-0" title="Reset filtri">
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEGEND -->
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduto</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> Da aggiornare</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Conforme</div>
|
||||
</div>
|
||||
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var isMobile = window.innerWidth < 768;
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
locale: 'it',
|
||||
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
|
||||
},
|
||||
height: 'auto',
|
||||
navLinks: true,
|
||||
eventSources: [{
|
||||
url: 'ajax/trainings/calendar_events.php',
|
||||
extraParams: function() {
|
||||
return {
|
||||
status: document.getElementById('filterStatus').value,
|
||||
topic_id: document.getElementById('filterTopic').value,
|
||||
department_id: document.getElementById('filterDepartment').value,
|
||||
employee_id: document.getElementById('filterEmployee').value
|
||||
};
|
||||
},
|
||||
failure: function() {
|
||||
if (window.Swal) Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
|
||||
}
|
||||
}],
|
||||
eventClick: function(info) {
|
||||
info.jsEvent.preventDefault();
|
||||
if (info.event.url) window.location.href = info.event.url;
|
||||
},
|
||||
windowResize: function() {
|
||||
calendar.changeView(window.innerWidth < 768 ? 'listMonth' : 'dayGridMonth');
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||
el.addEventListener('change', function() { calendar.refetchEvents(); });
|
||||
});
|
||||
|
||||
document.getElementById('btnResetFilters').addEventListener('click', function() {
|
||||
['filterStatus', 'filterTopic', 'filterDepartment', 'filterEmployee'].forEach(function(id) {
|
||||
document.getElementById(id).value = '';
|
||||
});
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -30,6 +30,15 @@ $fDepartmentId = isset($_GET['department_id'])&& $_GET['department_id']!== '' ?
|
||||
========================================== */
|
||||
$where = [];
|
||||
$params = [];
|
||||
// Only the most recent record per (employee, topic) — older initial/refresher
|
||||
// rows stay as history on the employee profile, not in this overview.
|
||||
$where[] = "NOT EXISTS (
|
||||
SELECT 1 FROM employee_trainings et2
|
||||
WHERE et2.employee_id = et.employee_id
|
||||
AND et2.training_topic_id = et.training_topic_id
|
||||
AND (et2.completed_date > et.completed_date
|
||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||
)";
|
||||
if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
|
||||
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
|
||||
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
||||
@@ -142,12 +151,13 @@ if ($fType === '' || $fType === 'initial') {
|
||||
|
||||
/* Dropdown data */
|
||||
$employees = $pdo->query("
|
||||
SELECT id, first_name, last_name, employee_code
|
||||
SELECT id, first_name, last_name, employee_code, department_id
|
||||
FROM employees
|
||||
ORDER BY last_name, first_name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$topics = $pdo->query("
|
||||
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
|
||||
SELECT id, name, default_frequency_months, default_reminder_days
|
||||
FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$departments = $pdo->query("
|
||||
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
||||
@@ -171,6 +181,9 @@ function fmtDate(?string $d): string {
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-size: 1.05rem; background: #f8fafc; }
|
||||
@@ -234,9 +247,14 @@ function fmtDate(?string $d): string {
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="mb-0">📚 Storico Formazione</h5>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-primary" id="btnBulkTraining">
|
||||
➕ Aggiungi sessione
|
||||
</button>
|
||||
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||
↩️ Torna alla Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
@@ -316,6 +334,12 @@ function fmtDate(?string $d): string {
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div id="bulkBar" class="d-none align-items-center flex-wrap gap-2 mb-3 p-2" style="background:#fff6e5;border:1px solid #ffe0a6;border-radius:10px;">
|
||||
<span class="fw-semibold"><span id="bulkSelCount">0</span> selezionati</span>
|
||||
<button type="button" class="btn btn-sm btn-warning" id="btnBulkRenew">🔄 Aggiorna scadenza</button>
|
||||
<button type="button" class="btn btn-sm btn-link text-decoration-none" id="btnBulkDeselect">Deseleziona</button>
|
||||
</div>
|
||||
|
||||
<?php if (empty($filtered)): ?>
|
||||
<div class="text-center text-muted py-4">
|
||||
Nessuna formazione corrispondente ai filtri.
|
||||
@@ -326,6 +350,7 @@ function fmtDate(?string $d): string {
|
||||
<table class="table table-striped align-middle">
|
||||
<thead style="background-color:#cfe3ff;">
|
||||
<tr>
|
||||
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
|
||||
<th>Dipendente</th>
|
||||
<th>Reparto</th>
|
||||
<th>Corso</th>
|
||||
@@ -344,6 +369,11 @@ function fmtDate(?string $d): string {
|
||||
$days = $r['_status']['days'] ?? null;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if (!empty($r['id'])): ?>
|
||||
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
|
||||
<?= htmlspecialchars($fullName) ?>
|
||||
@@ -388,7 +418,10 @@ function fmtDate(?string $d): string {
|
||||
?>
|
||||
<div class="tr-card">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
|
||||
<div class="name">
|
||||
<div class="name d-flex align-items-start gap-2">
|
||||
<?php if (!empty($r['id'])): ?>
|
||||
<input type="checkbox" class="form-check-input row-check mt-1" value="<?= (int)$r['id'] ?>">
|
||||
<?php endif; ?>
|
||||
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
|
||||
<?= htmlspecialchars($fullName) ?>
|
||||
</a>
|
||||
@@ -425,6 +458,296 @@ function fmtDate(?string $d): string {
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- BULK TRAINING SESSION MODAL -->
|
||||
<div class="modal fade" id="bulkTrainingModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">➕ Nuova sessione formativa</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<form id="bulkTrainingForm">
|
||||
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
|
||||
<p class="text-muted small">Registra lo stesso corso, con gli stessi parametri, per più dipendenti contemporaneamente.</p>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Corso <span class="text-danger">*</span></label>
|
||||
<select id="bulkTopic" class="form-select" required>
|
||||
<option value="">— Seleziona —</option>
|
||||
<?php foreach ($topics as $t): ?>
|
||||
<option value="<?= (int)$t['id'] ?>"
|
||||
data-freq="<?= $t['default_frequency_months'] !== null ? (int)$t['default_frequency_months'] : '' ?>"
|
||||
data-rem="<?= $t['default_reminder_days'] !== null ? (int)$t['default_reminder_days'] : '' ?>">
|
||||
<?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label fw-semibold">Data completamento <span class="text-danger">*</span></label>
|
||||
<input type="date" id="bulkCompletedDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label fw-semibold">Tipo</label>
|
||||
<select id="bulkType" class="form-select">
|
||||
<option value="initial">Iniziale</option>
|
||||
<option value="refresher">Aggiornamento</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label fw-semibold">Frequenza (mesi)</label>
|
||||
<input type="number" id="bulkFreq" class="form-control" min="0" max="600" placeholder="default corso">
|
||||
<div class="form-text">Vuoto = una tantum</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label fw-semibold">Promemoria (giorni)</label>
|
||||
<input type="number" id="bulkRem" class="form-control" min="0" max="365" placeholder="default corso">
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-semibold">Erogato da</label>
|
||||
<input type="text" id="bulkDeliveredBy" class="form-control" maxlength="255" placeholder="es. Ente formatore, docente interno...">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Descrizione / note</label>
|
||||
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-12"><hr class="my-1"></div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2 align-items-end">
|
||||
<div>
|
||||
<select id="bulkDept" class="form-select form-select-sm" style="min-width:180px">
|
||||
<option value="">— Reparto —</option>
|
||||
<?php foreach ($departments as $d): ?>
|
||||
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="bulkAddDept">+ Aggiungi reparto</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkSelectAll">Tutti</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulkClear">Pulisci</button>
|
||||
</div>
|
||||
<select id="bulkEmployees" class="form-select" multiple required>
|
||||
<?php foreach ($employees as $e): ?>
|
||||
<option value="<?= (int)$e['id'] ?>" data-dept="<?= (int)($e['department_id'] ?? 0) ?>">
|
||||
<?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?><?php if (!empty($e['employee_code'])): ?> (<?= htmlspecialchars($e['employee_code'], ENT_QUOTES, 'UTF-8') ?>)<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text"><span id="bulkCount">0</span> dipendenti selezionati</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary" id="bulkSaveBtn">Registra formazione</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var bulkModal = new bootstrap.Modal(document.getElementById('bulkTrainingModal'));
|
||||
var $emp = $('#bulkEmployees');
|
||||
|
||||
$emp.select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Seleziona dipendenti...',
|
||||
dropdownParent: $('#bulkTrainingModal'),
|
||||
closeOnSelect: false,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
function updateCount() {
|
||||
document.getElementById('bulkCount').textContent = ($emp.val() || []).length;
|
||||
}
|
||||
$emp.on('change', updateCount);
|
||||
|
||||
document.getElementById('btnBulkTraining').addEventListener('click', function() {
|
||||
document.getElementById('bulkTrainingForm').reset();
|
||||
$emp.val(null).trigger('change');
|
||||
document.getElementById('bulkTopic').value = '';
|
||||
document.getElementById('bulkType').value = 'initial';
|
||||
updateCount();
|
||||
bulkModal.show();
|
||||
});
|
||||
|
||||
// Prefill frequency/reminder from the selected course
|
||||
document.getElementById('bulkTopic').addEventListener('change', function() {
|
||||
var opt = this.options[this.selectedIndex];
|
||||
document.getElementById('bulkFreq').value = opt ? (opt.getAttribute('data-freq') || '') : '';
|
||||
document.getElementById('bulkRem').value = opt ? (opt.getAttribute('data-rem') || '') : '';
|
||||
});
|
||||
|
||||
// Add all employees of the chosen department to the selection
|
||||
document.getElementById('bulkAddDept').addEventListener('click', function() {
|
||||
var dept = document.getElementById('bulkDept').value;
|
||||
if (!dept) return;
|
||||
var current = ($emp.val() || []).map(String);
|
||||
$emp.find('option').each(function() {
|
||||
if (this.getAttribute('data-dept') === String(dept) && current.indexOf(this.value) === -1) {
|
||||
current.push(this.value);
|
||||
}
|
||||
});
|
||||
$emp.val(current).trigger('change');
|
||||
});
|
||||
|
||||
document.getElementById('bulkSelectAll').addEventListener('click', function() {
|
||||
var all = $emp.find('option').map(function() { return this.value; }).get();
|
||||
$emp.val(all).trigger('change');
|
||||
});
|
||||
document.getElementById('bulkClear').addEventListener('click', function() {
|
||||
$emp.val(null).trigger('change');
|
||||
});
|
||||
|
||||
document.getElementById('bulkTrainingForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var topicId = document.getElementById('bulkTopic').value;
|
||||
var completed = document.getElementById('bulkCompletedDate').value;
|
||||
var emps = $emp.val() || [];
|
||||
|
||||
if (!topicId) { Swal.fire('Attenzione', 'Selezionare un corso.', 'warning'); return; }
|
||||
if (!completed) { Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning'); return; }
|
||||
if (emps.length === 0) { Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning'); return; }
|
||||
|
||||
var btn = document.getElementById('bulkSaveBtn');
|
||||
btn.disabled = true;
|
||||
var orig = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('training_topic_id', topicId);
|
||||
fd.append('completed_date', completed);
|
||||
fd.append('training_type', document.getElementById('bulkType').value);
|
||||
fd.append('delivered_by', document.getElementById('bulkDeliveredBy').value);
|
||||
fd.append('description', document.getElementById('bulkDescription').value);
|
||||
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
|
||||
fd.append('reminder_days', document.getElementById('bulkRem').value);
|
||||
emps.forEach(function(id) { fd.append('employee_ids[]', id); });
|
||||
|
||||
fetch('ajax/trainings/save_bulk_training.php', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
bulkModal.hide();
|
||||
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
} else {
|
||||
btn.disabled = false; btn.innerHTML = orig;
|
||||
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.disabled = false; btn.innerHTML = orig;
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- BULK RENEW DEADLINE MODAL -->
|
||||
<div class="modal fade" id="bulkRenewModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<form id="bulkRenewForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">🔄 Aggiorna scadenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted small">Imposta la data di completamento per <b id="renewCount">0</b> record selezionati. Le prossime scadenze verranno ricalcolate in base alla frequenza di ciascun corso.</p>
|
||||
<label class="form-label fw-semibold">Nuova data di completamento</label>
|
||||
<input type="date" id="renewDate" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-light border" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-warning" id="renewSaveBtn">Aggiorna</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var renewModal = new bootstrap.Modal(document.getElementById('bulkRenewModal'));
|
||||
var checkAll = document.getElementById('checkAll');
|
||||
|
||||
function checkedIds() {
|
||||
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
|
||||
.map(function(c) { return c.value; });
|
||||
}
|
||||
function refreshBulkBar() {
|
||||
var ids = checkedIds();
|
||||
var bar = document.getElementById('bulkBar');
|
||||
document.getElementById('bulkSelCount').textContent = ids.length;
|
||||
if (ids.length > 0) { bar.classList.remove('d-none'); bar.classList.add('d-flex'); }
|
||||
else { bar.classList.add('d-none'); bar.classList.remove('d-flex'); }
|
||||
var all = document.querySelectorAll('.row-check');
|
||||
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
|
||||
});
|
||||
if (checkAll) checkAll.addEventListener('change', function() {
|
||||
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = checkAll.checked; });
|
||||
refreshBulkBar();
|
||||
});
|
||||
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
|
||||
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = false; });
|
||||
if (checkAll) checkAll.checked = false;
|
||||
refreshBulkBar();
|
||||
});
|
||||
|
||||
document.getElementById('btnBulkRenew').addEventListener('click', function() {
|
||||
var ids = checkedIds();
|
||||
if (ids.length === 0) return;
|
||||
document.getElementById('renewCount').textContent = ids.length;
|
||||
renewModal.show();
|
||||
});
|
||||
|
||||
document.getElementById('bulkRenewForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var ids = checkedIds();
|
||||
var date = document.getElementById('renewDate').value;
|
||||
if (ids.length === 0) { renewModal.hide(); return; }
|
||||
if (!date) { Swal.fire('Attenzione', 'Indicare la data.', 'warning'); return; }
|
||||
|
||||
var btn = document.getElementById('renewSaveBtn');
|
||||
btn.disabled = true;
|
||||
var orig = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Salvataggio...';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('completed_date', date);
|
||||
ids.forEach(function(id) { fd.append('training_ids[]', id); });
|
||||
|
||||
fetch('ajax/trainings/bulk_update_deadline.php', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
renewModal.hide();
|
||||
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
|
||||
.then(function() { location.reload(); });
|
||||
} else {
|
||||
btn.disabled = false; btn.innerHTML = orig;
|
||||
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.disabled = false; btn.innerHTML = orig;
|
||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,868 @@
|
||||
<?php include('include/headscript.php'); ?>
|
||||
<?php
|
||||
$db = DBHandlerSelect::getInstance();
|
||||
$pdo = $db->getConnection();
|
||||
|
||||
$userId = (int)($iduserlogin ?? 0);
|
||||
|
||||
if ($userId <= 0) {
|
||||
die('Utente non valido.');
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (empty($_SESSION['user_settings_csrf'])) {
|
||||
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
$csrfToken = $_SESSION['user_settings_csrf'];
|
||||
|
||||
$successMessage = '';
|
||||
$errorMessage = '';
|
||||
|
||||
// Load countries.
|
||||
$countries = [];
|
||||
try {
|
||||
$stmtCountries = $pdo->query("
|
||||
SELECT id, name, iso_3166_2
|
||||
FROM auth_countries
|
||||
ORDER BY name ASC
|
||||
");
|
||||
$countries = $stmtCountries->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Exception $e) {
|
||||
$countries = [];
|
||||
}
|
||||
|
||||
// Load current user.
|
||||
$stmtProfileUser = $pdo->prepare("
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
password,
|
||||
first_name,
|
||||
last_name,
|
||||
phone,
|
||||
avatar,
|
||||
address,
|
||||
country_id,
|
||||
birthday,
|
||||
role_id,
|
||||
status,
|
||||
last_login
|
||||
FROM auth_users
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtProfileUser->execute([$userId]);
|
||||
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$profileUser) {
|
||||
die('Utente non trovato.');
|
||||
}
|
||||
|
||||
function e($value)
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function normalizeAvatarPath($avatar)
|
||||
{
|
||||
$avatar = trim((string)$avatar);
|
||||
|
||||
if ($avatar === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If the database already contains a complete relative path, use it as it is.
|
||||
if (
|
||||
str_starts_with($avatar, '../') ||
|
||||
str_starts_with($avatar, './') ||
|
||||
str_starts_with($avatar, '/') ||
|
||||
str_starts_with($avatar, 'http://') ||
|
||||
str_starts_with($avatar, 'https://')
|
||||
) {
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
// If the database contains only the filename, build the expected user upload path.
|
||||
return '../upload/users/' . $avatar;
|
||||
}
|
||||
|
||||
function getAvatarInitials($profileUser)
|
||||
{
|
||||
$first = trim((string)($profileUser['first_name'] ?? ''));
|
||||
$last = trim((string)($profileUser['last_name'] ?? ''));
|
||||
$email = trim((string)($profileUser['email'] ?? ''));
|
||||
|
||||
$initials = '';
|
||||
|
||||
if ($first !== '') {
|
||||
$initials .= mb_substr($first, 0, 1);
|
||||
}
|
||||
|
||||
if ($last !== '') {
|
||||
$initials .= mb_substr($last, 0, 1);
|
||||
}
|
||||
|
||||
if ($initials === '' && $email !== '') {
|
||||
$initials = mb_substr($email, 0, 1);
|
||||
}
|
||||
|
||||
return strtoupper($initials ?: 'U');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$postedToken = $_POST['csrf_token'] ?? '';
|
||||
|
||||
if (!hash_equals($csrfToken, $postedToken)) {
|
||||
$errorMessage = 'Sessione non valida. Ricarica la pagina e riprova.';
|
||||
} else {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$firstName = trim($_POST['first_name'] ?? '');
|
||||
$lastName = trim($_POST['last_name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$countryId = $_POST['country_id'] !== '' ? (int)$_POST['country_id'] : null;
|
||||
$birthday = trim($_POST['birthday'] ?? '');
|
||||
|
||||
$currentPassword = $_POST['current_password'] ?? '';
|
||||
$newPassword = $_POST['new_password'] ?? '';
|
||||
$confirmPassword = $_POST['confirm_password'] ?? '';
|
||||
|
||||
$birthdayValue = null;
|
||||
$avatarToSave = $profileUser['avatar'];
|
||||
|
||||
if ($birthday !== '') {
|
||||
$dateObj = DateTime::createFromFormat('Y-m-d', $birthday);
|
||||
|
||||
if (!$dateObj || $dateObj->format('Y-m-d') !== $birthday) {
|
||||
$errorMessage = 'La data di nascita non è valida.';
|
||||
} else {
|
||||
$birthdayValue = $birthday;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errorMessage && $email === '') {
|
||||
$errorMessage = 'L’email è obbligatoria.';
|
||||
}
|
||||
|
||||
if (!$errorMessage && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errorMessage = 'L’email inserita non è valida.';
|
||||
}
|
||||
|
||||
// Check unique email.
|
||||
if (!$errorMessage) {
|
||||
$stmtCheckEmail = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM auth_users
|
||||
WHERE email = ? AND id <> ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtCheckEmail->execute([$email, $userId]);
|
||||
|
||||
if ($stmtCheckEmail->fetchColumn()) {
|
||||
$errorMessage = 'Questa email è già utilizzata da un altro utente.';
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar upload.
|
||||
if (!$errorMessage && isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
|
||||
$errorMessage = 'Errore durante il caricamento dell’avatar.';
|
||||
} else {
|
||||
$maxFileSize = 2 * 1024 * 1024; // 2 MB
|
||||
|
||||
if ($_FILES['avatar']['size'] > $maxFileSize) {
|
||||
$errorMessage = 'L’avatar non può superare 2 MB.';
|
||||
} else {
|
||||
$tmpFile = $_FILES['avatar']['tmp_name'];
|
||||
$originalName = $_FILES['avatar']['name'];
|
||||
|
||||
$allowedMimeTypes = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
'image/gif' => 'gif',
|
||||
];
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($tmpFile);
|
||||
|
||||
if (!array_key_exists($mimeType, $allowedMimeTypes)) {
|
||||
$errorMessage = 'Formato avatar non valido. Sono consentiti JPG, PNG, WEBP o GIF.';
|
||||
} else {
|
||||
$uploadDir = __DIR__ . '/../upload/users/';
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
$extension = $allowedMimeTypes[$mimeType];
|
||||
$safeOriginalName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
|
||||
$fileName = time() . '_' . $userId . '_' . $safeOriginalName . '.' . $extension;
|
||||
|
||||
$destination = $uploadDir . $fileName;
|
||||
|
||||
if (!move_uploaded_file($tmpFile, $destination)) {
|
||||
$errorMessage = 'Impossibile salvare il file avatar.';
|
||||
} else {
|
||||
// Path used by pages inside userarea, for example:
|
||||
// <img src="../upload/users/file.jpg">
|
||||
$avatarToSave = $fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$passwordToSave = null;
|
||||
$wantsPasswordChange = ($currentPassword !== '' || $newPassword !== '' || $confirmPassword !== '');
|
||||
|
||||
if (!$errorMessage && $wantsPasswordChange) {
|
||||
if ($currentPassword === '') {
|
||||
$errorMessage = 'Inserisci la password attuale.';
|
||||
} elseif ($newPassword === '') {
|
||||
$errorMessage = 'Inserisci la nuova password.';
|
||||
} elseif (strlen($newPassword) < 8) {
|
||||
$errorMessage = 'La nuova password deve contenere almeno 8 caratteri.';
|
||||
} elseif ($newPassword !== $confirmPassword) {
|
||||
$errorMessage = 'La conferma password non corrisponde.';
|
||||
} elseif (!password_verify($currentPassword, $profileUser['password'])) {
|
||||
$errorMessage = 'La password attuale non è corretta.';
|
||||
} else {
|
||||
// Password is encrypted before saving.
|
||||
$passwordToSave = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errorMessage) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmtUpdate = $pdo->prepare("
|
||||
UPDATE auth_users
|
||||
SET
|
||||
email = :email,
|
||||
first_name = :first_name,
|
||||
last_name = :last_name,
|
||||
phone = :phone,
|
||||
avatar = :avatar,
|
||||
address = :address,
|
||||
country_id = :country_id,
|
||||
birthday = :birthday,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpdate->execute([
|
||||
':email' => $email,
|
||||
':first_name' => $firstName !== '' ? $firstName : null,
|
||||
':last_name' => $lastName !== '' ? $lastName : null,
|
||||
':phone' => $phone !== '' ? $phone : null,
|
||||
':avatar' => $avatarToSave !== '' ? $avatarToSave : null,
|
||||
':address' => $address !== '' ? $address : null,
|
||||
':country_id' => $countryId,
|
||||
':birthday' => $birthdayValue,
|
||||
':id' => $userId,
|
||||
]);
|
||||
|
||||
if ($passwordToSave !== null) {
|
||||
$stmtPassword = $pdo->prepare("
|
||||
UPDATE auth_users
|
||||
SET password = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtPassword->execute([$passwordToSave, $userId]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$successMessage = $passwordToSave !== null
|
||||
? 'Profilo, avatar e password aggiornati correttamente.'
|
||||
: 'Profilo aggiornato correttamente.';
|
||||
|
||||
// Reload updated user.
|
||||
$stmtProfileUser->execute([$userId]);
|
||||
$profileUser = $stmtProfileUser->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$_SESSION['user_settings_csrf'] = bin2hex(random_bytes(32));
|
||||
$csrfToken = $_SESSION['user_settings_csrf'];
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
$errorMessage = 'Errore durante il salvataggio delle impostazioni.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$avatarPath = normalizeAvatarPath($profileUser['avatar'] ?? '');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||
<?php include('cssinclude.php'); ?>
|
||||
<title>Impostazioni Utente <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f3f6f8, #e8eef3);
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 1.4rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settings-wrap {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
h3.settings-title {
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
margin-bottom: 1.2rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-heading {
|
||||
margin: 0;
|
||||
font-weight: 800;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
margin: 0;
|
||||
color: #6b7a89;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.settings-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
background: linear-gradient(135deg, #f7fbff, #edf5ff);
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-box {
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
border-radius: 34px;
|
||||
background: linear-gradient(135deg, #cde5ff, #dff0ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin: 0 auto 14px auto;
|
||||
}
|
||||
|
||||
.avatar-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.avatar-email {
|
||||
color: #6b7a89;
|
||||
margin: 4px 0 14px 0;
|
||||
font-size: 0.92rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.avatar-upload-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
padding: 11px 14px;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.avatar-upload-label:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
}
|
||||
|
||||
.avatar-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar-help {
|
||||
font-size: 0.82rem;
|
||||
color: #6b7a89;
|
||||
margin-top: 10px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
color: #6b7a89;
|
||||
font-size: 0.88rem;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 700;
|
||||
color: #2b3e50;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #d8e0e7;
|
||||
padding: 10px 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #8bbcf7;
|
||||
box-shadow: 0 0 0 0.18rem rgba(139, 188, 247, 0.25);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.86rem;
|
||||
color: #6b7a89;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.password-box {
|
||||
background: linear-gradient(135deg, #fff7e6, #fffaf0);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(255, 184, 107, 0.45);
|
||||
}
|
||||
|
||||
.btn-save-settings {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
padding: 13px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #61ce5dff, #61ce5dff);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-save-settings:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
padding: 13px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: #2b3e50;
|
||||
background: linear-gradient(135deg, #e5e7eb, #f3f4f6);
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.13);
|
||||
color: #2b3e50;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 16px;
|
||||
border: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.readonly-note {
|
||||
background: linear-gradient(135deg, #cde5ff, #dff0ff);
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
color: #2b3e50;
|
||||
font-weight: 600;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.selected-file-name {
|
||||
font-size: 0.84rem;
|
||||
color: #2b3e50;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.profile-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.avatar-panel {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-save-settings,
|
||||
.btn-back {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper toggled">
|
||||
<?php include('include/navbar.php'); ?>
|
||||
<?php include('include/topbar.php'); ?>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
|
||||
<div class="settings-wrap">
|
||||
<h3 class="settings-title">Impostazioni Utente</h3>
|
||||
|
||||
<?php if ($successMessage): ?>
|
||||
<div class="alert alert-success">
|
||||
<?= e($successMessage); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($errorMessage): ?>
|
||||
<div class="alert alert-danger">
|
||||
<?= e($errorMessage); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken); ?>">
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-header">
|
||||
<div class="settings-icon">👤</div>
|
||||
<div>
|
||||
<p class="settings-heading">Profilo personale</p>
|
||||
<p class="settings-subtitle">Dati anagrafici, contatti e avatar utente</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-body">
|
||||
|
||||
<div class="readonly-note">
|
||||
Ruolo, stato account e impostazioni di sicurezza avanzate non sono modificabili da questa pagina.
|
||||
</div>
|
||||
|
||||
<div class="profile-layout">
|
||||
<div class="avatar-panel">
|
||||
<div class="avatar-box" id="avatarPreviewBox">
|
||||
<?php if (!empty($avatarPath)): ?>
|
||||
<img src="<?= e($avatarPath); ?>" class="user-img" alt="user avatar" id="avatarPreviewImage">
|
||||
<?php else: ?>
|
||||
<span id="avatarInitials"><?= e(getAvatarInitials($profileUser)); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<p class="avatar-name">
|
||||
<?= e(trim(($profileUser['first_name'] ?? '') . ' ' . ($profileUser['last_name'] ?? '')) ?: 'Utente'); ?>
|
||||
</p>
|
||||
|
||||
<p class="avatar-email">
|
||||
<?= e($profileUser['email']); ?>
|
||||
</p>
|
||||
|
||||
<label for="avatar" class="avatar-upload-label">
|
||||
Carica avatar
|
||||
</label>
|
||||
|
||||
<input type="file"
|
||||
class="avatar-upload-input"
|
||||
id="avatar"
|
||||
name="avatar"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif">
|
||||
|
||||
<div id="selectedFileName" class="selected-file-name"></div>
|
||||
|
||||
<div class="avatar-help">
|
||||
Formati consentiti: JPG, PNG, WEBP, GIF.<br>
|
||||
Dimensione massima: 2 MB.
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
Stato account: <?= e($profileUser['status']); ?>
|
||||
<?php if (!empty($profileUser['last_login'])): ?>
|
||||
<br>Ultimo accesso: <?= e(date('d/m/Y H:i', strtotime($profileUser['last_login']))); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="first_name">Nome</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
value="<?= e($profileUser['first_name']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="last_name">Cognome</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
value="<?= e($profileUser['last_name']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">Email</label>
|
||||
<input type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="<?= e($profileUser['email']); ?>"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">Telefono</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value="<?= e($profileUser['phone']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="birthday">Data di nascita</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="birthday"
|
||||
name="birthday"
|
||||
value="<?= e($profileUser['birthday']); ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="country_id">Paese</label>
|
||||
<select class="form-select" id="country_id" name="country_id">
|
||||
<option value="">Seleziona...</option>
|
||||
<?php foreach ($countries as $country): ?>
|
||||
<option value="<?= (int)$country['id']; ?>"
|
||||
<?= ((int)$profileUser['country_id'] === (int)$country['id']) ? 'selected' : ''; ?>>
|
||||
<?= e($country['name'] . (!empty($country['iso_3166_2']) ? ' (' . $country['iso_3166_2'] . ')' : '')); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label" for="address">Indirizzo</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="address"
|
||||
name="address"
|
||||
value="<?= e($profileUser['address']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-header">
|
||||
<div class="settings-icon">🔐</div>
|
||||
<div>
|
||||
<p class="settings-heading">Cambio password</p>
|
||||
<p class="settings-subtitle">Compila questa sezione solo se vuoi modificare la password</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-body">
|
||||
<div class="password-box">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="current_password">Password attuale</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="new_password">Nuova password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="confirm_password">Conferma nuova password</label>
|
||||
<input type="password"
|
||||
class="form-control"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
Se lasci questi campi vuoti, la password attuale rimane invariata.
|
||||
La nuova password deve avere almeno 8 caratteri.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions-row">
|
||||
<a href="production_dashboard.php" class="btn-back">← Torna alla dashboard</a>
|
||||
<button type="submit" class="btn-save-settings">Salva impostazioni</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include('jsinclude.php'); ?>
|
||||
<?php include('include/footer.php'); ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const avatarInput = document.getElementById('avatar');
|
||||
const previewBox = document.getElementById('avatarPreviewBox');
|
||||
const selectedFileName = document.getElementById('selectedFileName');
|
||||
|
||||
if (!avatarInput || !previewBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
avatarInput.addEventListener('change', function() {
|
||||
const file = this.files && this.files[0] ? this.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
selectedFileName.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFileName.textContent = file.name;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
previewBox.innerHTML = '';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = event.target.result;
|
||||
img.className = 'user-img';
|
||||
img.alt = 'user avatar';
|
||||
|
||||
previewBox.appendChild(img);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user