[FEAT] ISO-readiness: Audit interni (§9.2) + Riesame Direzione (§9.3) + Calendario scadenze (mig.055-056, v1.22.0)
Rilascio unico (3 moduli) costruito in flotta parallela + verifica avversariale. MODULO A — Audit interni (ISO 27001 §9.2, mig.055): internal_audits + internal_audit_items; codice AUD-NNN; checklist pre-popolata (clausole 4-10 + Annex A applicabili dal SoA); esiti per riga; apertura non conformità collegata a NCR/CAPA (source polimorfico); report HTML stampabile. InternalAuditController + internal-audits.html. MODULO B — Riesame di Direzione (ISO 27001 §9.3, mig.056): management_reviews + management_review_decisions; codice RD-AAAA-NN; INPUT aggregati automaticamente dai moduli (gather: NC/CAPA, audit interni, rischi+trattamenti, KPI/score, obiettivi, formazione, stakeholder, normative, scadenze), congelati nello snapshot all'approvazione; decisioni; verbale stampabile. ManagementReviewController + management-review.html. MODULO C — Calendario unico scadenze: aggregatore SOLA LETTURA (nessuna migrazione) di tutte le scadenze (incidenti/policy/rischi/NC-CAPA/formazione/stakeholder/audit/riesame/review_schedule), griglia mensile + lista + filtri + deep-link. CalendarController + calendario.html. Integrazione: router (3 controller+actionMap), api.js, sidebar V2+legacy, help.js (3 sezioni), i18n (IT+EN), review_schedule ENUM += internal_audit. v1.22.0 + sw cache + cache-buster 20260630. Verifica flotta (28 agenti, 4 dim + avversariale): 9 finding confermati, TUTTI corretti — MAJOR gatherRisks (risk_treatments.organization_id inesistente → JOIN risks); nextCode numerico (no dup >99/anno); footer report audit con etichetta "ISO buona prassi, non obbligo"; help 24→25 clausole; ARIA tab/calendario; focus modali; tasti su celle calendario; rimossi helper api morti. Smoke prod OK (calendario 18 eventi, audit AUD-001 25 item + report + audit→NCR + audit→calendario, riesame gather/decisione/approve/report, gatherRisks ora available); org 151 ripulita. Additivo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
18a4a0b509
commit
79ca72fba9
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Calendario unico delle scadenze (Modulo C)
|
||||
* ----------------------------------------------------------------------------
|
||||
* AGGREGATORE IN SOLA LETTURA. Nessuna tabella propria, nessuna migrazione: le
|
||||
* scadenze vivono gia' nei moduli sorgente, ciascuna col proprio ciclo di vita.
|
||||
* Questo controller fa una UNION LIVE di TUTTE le sorgenti e normalizza ogni
|
||||
* evento in una forma unica:
|
||||
* { source, type, title, date (YYYY-MM-DD), status, severity, entity_type,
|
||||
* entity_id, link }
|
||||
* dove status e' calcolato LIVE confrontando la data con oggi (come review_schedule):
|
||||
* - done = scadenza gia' assolta/chiusa
|
||||
* - overdue = data < oggi e non assolta
|
||||
* - due_soon = data entro 14 giorni da oggi
|
||||
* - upcoming = data oltre 14 giorni
|
||||
*
|
||||
* Generalizza il pattern di DashboardController::deadlines() su TUTTE le sorgenti.
|
||||
* Ogni sorgente e' racchiusa in un try/catch DIFENSIVO: alcune tabelle (Modulo A
|
||||
* internal_audits, Modulo B management_reviews) potrebbero non esistere ancora, e
|
||||
* il calendario deve continuare a funzionare comunque (degrada solo quella fonte).
|
||||
*
|
||||
* Multi-tenancy: OGNI query filtra organization_id (anti-IDOR). Sola lettura →
|
||||
* requireOrgAccess(). NOTE: Database::query/fetchAll/fetchOne; jsonSuccess/Error exit.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/BaseController.php';
|
||||
|
||||
class CalendarController extends BaseController
|
||||
{
|
||||
/** Soglia "in scadenza": eventi entro N giorni da oggi (inclusi) sono due_soon. */
|
||||
private const DUE_SOON_DAYS = 14;
|
||||
|
||||
/** Tipi noti (per la validazione del filtro ?types= e per la legenda frontend). */
|
||||
private const KNOWN_TYPES = [
|
||||
'incident_early_warning', 'incident_notification', 'incident_final_report',
|
||||
'policy_review', 'risk_treatment', 'control_review',
|
||||
'nc_target_close', 'capa_action', 'training_due',
|
||||
'stakeholder_activity', 'review_schedule',
|
||||
'internal_audit', 'management_review_decision',
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET /api/calendar/events?from=YYYY-MM-DD&to=YYYY-MM-DD&types=csv
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public function events(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
|
||||
[$from, $to] = $this->resolveRange();
|
||||
$typeFilter = $this->resolveTypeFilter();
|
||||
|
||||
$events = $this->collect($orgId, $from, $to);
|
||||
|
||||
// Filtro per tipo richiesto (applicato in PHP, gli eventi sono pochi per org).
|
||||
if ($typeFilter !== null) {
|
||||
$events = array_values(array_filter($events, static fn($e) => in_array($e['type'], $typeFilter, true)));
|
||||
}
|
||||
|
||||
// Ordina: overdue prima, poi per data crescente.
|
||||
usort($events, static function ($a, $b) {
|
||||
$oa = $a['status'] === 'overdue' ? 0 : 1;
|
||||
$ob = $b['status'] === 'overdue' ? 0 : 1;
|
||||
if ($oa !== $ob) { return $oa <=> $ob; }
|
||||
return strcmp($a['date'], $b['date']);
|
||||
});
|
||||
|
||||
$this->jsonSuccess([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'today' => date('Y-m-d'),
|
||||
'due_soon_days'=> self::DUE_SOON_DAYS,
|
||||
'known_types' => self::KNOWN_TYPES,
|
||||
'count' => count($events),
|
||||
'events' => $events,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET /api/calendar/summary — conteggi per stato (e per tipo)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public function summary(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
|
||||
[$from, $to] = $this->resolveRange();
|
||||
$events = $this->collect($orgId, $from, $to);
|
||||
|
||||
$byStatus = ['overdue' => 0, 'due_soon' => 0, 'upcoming' => 0, 'done' => 0];
|
||||
$byType = [];
|
||||
foreach ($events as $e) {
|
||||
$byStatus[$e['status']] = ($byStatus[$e['status']] ?? 0) + 1;
|
||||
$byType[$e['type']] = ($byType[$e['type']] ?? 0) + 1;
|
||||
}
|
||||
// Aperte = tutte tranne done (utile come badge dashboard).
|
||||
$open = $byStatus['overdue'] + $byStatus['due_soon'] + $byStatus['upcoming'];
|
||||
|
||||
$this->jsonSuccess([
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'today' => date('Y-m-d'),
|
||||
'total' => count($events),
|
||||
'open' => $open,
|
||||
'by_status' => $byStatus,
|
||||
'by_type' => $byType,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// RACCOLTA — UNION di tutte le sorgenti, ognuna difensiva (try/catch)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Raccoglie e normalizza tutti gli eventi della finestra [from,to] per l'org.
|
||||
* Ogni sorgente che fallisce (tabella/colonna mancante) viene saltata senza
|
||||
* far cadere l'intero calendario.
|
||||
*/
|
||||
private function collect(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$events = [];
|
||||
$sources = [
|
||||
'incidents' => fn() => $this->srcIncidents($orgId, $from, $to),
|
||||
'policies' => fn() => $this->srcPolicies($orgId, $from, $to),
|
||||
'risk_treatments' => fn() => $this->srcRiskTreatments($orgId, $from, $to),
|
||||
'controls' => fn() => $this->srcControls($orgId, $from, $to),
|
||||
'non_conformities' => fn() => $this->srcNonConformities($orgId, $from, $to),
|
||||
'capa_actions' => fn() => $this->srcCapaActions($orgId, $from, $to),
|
||||
'training' => fn() => $this->srcTraining($orgId, $from, $to),
|
||||
'stk_activities' => fn() => $this->srcStkActivities($orgId, $from, $to),
|
||||
'review_schedule' => fn() => $this->srcReviewSchedule($orgId, $from, $to),
|
||||
'internal_audits' => fn() => $this->srcInternalAudits($orgId, $from, $to),
|
||||
'mgmt_reviews' => fn() => $this->srcManagementReviewDecisions($orgId, $from, $to),
|
||||
];
|
||||
foreach ($sources as $rows) {
|
||||
try {
|
||||
foreach ($rows() as $ev) {
|
||||
if ($ev !== null) { $events[] = $ev; }
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Sorgente non disponibile (es. tabella di un modulo non ancora migrato):
|
||||
// si ignora senza compromettere le altre fonti.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return $events;
|
||||
}
|
||||
|
||||
/** Incidenti Art.23: early_warning / notification / final_report (datetime). */
|
||||
private function srcIncidents(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, title, severity,
|
||||
early_warning_due, early_warning_sent_at,
|
||||
notification_due, notification_sent_at,
|
||||
final_report_due, final_report_sent_at
|
||||
FROM incidents
|
||||
WHERE organization_id = ? AND is_significant = 1
|
||||
AND status NOT IN ("closed", "post_mortem")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
if ($r['early_warning_due'] && !$r['early_warning_sent_at']) {
|
||||
$out[] = $this->makeEvent('incidents', 'incident_early_warning',
|
||||
'Early Warning: ' . $r['title'], $r['early_warning_due'],
|
||||
'critical', 'incident', (int) $r['id'], '/incidents.html', $from, $to, false);
|
||||
}
|
||||
if ($r['notification_due'] && !$r['notification_sent_at']) {
|
||||
$out[] = $this->makeEvent('incidents', 'incident_notification',
|
||||
'Notifica CSIRT: ' . $r['title'], $r['notification_due'],
|
||||
'high', 'incident', (int) $r['id'], '/incidents.html', $from, $to, false);
|
||||
}
|
||||
if ($r['final_report_due'] && !$r['final_report_sent_at']) {
|
||||
$out[] = $this->makeEvent('incidents', 'incident_final_report',
|
||||
'Report finale: ' . $r['title'], $r['final_report_due'],
|
||||
'medium', 'incident', (int) $r['id'], '/incidents.html', $from, $to, false);
|
||||
}
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Revisione policy/procedure: policies.next_review_date (status != archived). */
|
||||
private function srcPolicies(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, title, next_review_date
|
||||
FROM policies
|
||||
WHERE organization_id = ? AND next_review_date IS NOT NULL
|
||||
AND status NOT IN ("archived")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = $this->makeEvent('policies', 'policy_review',
|
||||
'Revisione policy: ' . $r['title'], $r['next_review_date'],
|
||||
'medium', 'policy', (int) $r['id'], '/policies.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Trattamenti rischio: risk_treatments.due_date JOIN risks (org via risks). */
|
||||
private function srcRiskTreatments(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT rt.id, rt.due_date, r.title AS risk_title
|
||||
FROM risk_treatments rt
|
||||
JOIN risks r ON r.id = rt.risk_id
|
||||
WHERE r.organization_id = ? AND rt.status IN ("planned", "in_progress")
|
||||
AND rt.due_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = $this->makeEvent('risk_treatments', 'risk_treatment',
|
||||
'Trattamento rischio: ' . $r['risk_title'], $r['due_date'],
|
||||
'medium', 'risk_treatment', (int) $r['id'], '/risks.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Revisione controlli: compliance_controls.next_review_date (non verificati/in corso). */
|
||||
private function srcControls(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, control_code, title, next_review_date, status
|
||||
FROM compliance_controls
|
||||
WHERE organization_id = ? AND next_review_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$title = trim((string) $r['control_code'] . ' ' . (string) $r['title']);
|
||||
$out[] = $this->makeEvent('compliance_controls', 'control_review',
|
||||
'Revisione controllo: ' . $title, $r['next_review_date'],
|
||||
'medium', 'compliance_control', (int) $r['id'], '/reports.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Non conformita': non_conformities.target_close_date (status non chiuso). */
|
||||
private function srcNonConformities(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, ncr_code, title, target_close_date
|
||||
FROM non_conformities
|
||||
WHERE organization_id = ? AND target_close_date IS NOT NULL
|
||||
AND status NOT IN ("closed", "cancelled")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$title = trim((string) $r['ncr_code'] . ' ' . (string) $r['title']);
|
||||
$out[] = $this->makeEvent('non_conformities', 'nc_target_close',
|
||||
'Chiusura NC: ' . $title, $r['target_close_date'],
|
||||
'high', 'non_conformity', (int) $r['id'], '/reports.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Azioni correttive: capa_actions.due_date (status non completed/verified). */
|
||||
private function srcCapaActions(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, capa_code, title, due_date
|
||||
FROM capa_actions
|
||||
WHERE organization_id = ? AND due_date IS NOT NULL
|
||||
AND status NOT IN ("completed", "verified")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$title = trim((string) $r['capa_code'] . ' ' . (string) $r['title']);
|
||||
$out[] = $this->makeEvent('capa_actions', 'capa_action',
|
||||
'Azione correttiva: ' . $title, $r['due_date'],
|
||||
'medium', 'capa_action', (int) $r['id'], '/reports.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Formazione: training_assignments.due_date (status assigned/in_progress). */
|
||||
private function srcTraining(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT ta.id, ta.due_date, tc.title, u.full_name
|
||||
FROM training_assignments ta
|
||||
JOIN training_courses tc ON tc.id = ta.course_id
|
||||
LEFT JOIN users u ON u.id = ta.user_id
|
||||
WHERE ta.organization_id = ? AND ta.status IN ("assigned", "in_progress")
|
||||
AND ta.due_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$who = $r['full_name'] ? (' - ' . $r['full_name']) : '';
|
||||
$out[] = $this->makeEvent('training', 'training_due',
|
||||
'Formazione: ' . $r['title'] . $who, $r['due_date'],
|
||||
'low', 'training', (int) $r['id'], '/training.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attivita' stakeholder (C5.2): stk_activities.due_date (preferita) e, se assente,
|
||||
* planned_date. Esclude annullate/completate. due_date "assolta" se completed.
|
||||
*/
|
||||
private function srcStkActivities(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, title, type, planned_date, due_date, status
|
||||
FROM stk_activities
|
||||
WHERE organization_id = ? AND status NOT IN ("cancelled")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$done = ($r['status'] === 'completed');
|
||||
$date = $r['due_date'] ?: $r['planned_date'];
|
||||
if (!$date) { continue; }
|
||||
$label = ($r['due_date'] ? 'Scadenza attivita': 'Attivita pianificata') . ': ' . $r['title'];
|
||||
$out[] = $this->makeEvent('stk_activities', 'stakeholder_activity',
|
||||
$label, $date, 'medium', 'stk_activity', (int) $r['id'],
|
||||
'/stakeholder-activities.html', $from, $to, $done);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Scadenziario revisioni periodiche (A4 4.4): review_schedule.next_review_date. */
|
||||
private function srcReviewSchedule(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, title, entity_type, next_review_date, last_reviewed_at
|
||||
FROM review_schedule
|
||||
WHERE organization_id = ? AND next_review_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = $this->makeEvent('review_schedule', 'review_schedule',
|
||||
$r['title'], $r['next_review_date'],
|
||||
'medium', 'review_schedule', (int) $r['id'], '/review-schedule.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Audit interni (Modulo A, opzionale): internal_audits.planned_date. */
|
||||
private function srcInternalAudits(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, code, title, planned_date, status
|
||||
FROM internal_audits
|
||||
WHERE organization_id = ? AND planned_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$done = in_array($r['status'], ['completed', 'cancelled'], true);
|
||||
$title = trim((string) $r['code'] . ' ' . (string) $r['title']);
|
||||
$out[] = $this->makeEvent('internal_audits', 'internal_audit',
|
||||
'Audit interno: ' . $title, $r['planned_date'],
|
||||
'medium', 'internal_audit', (int) $r['id'], '/internal-audits.html', $from, $to, $done);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decisioni del riesame di direzione (Modulo B, opzionale):
|
||||
* management_review_decisions.due_date JOIN management_reviews per filtrare per org.
|
||||
*/
|
||||
private function srcManagementReviewDecisions(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT d.id, d.decision, d.due_date, d.status, mr.code AS review_code
|
||||
FROM management_review_decisions d
|
||||
JOIN management_reviews mr ON mr.id = d.review_id
|
||||
WHERE mr.organization_id = ? AND d.due_date IS NOT NULL',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$done = ($r['status'] === 'done');
|
||||
$txt = mb_substr((string) $r['decision'], 0, 120);
|
||||
$out[] = $this->makeEvent('management_reviews', 'management_review_decision',
|
||||
'Decisione riesame: ' . $txt, $r['due_date'],
|
||||
'medium', 'management_review_decision', (int) $r['id'],
|
||||
'/management-review.html', $from, $to, $done);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// HELPER
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalizza una riga in evento di calendario. Ritorna null se la data
|
||||
* (normalizzata a Y-m-d) cade fuori dalla finestra [from,to]. $done forza
|
||||
* lo status a 'done' (scadenza gia' assolta/chiusa), altrimenti lo calcola
|
||||
* LIVE rispetto a oggi.
|
||||
*/
|
||||
private function makeEvent(
|
||||
string $source, string $type, string $title, ?string $rawDate,
|
||||
string $severity, string $entityType, int $entityId, string $link,
|
||||
string $from, string $to, bool $done
|
||||
): ?array {
|
||||
if (!$rawDate) { return null; }
|
||||
$date = substr((string) $rawDate, 0, 10); // DATETIME o DATE → Y-m-d
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return null; }
|
||||
if ($date < $from || $date > $to) { return null; }
|
||||
|
||||
return [
|
||||
'source' => $source,
|
||||
'type' => $type,
|
||||
'title' => mb_substr($title, 0, 255),
|
||||
'date' => $date,
|
||||
'status' => $this->computeStatus($date, $done),
|
||||
'severity' => $severity,
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'link' => $link,
|
||||
];
|
||||
}
|
||||
|
||||
/** Stato LIVE: done | overdue | due_soon (<=N gg) | upcoming. */
|
||||
private function computeStatus(string $date, bool $done): string
|
||||
{
|
||||
if ($done) { return 'done'; }
|
||||
$today = new DateTimeImmutable('today');
|
||||
$d = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
|
||||
if (!$d) { return 'upcoming'; }
|
||||
$diffDays = (int) $today->diff($d)->format('%r%a');
|
||||
if ($diffDays < 0) { return 'overdue'; }
|
||||
if ($diffDays <= self::DUE_SOON_DAYS) { return 'due_soon'; }
|
||||
return 'upcoming';
|
||||
}
|
||||
|
||||
/**
|
||||
* Risolve la finestra [from,to]. Default ampio: dal 1° giorno di 1 mese fa al
|
||||
* 1° giorno di 13 mesi avanti (copre tutto cio' che ha senso vedere in un
|
||||
* calendario annuale). Param from/to opzionali sovrascrivono (validati Y-m-d).
|
||||
*/
|
||||
private function resolveRange(): array
|
||||
{
|
||||
$from = $this->validDate($this->getParam('from'));
|
||||
$to = $this->validDate($this->getParam('to'));
|
||||
if (!$from) { $from = (new DateTimeImmutable('first day of this month'))->modify('-1 month')->format('Y-m-d'); }
|
||||
if (!$to) { $to = (new DateTimeImmutable('first day of this month'))->modify('+13 months')->format('Y-m-d'); }
|
||||
if ($from > $to) { [$from, $to] = [$to, $from]; }
|
||||
return [$from, $to];
|
||||
}
|
||||
|
||||
/** Filtro tipi: ?types=a,b,c → solo i tipi noti; null = nessun filtro. */
|
||||
private function resolveTypeFilter(): ?array
|
||||
{
|
||||
$raw = $this->getParam('types');
|
||||
if ($raw === null || $raw === '') { return null; }
|
||||
$parts = array_filter(array_map('trim', explode(',', (string) $raw)), static fn($t) => $t !== '');
|
||||
$valid = array_values(array_intersect($parts, self::KNOWN_TYPES));
|
||||
return $valid ?: null;
|
||||
}
|
||||
|
||||
/** Valida una data Y-m-d; ritorna la stringa normalizzata o null. */
|
||||
private function validDate($v): ?string
|
||||
{
|
||||
if ($v === null || $v === '') { return null; }
|
||||
$s = trim((string) $v);
|
||||
$dt = DateTime::createFromFormat('Y-m-d', $s);
|
||||
return ($dt && $dt->format('Y-m-d') === $s) ? $s : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user