From 408aaa701267f41704c4d4d65607a04e404e6a90 Mon Sep 17 00:00:00 2001
From: Jordan Ramos
Date: Thu, 14 May 2026 11:54:58 -0600
Subject: [PATCH] Add data management panel with delete vertical, rollback
upload, and reset all
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Backend:
- DELETE /api/compliance/vcl-multi/vertical/:code — wipe a single vertical
- DELETE /api/compliance/vcl-multi/upload/:uploadId — rollback most recent upload
- DELETE /api/compliance/vcl-multi/all — nuclear reset of all multi-vertical data
- All delete operations are Admin-only and audit-logged
Frontend:
- Manage button (red, Admin-only) in CCP Metrics header
- DataManagementPanel modal showing upload history grouped by vertical
- Per-vertical delete button
- Per-upload rollback button (most recent only)
- Reset All button with confirmation dialog
- Success/error messaging
---
backend/routes/vclMultiVertical.js | 247 ++++++++++++++++++
.../src/components/pages/CCPMetricsPage.js | 235 +++++++++++++++--
2 files changed, 461 insertions(+), 21 deletions(-)
diff --git a/backend/routes/vclMultiVertical.js b/backend/routes/vclMultiVertical.js
index 8f719d5..b62ab94 100644
--- a/backend/routes/vclMultiVertical.js
+++ b/backend/routes/vclMultiVertical.js
@@ -874,6 +874,253 @@ function createVCLMultiVerticalRouter(upload) {
}
});
+ // -----------------------------------------------------------------------
+ // Data Management — Delete / Rollback
+ // -----------------------------------------------------------------------
+
+ /**
+ * DELETE /vertical/:code
+ * Deletes all data for a single vertical — items, uploads, summary, and snapshots.
+ * Admin only.
+ *
+ * @method DELETE
+ * @route /vertical/:code
+ * @group Admin
+ * @param {string} code — vertical code to delete (e.g., "NTS_AEO")
+ *
+ * @response 200
+ * {
+ * message: string,
+ * deleted: { items: number, uploads: number }
+ * }
+ * @response 400 { error: string } — invalid vertical code
+ * @response 500 { error: string }
+ */
+ router.delete('/vertical/:code', requireGroup('Admin'), async (req, res) => {
+ const vertical = req.params.code;
+ if (!vertical || vertical.length > 100) return res.status(400).json({ error: 'Invalid vertical code' });
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ const { rows: uploadRows } = await client.query(
+ `SELECT id FROM compliance_uploads WHERE vertical = $1`, [vertical]
+ );
+ const uploadIds = uploadRows.map(r => r.id);
+
+ if (uploadIds.length > 0) {
+ await client.query(
+ `DELETE FROM vcl_multi_vertical_summary WHERE upload_id = ANY($1)`, [uploadIds]
+ );
+ }
+
+ const itemResult = await client.query(
+ `DELETE FROM compliance_items WHERE vertical = $1`, [vertical]
+ );
+ const uploadResult = await client.query(
+ `DELETE FROM compliance_uploads WHERE vertical = $1`, [vertical]
+ );
+ await client.query(
+ `DELETE FROM compliance_snapshots WHERE vertical = $1`, [vertical]
+ );
+
+ await client.query('COMMIT');
+
+ logAudit({
+ userId: req.user.id,
+ username: req.user.username,
+ action: 'vcl_multi_vertical_delete',
+ entityType: 'compliance_vertical',
+ entityId: vertical,
+ details: { items_deleted: itemResult.rowCount, uploads_deleted: uploadResult.rowCount },
+ ipAddress: req.ip,
+ });
+
+ res.json({
+ message: `Deleted all data for vertical "${vertical}"`,
+ deleted: { items: itemResult.rowCount, uploads: uploadResult.rowCount },
+ });
+ } catch (err) {
+ await client.query('ROLLBACK');
+ console.error('[VCL Multi] DELETE /vertical/:code error:', err.message);
+ res.status(500).json({ error: 'Failed to delete vertical data' });
+ } finally {
+ client.release();
+ }
+ });
+
+ /**
+ * DELETE /all
+ * Deletes ALL multi-vertical data — items, uploads, summary, and snapshots.
+ * Admin only. Nuclear reset.
+ *
+ * @method DELETE
+ * @route /all
+ * @group Admin
+ *
+ * @response 200
+ * {
+ * message: string,
+ * deleted: { items: number, uploads: number }
+ * }
+ * @response 500 { error: string }
+ */
+ router.delete('/all', requireGroup('Admin'), async (req, res) => {
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ await client.query(`DELETE FROM vcl_multi_vertical_summary`);
+ const itemResult = await client.query(`DELETE FROM compliance_items WHERE vertical IS NOT NULL`);
+ const uploadResult = await client.query(`DELETE FROM compliance_uploads WHERE vertical IS NOT NULL`);
+ await client.query(`DELETE FROM compliance_snapshots WHERE vertical IS NOT NULL AND vertical != ''`);
+
+ await client.query('COMMIT');
+
+ logAudit({
+ userId: req.user.id,
+ username: req.user.username,
+ action: 'vcl_multi_vertical_reset',
+ entityType: 'compliance_vertical',
+ entityId: 'ALL',
+ details: { items_deleted: itemResult.rowCount, uploads_deleted: uploadResult.rowCount },
+ ipAddress: req.ip,
+ });
+
+ res.json({
+ message: 'All multi-vertical data has been deleted',
+ deleted: { items: itemResult.rowCount, uploads: uploadResult.rowCount },
+ });
+ } catch (err) {
+ await client.query('ROLLBACK');
+ console.error('[VCL Multi] DELETE /all error:', err.message);
+ res.status(500).json({ error: 'Failed to reset data' });
+ } finally {
+ client.release();
+ }
+ });
+
+ /**
+ * DELETE /upload/:uploadId
+ * Rolls back a specific upload — deletes items introduced by it, reactivates items it resolved.
+ * Admin only. Must be the most recent upload for that vertical.
+ *
+ * @method DELETE
+ * @route /upload/:uploadId
+ * @group Admin
+ * @param {number} uploadId — numeric ID of the upload to roll back
+ *
+ * @response 200
+ * {
+ * message: string,
+ * rolled_back: {
+ * upload_id: number,
+ * vertical: string,
+ * filename: string,
+ * items_deleted: number,
+ * items_reactivated: number
+ * }
+ * }
+ * @response 400 { error: string } — invalid upload ID, not a multi-vertical upload, or not the most recent upload
+ * @response 404 { error: string } — upload not found
+ * @response 500 { error: string }
+ */
+ router.delete('/upload/:uploadId', requireGroup('Admin'), async (req, res) => {
+ const uploadId = parseInt(req.params.uploadId, 10);
+ if (isNaN(uploadId)) return res.status(400).json({ error: 'Invalid upload ID' });
+
+ try {
+ const { rows: uploadRows } = await pool.query(
+ `SELECT id, filename, report_date, vertical, new_count, resolved_count FROM compliance_uploads WHERE id = $1`,
+ [uploadId]
+ );
+ const upload = uploadRows[0];
+ if (!upload) return res.status(404).json({ error: 'Upload not found' });
+ if (!upload.vertical) return res.status(400).json({ error: 'This upload is not a multi-vertical upload' });
+
+ const { rows: latestRows } = await pool.query(
+ `SELECT id FROM compliance_uploads WHERE vertical = $1 ORDER BY id DESC LIMIT 1`,
+ [upload.vertical]
+ );
+ if (latestRows[0].id !== uploadId) {
+ return res.status(400).json({
+ error: `Only the most recent upload for "${upload.vertical}" can be rolled back`,
+ latest_upload_id: latestRows[0].id,
+ });
+ }
+
+ const client = await pool.connect();
+ try {
+ await client.query('BEGIN');
+
+ const deleteNew = await client.query(
+ `DELETE FROM compliance_items WHERE first_seen_upload_id = $1 AND upload_id = $1`, [uploadId]
+ );
+ const reactivate = await client.query(
+ `UPDATE compliance_items SET status = 'active', resolved_upload_id = NULL WHERE resolved_upload_id = $1`, [uploadId]
+ );
+
+ const { rows: prevRows } = await pool.query(
+ `SELECT id FROM compliance_uploads WHERE vertical = $1 AND id < $2 ORDER BY id DESC LIMIT 1`,
+ [upload.vertical, uploadId]
+ );
+ if (prevRows.length > 0) {
+ await client.query(
+ `UPDATE compliance_items SET upload_id = $1, seen_count = GREATEST(seen_count - 1, 1)
+ WHERE upload_id = $2 AND first_seen_upload_id != $2`,
+ [prevRows[0].id, uploadId]
+ );
+ }
+
+ await client.query(`DELETE FROM vcl_multi_vertical_summary WHERE upload_id = $1`, [uploadId]);
+ await client.query(`DELETE FROM compliance_uploads WHERE id = $1`, [uploadId]);
+
+ const currentMonth = new Date().toISOString().slice(0, 7);
+ await client.query(
+ `DELETE FROM compliance_snapshots WHERE vertical = $1 AND snapshot_month = $2`,
+ [upload.vertical, currentMonth]
+ );
+
+ await client.query('COMMIT');
+
+ logAudit({
+ userId: req.user.id,
+ username: req.user.username,
+ action: 'vcl_multi_upload_rollback',
+ entityType: 'compliance_upload',
+ entityId: String(uploadId),
+ details: {
+ vertical: upload.vertical,
+ filename: upload.filename,
+ items_deleted: deleteNew.rowCount,
+ items_reactivated: reactivate.rowCount,
+ },
+ ipAddress: req.ip,
+ });
+
+ res.json({
+ message: `Rolled back upload "${upload.filename}" for ${upload.vertical}`,
+ rolled_back: {
+ upload_id: uploadId,
+ vertical: upload.vertical,
+ filename: upload.filename,
+ items_deleted: deleteNew.rowCount,
+ items_reactivated: reactivate.rowCount,
+ },
+ });
+ } catch (err) {
+ await client.query('ROLLBACK');
+ throw err;
+ } finally {
+ client.release();
+ }
+ } catch (err) {
+ console.error('[VCL Multi] DELETE /upload/:uploadId error:', err.message);
+ res.status(500).json({ error: 'Failed to rollback upload: ' + err.message });
+ }
+ });
+
return router;
}
diff --git a/frontend/src/components/pages/CCPMetricsPage.js b/frontend/src/components/pages/CCPMetricsPage.js
index 651a338..9103a21 100644
--- a/frontend/src/components/pages/CCPMetricsPage.js
+++ b/frontend/src/components/pages/CCPMetricsPage.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
-import { Upload, Building2, ChevronLeft, Loader, AlertCircle, BarChart3 } from 'lucide-react';
+import { Upload, Building2, ChevronLeft, Loader, AlertCircle, BarChart3, Settings, Trash2, RotateCcw } from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import { PieChart, Pie, Cell, ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts';
import MultiVerticalUploadModal from './MultiVerticalUploadModal';
@@ -421,6 +421,166 @@ function MetricDeviceList({ vertical, metricId, onBack }) {
}
+// ---------------------------------------------------------------------------
+// Data Management Panel
+// ---------------------------------------------------------------------------
+function DataManagementPanel({ onClose, onDataChanged }) {
+ const [uploads, setUploads] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [actionLoading, setActionLoading] = useState(null);
+ const [confirmAction, setConfirmAction] = useState(null); // { type, label, action }
+ const [message, setMessage] = useState(null);
+
+ useEffect(() => {
+ fetchUploads();
+ }, []);
+
+ const fetchUploads = () => {
+ setLoading(true);
+ fetch(`${API_BASE}/compliance/vcl-multi/uploads`, { credentials: 'include' })
+ .then(r => r.json())
+ .then(data => { setUploads(data.uploads || []); setLoading(false); })
+ .catch(() => setLoading(false));
+ };
+
+ const handleDeleteVertical = async (vertical) => {
+ setActionLoading(vertical);
+ setMessage(null);
+ try {
+ const res = await fetch(`${API_BASE}/compliance/vcl-multi/vertical/${encodeURIComponent(vertical)}`, {
+ method: 'DELETE', credentials: 'include',
+ });
+ const data = await res.json();
+ if (!res.ok) { setMessage({ type: 'error', text: data.error }); }
+ else { setMessage({ type: 'success', text: data.message }); fetchUploads(); onDataChanged(); }
+ } catch (err) { setMessage({ type: 'error', text: err.message }); }
+ setActionLoading(null);
+ setConfirmAction(null);
+ };
+
+ const handleRollbackUpload = async (uploadId) => {
+ setActionLoading(uploadId);
+ setMessage(null);
+ try {
+ const res = await fetch(`${API_BASE}/compliance/vcl-multi/upload/${uploadId}`, {
+ method: 'DELETE', credentials: 'include',
+ });
+ const data = await res.json();
+ if (!res.ok) { setMessage({ type: 'error', text: data.error }); }
+ else { setMessage({ type: 'success', text: data.message }); fetchUploads(); onDataChanged(); }
+ } catch (err) { setMessage({ type: 'error', text: err.message }); }
+ setActionLoading(null);
+ setConfirmAction(null);
+ };
+
+ const handleDeleteAll = async () => {
+ setActionLoading('all');
+ setMessage(null);
+ try {
+ const res = await fetch(`${API_BASE}/compliance/vcl-multi/all`, {
+ method: 'DELETE', credentials: 'include',
+ });
+ const data = await res.json();
+ if (!res.ok) { setMessage({ type: 'error', text: data.error }); }
+ else { setMessage({ type: 'success', text: data.message }); fetchUploads(); onDataChanged(); }
+ } catch (err) { setMessage({ type: 'error', text: err.message }); }
+ setActionLoading(null);
+ setConfirmAction(null);
+ };
+
+ // Group uploads by vertical
+ const verticalGroups = {};
+ for (const u of uploads) {
+ if (!verticalGroups[u.vertical]) verticalGroups[u.vertical] = [];
+ verticalGroups[u.vertical].push(u);
+ }
+
+ return (
+
+
e.stopPropagation()}>
+
+
Manage Data
+ {/* ⚠️ CONVENTION: Use lucide-react icon instead of raw Unicode character */}
+
+
+
+ {/* Message */}
+ {message && (
+
+ {message.text}
+
+ )}
+
+ {/* Confirm dialog */}
+ {confirmAction && (
+
+
+ {confirmAction.label}
+
+
+
+
+
+
+ )}
+
+ {/* Delete All button */}
+
+
+
+
+ {loading &&
Loading uploads...
}
+
+ {!loading && uploads.length === 0 && (
+
No uploads yet.
+ )}
+
+ {/* Per-vertical sections */}
+ {!loading && Object.entries(verticalGroups).map(([vertical, vUploads]) => (
+
+
+ {vertical}
+
+
+ {vUploads.slice(0, 5).map((u, i) => (
+
+ {u.filename}
+ {u.report_date || '—'}
+ +{u.new_count || 0} / -{u.resolved_count || 0}
+ {i === 0 && (
+
+ )}
+
+ ))}
+ {vUploads.length > 5 && (
+
...and {vUploads.length - 5} more
+ )}
+
+ ))}
+
+
+ );
+}
+
// ---------------------------------------------------------------------------
// Main Page Component
// ---------------------------------------------------------------------------
@@ -431,6 +591,7 @@ export default function CCPMetricsPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showUpload, setShowUpload] = useState(false);
+ const [showManage, setShowManage] = useState(false);
// Drill-down state
const [selectedVertical, setSelectedVertical] = useState(null);
@@ -499,26 +660,50 @@ export default function CCPMetricsPage() {
{(isAdmin() || isEditor()) && (
-
+
+ {isAdmin() && (
+
+ )}
+
+
)}
@@ -597,6 +782,14 @@ export default function CCPMetricsPage() {
onUploadComplete={handleUploadComplete}
/>
)}
+
+ {/* Data Management Panel */}
+ {showManage && (
+ setShowManage(false)}
+ onDataChanged={fetchData}
+ />
+ )}
);
}