feedbackService = new FeedbackService(); } /** * POST /api/feedback/submit * Crea segnalazione + classifica con AI (10s timeout) */ public function submit(): void { $this->requireOrgAccess(); $body = $this->getJsonBody(); $tipo = trim($body['tipo'] ?? 'bug'); $priorita = trim($body['priorita'] ?? 'media'); $descrizione = trim($body['descrizione'] ?? ''); $pageUrl = trim($body['page_url'] ?? ''); $attachment = $body['attachment'] ?? null; // base64 string if (strlen($descrizione) < 10) { $this->jsonError('Descrizione troppo breve (minimo 10 caratteri).', 422); return; } $validTipi = ['bug', 'ux', 'funzionalita', 'domanda', 'altro']; if (!in_array($tipo, $validTipi)) { $tipo = 'bug'; } $validPriorita = ['alta', 'media', 'bassa']; if (!in_array($priorita, $validPriorita)) { $priorita = 'media'; } $report = $this->feedbackService->createReport( $this->currentOrgId, $this->getCurrentUserId(), $this->currentUser['email'], $this->currentOrgRole ?? 'employee', [ 'tipo' => $tipo, 'priorita' => $priorita, 'descrizione' => $descrizione, 'page_url' => $pageUrl, 'attachment' => $attachment, ] ); $this->jsonSuccess($report, 'Segnalazione inviata. L\'AI ha analizzato il problema.'); } /** * GET /api/feedback/mine * Ultime 20 segnalazioni dell'utente corrente per questa org */ public function mine(): void { $this->requireOrgAccess(); $reports = Database::fetchAll( 'SELECT id, tipo, priorita, descrizione, status, ai_categoria, ai_priorita, ai_risposta, ai_processed, created_at, updated_at FROM feedback_reports WHERE organization_id = ? AND user_id = ? ORDER BY created_at DESC LIMIT 20', [$this->currentOrgId, $this->getCurrentUserId()] ); $this->jsonSuccess($reports); } /** * GET /api/feedback/list * Lista admin con filtri opzionali: status, tipo, priorita */ public function list(): void { $this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']); $pagination = $this->getPagination(20); $status = $this->getParam('status', ''); $tipo = $this->getParam('tipo', ''); $priorita = $this->getParam('priorita', ''); $where = ['r.organization_id = ?']; $params = [$this->currentOrgId]; if ($status) { $where[] = 'r.status = ?'; $params[] = $status; } if ($tipo) { $where[] = 'r.tipo = ?'; $params[] = $tipo; } if ($priorita) { $where[] = 'r.priorita = ?'; $params[] = $priorita; } $whereSql = implode(' AND ', $where); $total = (int) Database::fetchOne( "SELECT COUNT(*) AS cnt FROM feedback_reports r WHERE {$whereSql}", $params )['cnt']; $countParams = $params; $params[] = $pagination['per_page']; $params[] = $pagination['offset']; $items = Database::fetchAll( "SELECT r.id, r.tipo, r.priorita, r.descrizione, r.status, r.page_url, r.nota_admin, r.ai_categoria, r.ai_priorita, r.ai_risposta, r.ai_processed, r.user_email, r.user_role, r.created_at, r.updated_at, u.name AS user_name FROM feedback_reports r LEFT JOIN users u ON u.id = r.user_id WHERE {$whereSql} ORDER BY FIELD(r.priorita,'alta','media','bassa'), FIELD(r.status,'aperto','in_lavorazione','risolto','chiuso'), r.created_at DESC LIMIT ? OFFSET ?", $params ); $this->jsonPaginated($items, $total, $pagination['page'], $pagination['per_page']); } /** * GET /api/feedback/{id} * Dettaglio segnalazione (admin o autore) */ public function show(int $id): void { $this->requireOrgAccess(); $report = Database::fetchOne( 'SELECT r.*, u.name AS user_name FROM feedback_reports r LEFT JOIN users u ON u.id = r.user_id WHERE r.id = ? AND r.organization_id = ?', [$id, $this->currentOrgId] ); if (!$report) { $this->jsonError('Segnalazione non trovata.', 404, 'NOT_FOUND'); return; } // Solo admin o autore possono vedere il dettaglio $isAdmin = in_array($this->currentOrgRole, ['org_admin', 'compliance_manager', 'auditor', 'super_admin']); if (!$isAdmin && $report['user_id'] != $this->getCurrentUserId()) { $this->jsonError('Accesso non autorizzato.', 403, 'ACCESS_DENIED'); return; } $this->jsonSuccess($report); } /** * PUT /api/feedback/{id} * Aggiorna status e/o nota_admin (solo admin) */ public function update(int $id): void { $this->requireOrgRole(['org_admin', 'compliance_manager']); $report = Database::fetchOne( 'SELECT id, status FROM feedback_reports WHERE id = ? AND organization_id = ?', [$id, $this->currentOrgId] ); if (!$report) { $this->jsonError('Segnalazione non trovata.', 404, 'NOT_FOUND'); return; } $body = $this->getJsonBody(); $fields = []; $params = []; $validStatus = ['aperto', 'in_lavorazione', 'risolto', 'chiuso']; if (isset($body['status']) && in_array($body['status'], $validStatus)) { $fields[] = 'status = ?'; $params[] = $body['status']; } if (isset($body['nota_admin'])) { $fields[] = 'nota_admin = ?'; $params[] = htmlspecialchars(trim($body['nota_admin']), ENT_QUOTES, 'UTF-8'); } if (empty($fields)) { $this->jsonError('Nessun campo da aggiornare.', 422); return; } $params[] = $id; $params[] = $this->currentOrgId; Database::query( 'UPDATE feedback_reports SET ' . implode(', ', $fields) . ' WHERE id = ? AND organization_id = ?', $params ); // Broadcast se risolto if (isset($body['status']) && $body['status'] === 'risolto') { $this->feedbackService->broadcastResolution($id, $this->currentOrgId); } $updated = Database::fetchOne( 'SELECT * FROM feedback_reports WHERE id = ?', [$id] ); $this->jsonSuccess($updated, 'Segnalazione aggiornata.'); } /** * POST /api/feedback/{id}/resolve * Chiusura manuale con password gate (utente dice "sì, risolto") */ public function resolve(int $id): void { $this->requireOrgAccess(); $body = $this->getJsonBody(); $password = trim($body['password'] ?? ''); if (empty($password)) { $this->jsonError('Password di conferma richiesta.', 422); return; } $resolvePassword = defined('FEEDBACK_RESOLVE_PASSWORD') ? FEEDBACK_RESOLVE_PASSWORD : ''; if (empty($resolvePassword) || $password !== $resolvePassword) { $this->jsonError('Password non corretta.', 403, 'INVALID_PASSWORD'); return; } $report = Database::fetchOne( 'SELECT id, status, user_id FROM feedback_reports WHERE id = ? AND organization_id = ?', [$id, $this->currentOrgId] ); if (!$report) { $this->jsonError('Segnalazione non trovata.', 404, 'NOT_FOUND'); return; } // Solo l'autore o un admin possono risolvere $isAdmin = in_array($this->currentOrgRole, ['org_admin', 'compliance_manager', 'super_admin']); if (!$isAdmin && $report['user_id'] != $this->getCurrentUserId()) { $this->jsonError('Accesso non autorizzato.', 403, 'ACCESS_DENIED'); return; } if ($report['status'] === 'chiuso') { $this->jsonError('Segnalazione già chiusa.', 422); return; } Database::query( 'UPDATE feedback_reports SET status = ? WHERE id = ?', ['risolto', $id] ); $this->feedbackService->broadcastResolution($id, $this->currentOrgId); $this->jsonSuccess(null, 'Ottimo! Segnalazione marcata come risolta. Grazie per il feedback.'); } }