Audito dashboard and OPERATOR permissions
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
CartesianGrid,
|
||||
} from "recharts";
|
||||
import { fetchMeters, type Meter } from "../api/meters";
|
||||
import { getAuditLogs, type AuditLog } from "../api/audit";
|
||||
import { getCurrentUserRole } from "../api/auth";
|
||||
import type { Page } from "../App";
|
||||
import grhWatermark from "../assets/images/grhWatermark.png";
|
||||
|
||||
@@ -46,6 +48,10 @@ export default function Home({
|
||||
setPage: (page: Page) => void;
|
||||
navigateToMetersWithProject: (projectName: string) => void;
|
||||
}) {
|
||||
|
||||
const userRole = useMemo(() => getCurrentUserRole(), []);
|
||||
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
|
||||
|
||||
/* ================= ORGANISMS (MOCK) ================= */
|
||||
|
||||
const organismsData: Organism[] = [
|
||||
@@ -111,6 +117,29 @@ export default function Home({
|
||||
return "CESPT TIJUANA";
|
||||
};
|
||||
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||
const [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
|
||||
|
||||
const loadAuditLogs = async () => {
|
||||
setLoadingAuditLogs(true);
|
||||
try {
|
||||
const response = await getAuditLogs({ limit: 10, page: 1 });
|
||||
setAuditLogs(response.data);
|
||||
} catch (err) {
|
||||
console.error("Error loading audit logs:", err);
|
||||
setAuditLogs([]);
|
||||
} finally {
|
||||
setLoadingAuditLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOperator) {
|
||||
loadAuditLogs();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const filteredMeters = useMemo(
|
||||
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism),
|
||||
[meters, selectedOrganism]
|
||||
@@ -146,46 +175,76 @@ export default function Home({
|
||||
return organismsData.filter((o) => o.name.toLowerCase().includes(q));
|
||||
}, [organismQuery]);
|
||||
|
||||
/* ================= MOCK ALERTS / HISTORY ================= */
|
||||
|
||||
const alerts: AlertItem[] = [
|
||||
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
|
||||
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
|
||||
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" },
|
||||
];
|
||||
|
||||
const history: HistoryItem[] = [
|
||||
{
|
||||
user: "GRH",
|
||||
action: "Creó un nuevo medidor",
|
||||
target: "SN001",
|
||||
time: "Hace 5 minutos",
|
||||
},
|
||||
{
|
||||
user: "CESPT",
|
||||
action: "Actualizó concentrador",
|
||||
target: "Planta 1",
|
||||
time: "Hace 20 minutos",
|
||||
},
|
||||
{
|
||||
user: "GRH",
|
||||
action: "Eliminó un usuario",
|
||||
target: "Juan Pérez",
|
||||
time: "Hace 1 hora",
|
||||
},
|
||||
{
|
||||
user: "CESPT",
|
||||
action: "Creó un payload",
|
||||
target: "Payload 12",
|
||||
time: "Hace 2 horas",
|
||||
},
|
||||
{
|
||||
user: "GRH",
|
||||
action: "Actualizó medidor",
|
||||
target: "SN002",
|
||||
time: "Hace 3 horas",
|
||||
},
|
||||
];
|
||||
const formatAuditAction = (action: string): string => {
|
||||
const actionMap: Record<string, string> = {
|
||||
CREATE: "creó",
|
||||
UPDATE: "actualizó",
|
||||
DELETE: "eliminó",
|
||||
READ: "consultó",
|
||||
LOGIN: "inició sesión",
|
||||
LOGOUT: "cerró sesión",
|
||||
EXPORT: "exportó",
|
||||
BULK_UPLOAD: "cargó masivamente",
|
||||
STATUS_CHANGE: "cambió estado de",
|
||||
PERMISSION_CHANGE: "cambió permisos de",
|
||||
};
|
||||
return actionMap[action] || action.toLowerCase();
|
||||
};
|
||||
|
||||
const formatTableName = (tableName: string): string => {
|
||||
const tableMap: Record<string, string> = {
|
||||
meters: "medidor",
|
||||
concentrators: "concentrador",
|
||||
projects: "proyecto",
|
||||
users: "usuario",
|
||||
roles: "rol",
|
||||
gateways: "gateway",
|
||||
devices: "dispositivo",
|
||||
readings: "lectura",
|
||||
webhooks: "webhook",
|
||||
};
|
||||
return tableMap[tableName] || tableName;
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "Hace un momento";
|
||||
if (diffMins < 60) return `Hace ${diffMins} minuto${diffMins > 1 ? "s" : ""}`;
|
||||
if (diffHours < 24) return `Hace ${diffHours} hora${diffHours > 1 ? "s" : ""}`;
|
||||
if (diffDays < 7) return `Hace ${diffDays} día${diffDays > 1 ? "s" : ""}`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
const formatAuditLog = (log: AuditLog): HistoryItem => {
|
||||
const action = formatAuditAction(log.action);
|
||||
const target = formatTableName(log.table_name);
|
||||
const recordInfo = log.description || log.record_id || target;
|
||||
|
||||
return {
|
||||
user: log.user_name || log.user_email || "Sistema",
|
||||
action: action,
|
||||
target: recordInfo,
|
||||
time: formatRelativeTime(log.created_at),
|
||||
};
|
||||
};
|
||||
|
||||
const history: HistoryItem[] = useMemo(
|
||||
() => auditLogs.map(formatAuditLog),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[auditLogs]
|
||||
);
|
||||
|
||||
/* ================= KPIs (Optional) ================= */
|
||||
|
||||
@@ -458,24 +517,35 @@ export default function Home({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Historial */}
|
||||
<div className="bg-white rounded-xl shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Historial Reciente</h2>
|
||||
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
|
||||
{history.map((h, i) => (
|
||||
<li key={i} className="py-2 flex items-start gap-3">
|
||||
<span className="text-gray-400 mt-1">•</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-700">
|
||||
<span className="font-semibold">{h.user}</span> {h.action}{" "}
|
||||
<span className="font-medium">{h.target}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">{h.time}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{!isOperator && (
|
||||
<div className="bg-white rounded-xl shadow p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Historial Reciente de Auditoría</h2>
|
||||
{loadingAuditLogs ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : history.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 text-center py-8">
|
||||
No hay registros de auditoría disponibles
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
|
||||
{history.map((h, i) => (
|
||||
<li key={i} className="py-2 flex items-start gap-3">
|
||||
<span className="text-gray-400 mt-1">•</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-700">
|
||||
<span className="font-semibold">{h.user}</span> {h.action}{" "}
|
||||
<span className="font-medium">{h.target}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">{h.time}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Últimas alertas */}
|
||||
<div className="bg-white rounded-xl shadow p-6">
|
||||
|
||||
Reference in New Issue
Block a user