Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc3af01f3 | |||
| 3d54140280 | |||
| bfdbbbfc8f | |||
| 40a5771a4b | |||
| 9f5a585717 | |||
| 9ec5419a86 | |||
| c05091e020 | |||
| 0b470f290e | |||
| e74870c8d3 | |||
| 9001eff317 | |||
| 7cbd74111d | |||
| 650676037a | |||
| 2fc34c3cf4 | |||
| 955a7ed9e9 | |||
| cb221a8039 |
@@ -31,6 +31,8 @@ MAIL_USERNAME=null
|
|||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
|
||||||
|
MANAGER_USER_ID=
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
PUSHER_APP_ID=
|
||||||
PUSHER_APP_KEY=
|
PUSHER_APP_KEY=
|
||||||
PUSHER_APP_SECRET=
|
PUSHER_APP_SECRET=
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ class LoginController extends Controller
|
|||||||
return redirect()->to('userarea/production_dashboard.php');
|
return redirect()->to('userarea/production_dashboard.php');
|
||||||
} elseif ($user->hasRole('User')) {
|
} elseif ($user->hasRole('User')) {
|
||||||
return redirect()->to('userarea/production_dashboard.php');
|
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
|
// 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;
|
$skipped = 0;
|
||||||
$errors = 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("
|
$stmt = $pdo->query("
|
||||||
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||||
et.reminder_days, et.delivered_by,
|
et.reminder_days, et.delivered_by,
|
||||||
@@ -60,6 +61,13 @@ $stmt = $pdo->query("
|
|||||||
JOIN employees e ON e.id = et.employee_id
|
JOIN employees e ON e.id = et.employee_id
|
||||||
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
||||||
WHERE et.next_due_date IS NOT NULL
|
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);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -321,6 +321,11 @@
|
|||||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
<i class='bx bx-radio-circle'></i>Storico Formazione
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="training_calendar.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Calendario Formazione
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.skills.view')) : ?>
|
<?php if (userCan('hr.skills.view')) : ?>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ if (!$__trWidgetHr) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Only the most recent record per (employee, topic) — older history rows ignored. */
|
||||||
$__trRows = $pdo->query("
|
$__trRows = $pdo->query("
|
||||||
SELECT et.id,
|
SELECT et.id,
|
||||||
et.next_due_date,
|
et.next_due_date,
|
||||||
@@ -27,6 +28,13 @@ $__trRows = $pdo->query("
|
|||||||
FROM employee_trainings et
|
FROM employee_trainings et
|
||||||
JOIN training_topics tt ON tt.id = et.training_topic_id
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
WHERE et.next_due_date IS NOT NULL
|
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);
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$__expiredCount = 0;
|
$__expiredCount = 0;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ $dashboardSections = [
|
|||||||
'permission' => 'warehouse.dashboard.view',
|
'permission' => 'warehouse.dashboard.view',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Scadenziario',
|
'label' => 'Smart-Alert',
|
||||||
'icon' => '⏰',
|
'icon' => '⏰',
|
||||||
'class' => 'btn-scadenziario',
|
'class' => 'btn-scadenziario',
|
||||||
'url' => 'scadenzario/index.php',
|
'url' => 'scadenzario/index.php',
|
||||||
@@ -187,7 +187,7 @@ $dashboardSections = [
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
<?php include('cssinclude.php'); ?>
|
<?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 -->
|
<!-- Bootstrap + jQuery -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<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'); ?>
|
<?php include(__DIR__ . '/include/training_widget.php'); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="dashboard-title">Dashboard Produzione</h3>
|
<h3 class="dashboard-title">Dashboard</h3>
|
||||||
|
|
||||||
<!-- ===== STATISTICHE PRINCIPALI ===== -->
|
<!-- ===== STATISTICHE PRINCIPALI ===== -->
|
||||||
<div class="stats-row">
|
<div class="stats-row">
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ header('Content-Type: application/json');
|
|||||||
require_once(__DIR__ . '/../../class/db-functions.php');
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
try {
|
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.']);
|
echo json_encode(['success' => false, 'message' => 'ID non valido.']);
|
||||||
exit;
|
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();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
@@ -34,11 +41,13 @@ try {
|
|||||||
->execute([$id, $currentUserId]);
|
->execute([$id, $currentUserId]);
|
||||||
|
|
||||||
$newId = null;
|
$newId = null;
|
||||||
|
$newDueDate = null;
|
||||||
|
|
||||||
// If recurring, create next deadline
|
// If recurring AND the user asked for it, create the next deadline
|
||||||
if ($deadline['recurrence_type'] !== 'once') {
|
if ($deadline['recurrence_type'] !== 'once' && $createNext) {
|
||||||
$dueDate = new DateTime($deadline['due_date']);
|
$dueDate = new DateTime($deadline['due_date']);
|
||||||
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
$checkDate = $deadline['check_date'] ? new DateTime($deadline['check_date']) : null;
|
||||||
|
$documentDate = $deadline['document_date'] ? new DateTime($deadline['document_date']) : null;
|
||||||
|
|
||||||
switch ($deadline['recurrence_type']) {
|
switch ($deadline['recurrence_type']) {
|
||||||
case 'monthly': $interval = new DateInterval('P1M'); break;
|
case 'monthly': $interval = new DateInterval('P1M'); break;
|
||||||
@@ -57,23 +66,25 @@ try {
|
|||||||
if ($interval) {
|
if ($interval) {
|
||||||
$dueDate->add($interval);
|
$dueDate->add($interval);
|
||||||
if ($checkDate) $checkDate->add($interval);
|
if ($checkDate) $checkDate->add($interval);
|
||||||
|
if ($documentDate) $documentDate->add($interval);
|
||||||
|
|
||||||
$ins = $pdo->prepare("
|
$ins = $pdo->prepare("
|
||||||
INSERT INTO scad_deadlines
|
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)
|
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$ins->execute([
|
$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'),
|
$deadline['recurrence_type'], $dueDate->format('Y-m-d'),
|
||||||
$checkDate ? $checkDate->format('Y-m-d') : null,
|
$checkDate ? $checkDate->format('Y-m-d') : null,
|
||||||
$deadline['document_date'],
|
$documentDate ? $documentDate->format('Y-m-d') : null,
|
||||||
$deadline['notification_days'], $deadline['storage_location'],
|
$deadline['notification_days'], $deadline['storage_location'],
|
||||||
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
$deadline['notes'], $deadline['created_by'], $deadline['departments']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newId = $pdo->lastInsertId();
|
$newId = $pdo->lastInsertId();
|
||||||
|
$newDueDate = $dueDate;
|
||||||
|
|
||||||
// Copy employee assignments
|
// Copy employee assignments
|
||||||
$empStmt = $pdo->prepare("SELECT employee_id FROM scad_deadline_employee WHERE deadline_id = ?");
|
$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
|
// History for new
|
||||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'created', ?)")
|
||||||
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
->execute([$newId, $currentUserId, 'Creata automaticamente dalla scadenza #' . $id]);
|
||||||
@@ -97,7 +133,7 @@ try {
|
|||||||
|
|
||||||
$msg = 'Scadenza completata con successo.';
|
$msg = 'Scadenza completata con successo.';
|
||||||
if ($newId) {
|
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]);
|
echo json_encode(['success' => true, 'message' => $msg, 'new_id' => $newId]);
|
||||||
|
|||||||
@@ -23,20 +23,32 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete file
|
// Remove this link (DB record) first
|
||||||
|
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if ($stillReferenced) {
|
||||||
|
$action = 'attachment_unlinked';
|
||||||
|
$message = 'Collegamento rimosso. Il file è conservato (usato da un\'altra scadenza).';
|
||||||
|
} else {
|
||||||
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
$filePath = __DIR__ . '/../attachments/' . $att['stored_name'];
|
||||||
if (file_exists($filePath)) {
|
if (file_exists($filePath)) {
|
||||||
unlink($filePath);
|
unlink($filePath);
|
||||||
}
|
}
|
||||||
|
$action = 'attachment_removed';
|
||||||
// Delete DB record
|
$message = 'Allegato eliminato.';
|
||||||
$pdo->prepare("DELETE FROM scad_deadline_attachments WHERE id = ?")->execute([$id]);
|
}
|
||||||
|
|
||||||
// History
|
// History
|
||||||
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, 'attachment_removed', ?)")
|
$pdo->prepare("INSERT INTO scad_deadline_histories (deadline_id, user_id, action, notes) VALUES (?, ?, ?, ?)")
|
||||||
->execute([$att['deadline_id'], $currentUserId, $att['original_name']]);
|
->execute([$att['deadline_id'], $currentUserId, $action, $att['original_name']]);
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'message' => 'Allegato eliminato.']);
|
echo json_encode(['success' => true, 'message' => $message]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
echo json_encode(['success' => false, 'message' => 'Errore: ' . $e->getMessage()]);
|
||||||
|
|||||||
@@ -13,10 +13,29 @@ try {
|
|||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$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 = $pdo->prepare("DELETE FROM scad_deadlines WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
if ($stmt->rowCount() > 0) {
|
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.']);
|
echo json_encode(['success' => true, 'message' => 'Scadenza eliminata con successo.']);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['success' => false, 'message' => 'Scadenza non trovata.']);
|
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;
|
$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;
|
$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'] ?? '');
|
$topic = trim($_POST['topic'] ?? '');
|
||||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||||
@@ -52,15 +53,25 @@ try {
|
|||||||
if ($id) {
|
if ($id) {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE scad_deadlines SET
|
UPDATE scad_deadlines SET
|
||||||
subject_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||||
storage_location = ?, notes = ?, departments = ?
|
storage_location = ?, notes = ?, departments = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id, $topic, $law_regulation, $recurrence_type,
|
$subject_id,
|
||||||
$due_date, $check_date, $document_date, $notification_days,
|
$function_id,
|
||||||
$storage_location, $notes, $departmentsStr, $id
|
$topic,
|
||||||
|
$law_regulation,
|
||||||
|
$recurrence_type,
|
||||||
|
$due_date,
|
||||||
|
$check_date,
|
||||||
|
$document_date,
|
||||||
|
$notification_days,
|
||||||
|
$storage_location,
|
||||||
|
$notes,
|
||||||
|
$departmentsStr,
|
||||||
|
$id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-link employees
|
// Re-link employees
|
||||||
@@ -75,14 +86,24 @@ try {
|
|||||||
// INSERT
|
// INSERT
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO scad_deadlines
|
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)
|
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id, $topic, $law_regulation, $recurrence_type,
|
$subject_id,
|
||||||
$due_date, $check_date, $document_date, $notification_days,
|
$function_id,
|
||||||
$storage_location, $notes, $currentUserId, $departmentsStr
|
$topic,
|
||||||
|
$law_regulation,
|
||||||
|
$recurrence_type,
|
||||||
|
$due_date,
|
||||||
|
$check_date,
|
||||||
|
$document_date,
|
||||||
|
$notification_days,
|
||||||
|
$storage_location,
|
||||||
|
$notes,
|
||||||
|
$currentUserId,
|
||||||
|
$departmentsStr
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$deadlineId = $pdo->lastInsertId();
|
$deadlineId = $pdo->lastInsertId();
|
||||||
@@ -107,7 +128,6 @@ try {
|
|||||||
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
'message' => $id ? 'Scadenza aggiornata con successo.' : 'Scadenza creata con successo.',
|
||||||
'id' => $deadlineId
|
'id' => $deadlineId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if (isset($pdo) && $pdo->inTransaction()) {
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
$pdo->rollBack();
|
$pdo->rollBack();
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ $pdo = $db->getConnection();
|
|||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
$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;
|
$sent = 0;
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
$errors = 0;
|
$errors = 0;
|
||||||
@@ -143,6 +154,11 @@ foreach ($deadlines as $dl) {
|
|||||||
);
|
);
|
||||||
$mail->addAddress($emp['email'], trim($emp['first_name'] . ' ' . $emp['last_name']));
|
$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'];
|
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||||
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
$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'];
|
$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'];
|
$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', 'notification_sent' => '#adb5bd'];
|
$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', 'notification_sent' => 'fa-bell'];
|
$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 ?>">
|
<base href="<?= $baseHref ?>">
|
||||||
<?php include('../cssinclude.php'); ?>
|
<?php include('../cssinclude.php'); ?>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<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>
|
<title><?= $deadline ? htmlspecialchars($deadline['topic'], ENT_QUOTES, 'UTF-8') . ' — ' : '' ?>Scadenzario</title>
|
||||||
<script>
|
<script>
|
||||||
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
|
if (window.innerWidth > 1024) document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -755,25 +763,33 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
|||||||
</div>
|
</div>
|
||||||
<?php include('../include/footer.php'); ?>
|
<?php include('../include/footer.php'); ?>
|
||||||
</div>
|
</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 include('../jsinclude.php'); ?>
|
||||||
<?php if ($deadline && !$isCompleted): ?>
|
<?php if ($deadline && !$isCompleted): ?>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(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() {
|
$('#btnModifica').on('click', function() {
|
||||||
window.location.href = 'scadenzario/index.php?edit=<?= (int)$deadline['id'] ?>';
|
window.openDeadlineEdit(<?= (int)$deadline['id'] ?>);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#btnCompleta').on('click', function() {
|
function detailSubmitComplete(createNext, copyAttachments) {
|
||||||
Swal.fire({
|
var fd = new FormData();
|
||||||
title: 'Completare la scadenza?',
|
fd.append('id', '<?= (int)$deadline['id'] ?>');
|
||||||
icon: 'question',
|
fd.append('create_next', createNext ? '1' : '0');
|
||||||
showCancelButton: true,
|
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||||
confirmButtonColor: '#198754',
|
|
||||||
cancelButtonText: 'Annulla',
|
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||||
confirmButtonText: 'Completa'
|
method: 'POST',
|
||||||
}).then(function(result) {
|
body: fd
|
||||||
if (result.isConfirmed) {
|
})
|
||||||
fetch('scadenzario/ajax/complete_deadline.php?id=<?= (int)$deadline['id'] ?>')
|
|
||||||
.then(function(r) {
|
.then(function(r) {
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
@@ -783,11 +799,15 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
|||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Completata',
|
title: 'Completata',
|
||||||
text: data.message,
|
text: data.message,
|
||||||
timer: 2500,
|
timer: 1800,
|
||||||
showConfirmButton: false
|
showConfirmButton: false
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
if (data.new_id) {
|
||||||
|
window.location.href = 'scadenzario/detail.php?id=' + data.new_id + '#edit';
|
||||||
|
} else {
|
||||||
window.location.href = 'scadenzario/index.php';
|
window.location.href = 'scadenzario/index.php';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire('Errore', data.message, 'error');
|
Swal.fire('Errore', data.message, 'error');
|
||||||
@@ -797,10 +817,60 @@ if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
|||||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
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',
|
||||||
|
reverseButtons: true
|
||||||
|
}).then(function(result) {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
var copy = attCount > 0 ? document.getElementById('swCopyAtt').checked : false;
|
||||||
|
detailSubmitComplete(true, copy);
|
||||||
|
} else if (result.isDenied) {
|
||||||
|
detailSubmitComplete(false, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</body>
|
</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
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders two status banners for the current user:
|
* Renders two status banners for the current user:
|
||||||
* - red -> overdue deadlines (scaduta)
|
* - red -> overdue deadlines (scaduta)
|
||||||
@@ -44,42 +45,69 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
|||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.my-deadlines-widgets {
|
.my-deadlines-widgets {
|
||||||
display: flex; flex-wrap: wrap; gap: 0.75rem;
|
display: flex;
|
||||||
margin-bottom: 1rem; width: 100%;
|
gap: 0.75rem;
|
||||||
}
|
margin-bottom: 1rem;
|
||||||
.my-deadlines-widgets:empty { display: none; }
|
flex-wrap: wrap;
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-deadlines-widgets .mdw {
|
.my-deadlines-widgets .mdw {
|
||||||
flex: 1 1 0; min-width: 0;
|
flex: 1 1 260px;
|
||||||
display: flex; align-items: center; gap: 0.75rem;
|
display: flex;
|
||||||
padding: 0.8rem 0.9rem; border-radius: 0.6rem;
|
align-items: center;
|
||||||
text-decoration: none; color: #fff;
|
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);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
transition: transform 0.15s, box-shadow 0.15s;
|
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:hover {
|
||||||
.my-deadlines-widgets .mdw-orange { background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%); }
|
transform: translateY(-1px);
|
||||||
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
|
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 {
|
.my-deadlines-widgets .mdw-icon {
|
||||||
width: 38px; height: 38px; border-radius: 50%;
|
width: 42px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
height: 42px;
|
||||||
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
|
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-body {
|
||||||
.my-deadlines-widgets .mdw-label { font-size: 0.78rem; opacity: 0.95;
|
flex: 1;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
line-height: 1.2;
|
||||||
.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); }
|
|
||||||
}
|
}
|
||||||
@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>
|
</style>
|
||||||
<div class="my-deadlines-widgets">
|
<div class="my-deadlines-widgets">
|
||||||
@@ -88,7 +116,7 @@ if (!$_emp || ($_overdue === 0 && $_approaching === 0)) {
|
|||||||
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
<span class="mdw-icon"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
||||||
<span class="mdw-body">
|
<span class="mdw-body">
|
||||||
<span class="mdw-count"><?= $_overdue ?></span>
|
<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 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>
|
||||||
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -37,12 +37,14 @@ $sql = "
|
|||||||
SELECT d.*,
|
SELECT d.*,
|
||||||
s.name AS subject_name,
|
s.name AS subject_name,
|
||||||
s.color AS subject_color,
|
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 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,
|
GROUP_CONCAT(DISTINCT dep.name ORDER BY dep.name SEPARATOR ', ') as reparti_persone,
|
||||||
d.departments as reparti_assegnati,
|
d.departments as reparti_assegnati,
|
||||||
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
|
(SELECT COUNT(*) FROM scad_deadline_attachments att WHERE att.deadline_id = d.id) as attachment_count
|
||||||
FROM scad_deadlines d
|
FROM scad_deadlines d
|
||||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
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 scad_deadline_employee de ON de.deadline_id = d.id
|
||||||
LEFT JOIN employees e ON e.id = de.employee_id
|
LEFT JOIN employees e ON e.id = de.employee_id
|
||||||
LEFT JOIN departments dep ON dep.id = e.department_id
|
LEFT JOIN departments dep ON dep.id = e.department_id
|
||||||
@@ -69,27 +71,7 @@ $stmt = $pdo->prepare($sql);
|
|||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$deadlines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$employees = $pdo->query("
|
require __DIR__ . '/include/deadline_form_data.php';
|
||||||
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);
|
|
||||||
|
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
@@ -494,7 +476,8 @@ function getContrastTextColor($hexColor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#deadlinesTable td:first-child {
|
#deadlinesTable td:first-child {
|
||||||
max-width: 150px;
|
max-width: 110px;
|
||||||
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Attachment list in modal */
|
/* 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">
|
<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>
|
<i class="fa-solid fa-tags"></i><span>Argomenti</span>
|
||||||
</a>
|
</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">
|
<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>
|
<i class="fa-solid fa-calendar-days"></i><span>Calendario</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -842,6 +828,7 @@ function getContrastTextColor($hexColor)
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<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/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><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>
|
<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>
|
</ul>
|
||||||
@@ -923,7 +910,9 @@ function getContrastTextColor($hexColor)
|
|||||||
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
data-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', 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-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'])): ?>
|
<?php if (!empty($row['subject_name'])): ?>
|
||||||
<div class="mb-1"><?php
|
<div class="mb-1"><?php
|
||||||
$subjectBadgeBg = $row['subject_color'] ?: '#6c757d';
|
$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%">
|
<table id="deadlinesTable" class="table table-hover align-middle mb-0" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Argomento</th>
|
<th style="width:110px">Argomento</th>
|
||||||
<th>Dettaglio</th>
|
<th>Dettaglio</th>
|
||||||
<th class="d-none d-lg-table-cell">Legge/Art.</th>
|
<th class="d-none d-lg-table-cell">Legge/Art.</th>
|
||||||
<th>Scadenza</th>
|
<th>Scadenza</th>
|
||||||
<th class="d-none d-lg-table-cell">Verifica</th>
|
<th class="d-none d-lg-table-cell">Verifica</th>
|
||||||
|
<th>Funzione</th>
|
||||||
<th>Responsabili</th>
|
<th>Responsabili</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th class="text-center" style="width:120px">Azioni</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-department="<?= htmlspecialchars($row['reparti'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-employees="<?= htmlspecialchars($row['responsabili'] ?? '', 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-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>
|
<td>
|
||||||
<?php if (!empty($row['subject_name'])): ?>
|
<?php if (!empty($row['subject_name'])): ?>
|
||||||
<?php
|
<?php
|
||||||
@@ -1014,6 +1006,7 @@ function getContrastTextColor($hexColor)
|
|||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<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>
|
<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): ?>
|
<?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 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><span class="text-nowrap"><?= $row['_dueFmt'] ?></span></td>
|
||||||
<td class="d-none d-lg-table-cell text-muted"><?= $row['_checkFmt'] ?></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>
|
<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']): ?><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; ?>
|
<?php if ($row['reparti'] && $row['responsabili']): ?><br><?php endif; ?>
|
||||||
@@ -1055,143 +1059,7 @@ function getContrastTextColor($hexColor)
|
|||||||
<?php include('../include/footer.php'); ?>
|
<?php include('../include/footer.php'); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Deadline Modal -->
|
<?php include __DIR__ . '/include/deadline_modal.php'; ?>
|
||||||
<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('../jsinclude.php'); ?>
|
<?php include('../jsinclude.php'); ?>
|
||||||
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
<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 fpDue = flatpickr('#filterDueRange', fpOpts);
|
||||||
var fpCheck = flatpickr('#filterCheckRange', 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 ---
|
// --- DataTables custom filters ---
|
||||||
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
|
$.fn.dataTable.ext.search.push(function(settings, data, dataIndex) {
|
||||||
if (settings.nTable.id !== 'deadlinesTable') return true;
|
if (settings.nTable.id !== 'deadlinesTable') return true;
|
||||||
@@ -1460,148 +1250,8 @@ function getContrastTextColor($hexColor)
|
|||||||
// Apply default filter on load
|
// Apply default filter on load
|
||||||
applyFiltersRefresh();
|
applyFiltersRefresh();
|
||||||
|
|
||||||
// --- Modal ---
|
|
||||||
var modal = new bootstrap.Modal(document.getElementById('deadlineModal'));
|
|
||||||
var form = document.getElementById('deadlineForm');
|
|
||||||
|
|
||||||
// Add
|
|
||||||
document.getElementById('btnAddDeadline').addEventListener('click', function() {
|
document.getElementById('btnAddDeadline').addEventListener('click', function() {
|
||||||
form.reset();
|
if (window.openDeadlineCreate) window.openDeadlineCreate();
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Edit with confirmation
|
// Edit with confirmation
|
||||||
@@ -1618,69 +1268,23 @@ function getContrastTextColor($hexColor)
|
|||||||
confirmButtonText: 'Sì, modifica',
|
confirmButtonText: 'Sì, modifica',
|
||||||
reverseButtons: true
|
reverseButtons: true
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
if (!result.isConfirmed) {
|
if (result.isConfirmed && window.openDeadlineEdit) {
|
||||||
return;
|
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
|
// Complete
|
||||||
$(document).on('click', '.btn-complete', function() {
|
function submitComplete(id, createNext, copyAttachments) {
|
||||||
var el = $(this).closest('[data-id]');
|
var fd = new FormData();
|
||||||
var id = el.data('id');
|
fd.append('id', id);
|
||||||
Swal.fire({
|
fd.append('create_next', createNext ? '1' : '0');
|
||||||
title: 'Completare la scadenza?',
|
fd.append('copy_attachments', copyAttachments ? '1' : '0');
|
||||||
icon: 'question',
|
|
||||||
showCancelButton: true,
|
fetch('scadenzario/ajax/complete_deadline.php', {
|
||||||
confirmButtonColor: '#198754',
|
method: 'POST',
|
||||||
cancelButtonText: 'Annulla',
|
body: fd
|
||||||
confirmButtonText: 'Completa'
|
})
|
||||||
}).then(function(result) {
|
|
||||||
if (result.isConfirmed) {
|
|
||||||
fetch('scadenzario/ajax/complete_deadline.php?id=' + id)
|
|
||||||
.then(function(r) {
|
.then(function(r) {
|
||||||
return r.json();
|
return r.json();
|
||||||
})
|
})
|
||||||
@@ -1690,11 +1294,16 @@ function getContrastTextColor($hexColor)
|
|||||||
icon: 'success',
|
icon: 'success',
|
||||||
title: 'Completata',
|
title: 'Completata',
|
||||||
text: data.message,
|
text: data.message,
|
||||||
timer: 2500,
|
timer: 1800,
|
||||||
showConfirmButton: false
|
showConfirmButton: false
|
||||||
})
|
})
|
||||||
.then(function() {
|
.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();
|
location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire('Errore', data.message, 'error');
|
Swal.fire('Errore', data.message, 'error');
|
||||||
@@ -1704,6 +1313,59 @@ function getContrastTextColor($hexColor)
|
|||||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
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',
|
||||||
|
reverseButtons: true
|
||||||
|
}).then(function(result) {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
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
|
// Stampa
|
||||||
function doStampa() {
|
function doStampa() {
|
||||||
var params = [];
|
var params = [];
|
||||||
@@ -1802,6 +1432,7 @@ function getContrastTextColor($hexColor)
|
|||||||
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
|
if (btnStampaMobile) btnStampaMobile.addEventListener('click', doStampa);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<?php include __DIR__ . '/include/deadline_modal_js.php'; ?>
|
||||||
</body>
|
</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 = [];
|
$where = [];
|
||||||
$params = [];
|
$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 ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
|
||||||
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
|
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
|
||||||
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
||||||
@@ -142,12 +151,13 @@ if ($fType === '' || $fType === 'initial') {
|
|||||||
|
|
||||||
/* Dropdown data */
|
/* Dropdown data */
|
||||||
$employees = $pdo->query("
|
$employees = $pdo->query("
|
||||||
SELECT id, first_name, last_name, employee_code
|
SELECT id, first_name, last_name, employee_code, department_id
|
||||||
FROM employees
|
FROM employees
|
||||||
ORDER BY last_name, first_name
|
ORDER BY last_name, first_name
|
||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$topics = $pdo->query("
|
$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);
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$departments = $pdo->query("
|
$departments = $pdo->query("
|
||||||
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
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://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></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>
|
<style>
|
||||||
body { font-size: 1.05rem; background: #f8fafc; }
|
body { font-size: 1.05rem; background: #f8fafc; }
|
||||||
@@ -234,10 +247,15 @@ function fmtDate(?string $d): string {
|
|||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<h5 class="mb-0">📚 Storico Formazione</h5>
|
<h5 class="mb-0">📚 Storico Formazione</h5>
|
||||||
|
<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'">
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
↩️ Torna alla Dashboard
|
↩️ Torna alla Dashboard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- COUNTERS -->
|
<!-- COUNTERS -->
|
||||||
@@ -316,6 +334,12 @@ function fmtDate(?string $d): string {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</form>
|
</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)): ?>
|
<?php if (empty($filtered)): ?>
|
||||||
<div class="text-center text-muted py-4">
|
<div class="text-center text-muted py-4">
|
||||||
Nessuna formazione corrispondente ai filtri.
|
Nessuna formazione corrispondente ai filtri.
|
||||||
@@ -326,6 +350,7 @@ function fmtDate(?string $d): string {
|
|||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle">
|
||||||
<thead style="background-color:#cfe3ff;">
|
<thead style="background-color:#cfe3ff;">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width:36px"><input type="checkbox" class="form-check-input" id="checkAll" title="Seleziona tutti"></th>
|
||||||
<th>Dipendente</th>
|
<th>Dipendente</th>
|
||||||
<th>Reparto</th>
|
<th>Reparto</th>
|
||||||
<th>Corso</th>
|
<th>Corso</th>
|
||||||
@@ -344,6 +369,11 @@ function fmtDate(?string $d): string {
|
|||||||
$days = $r['_status']['days'] ?? null;
|
$days = $r['_status']['days'] ?? null;
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($r['id'])): ?>
|
||||||
|
<input type="checkbox" class="form-check-input row-check" value="<?= (int)$r['id'] ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
|
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training" class="fw-semibold text-decoration-none">
|
||||||
<?= htmlspecialchars($fullName) ?>
|
<?= htmlspecialchars($fullName) ?>
|
||||||
@@ -388,7 +418,10 @@ function fmtDate(?string $d): string {
|
|||||||
?>
|
?>
|
||||||
<div class="tr-card">
|
<div class="tr-card">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-2 mb-1">
|
<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">
|
<a href="employee-profile.php?id=<?= (int)$r['employee_id'] ?>#tab-training">
|
||||||
<?= htmlspecialchars($fullName) ?>
|
<?= htmlspecialchars($fullName) ?>
|
||||||
</a>
|
</a>
|
||||||
@@ -425,6 +458,296 @@ function fmtDate(?string $d): string {
|
|||||||
<?php include('include/footer.php'); ?>
|
<?php include('include/footer.php'); ?>
|
||||||
</div>
|
</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'); ?>
|
<?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>
|
</body>
|
||||||
</html>
|
</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