[FEAT] A4 Fasi 4.4 + 4.5 (A4 COMPLETA 5/5): scadenziario centralizzato review_schedule (mig.044; ReviewScheduleController list/create/update/delete/complete/sync) + stakeholder estesi clienti/partner GV.SC-02 (mig.045 suppliers.stakeholder_type, SupplyChainController::stakeholderMap). Pagine review-schedule.html + stakeholders.html (Bootstrap Italia/AGID). Workflow build+review adversariale (3 major risolti: 045 da DELIMITER -> ALTER bare runner-safe). help GV.SC-04/05 allineati al titolo canonico ACN. Cache-buster ?v=20260620.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-15 23:10:28 +02:00
co-authored by Claude Opus 4.8
parent c7d36c910f
commit c5b00ccd96
48 changed files with 1597 additions and 122 deletions
@@ -29,6 +29,7 @@ class SupplyChainController extends BaseController
$supplierId = Database::insert('suppliers', [
'organization_id' => $this->getCurrentOrgId(),
'name' => trim($this->getParam('name')),
'stakeholder_type' => $this->validateStakeholderType($this->getParam('stakeholder_type', 'supplier')),
'vat_number' => $this->getParam('vat_number'),
'contact_email' => $this->getParam('contact_email'),
'contact_name' => $this->getParam('contact_name'),
@@ -76,6 +77,11 @@ class SupplyChainController extends BaseController
}
}
// stakeholder_type (GV.SC-02): gestito a parte per validare l'enum
if ($this->hasParam('stakeholder_type')) {
$updates['stakeholder_type'] = $this->validateStakeholderType($this->getParam('stakeholder_type'));
}
if (!empty($updates)) {
Database::update('suppliers', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
$this->logAudit('supplier_updated', 'supplier', $id, $updates);
@@ -146,6 +152,58 @@ class SupplyChainController extends BaseController
]);
}
// ══════════════════════════════════════════════════════════════════════
// STAKEHOLDER ESTESI (A4 Fase 4.5) — GV.SC-02: fornitori + clienti + partner
// ══════════════════════════════════════════════════════════════════════
/**
* GET /api/supply-chain/stakeholder-map — mappa di sintesi degli stakeholder
* dell'org raggruppati per tipo (supplier/customer/partner). RIUSA la tabella
* suppliers (nessun duplicato): il dettaglio fornitori resta nel modulo Supply
* Chain. Ancoraggio GV.SC-02 (ruoli/responsabilita verso fornitori, clienti E
* partner) + GV.SC-04/05/07. Org-scoped, esclude i soft-deleted.
*/
public function stakeholderMap(): void
{
$this->requireOrgAccess();
$orgId = $this->getCurrentOrgId();
$rows = Database::fetchAll(
'SELECT id, name, stakeholder_type, service_type, criticality, status,
contact_email, contact_name, risk_score
FROM suppliers
WHERE organization_id = ? AND deleted_at IS NULL
ORDER BY stakeholder_type, criticality DESC, name',
[$orgId]
);
$groups = ['supplier' => [], 'customer' => [], 'partner' => []];
foreach ($rows as $r) {
$t = $r['stakeholder_type'] ?? 'supplier';
if (!isset($groups[$t])) {
$t = 'supplier';
}
$groups[$t][] = $r;
}
$this->jsonSuccess([
'groups' => $groups,
'counts' => [
'supplier' => count($groups['supplier']),
'customer' => count($groups['customer']),
'partner' => count($groups['partner']),
],
'total' => count($rows),
]);
}
/** Normalizza/valida il tipo stakeholder (default 'supplier' se non valido). */
private function validateStakeholderType($t): string
{
$t = strtolower((string) $t);
return in_array($t, ['supplier', 'customer', 'partner'], true) ? $t : 'supplier';
}
private function calculateSupplierRiskScore(array $responses): int
{
if (empty($responses)) return 0;