Audito dashboard and OPERATOR permissions
This commit is contained in:
@@ -34,9 +34,19 @@ export interface AuthUser {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
projectId?: string | null;
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
roleId: string;
|
||||||
|
roleName: string;
|
||||||
|
projectId?: string | null;
|
||||||
|
exp?: number;
|
||||||
|
iat?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login response combining tokens and user data
|
* Login response combining tokens and user data
|
||||||
*/
|
*/
|
||||||
@@ -329,3 +339,60 @@ export async function changePassword(
|
|||||||
newPassword,
|
newPassword,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's project ID from JWT token
|
||||||
|
* @returns The project ID or null if not assigned
|
||||||
|
*/
|
||||||
|
export function getCurrentUserProjectId(): string | null {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = parseJwtPayload(token) as JwtPayload | null;
|
||||||
|
return payload?.projectId || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's role name from JWT token
|
||||||
|
* @returns The role name or null
|
||||||
|
*/
|
||||||
|
export function getCurrentUserRole(): string | null {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = parseJwtPayload(token) as JwtPayload | null;
|
||||||
|
return payload?.roleName || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's ID from JWT token
|
||||||
|
* @returns The user ID or null
|
||||||
|
*/
|
||||||
|
export function getCurrentUserId(): string | null {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = parseJwtPayload(token) as JwtPayload | null;
|
||||||
|
return payload?.userId || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is an admin
|
||||||
|
* @returns boolean indicating if user is admin
|
||||||
|
*/
|
||||||
|
export function isCurrentUserAdmin(): boolean {
|
||||||
|
const role = getCurrentUserRole();
|
||||||
|
return role?.toUpperCase() === 'ADMIN';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
People,
|
People,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { Page } from "../../App";
|
import { Page } from "../../App";
|
||||||
|
import { getCurrentUserRole } from "../../api/auth";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setPage: (page: Page) => void;
|
setPage: (page: Page) => void;
|
||||||
@@ -19,6 +20,9 @@ export default function Sidebar({ setPage }: SidebarProps) {
|
|||||||
const [pinned, setPinned] = useState(false);
|
const [pinned, setPinned] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
|
const userRole = useMemo(() => getCurrentUserRole(), []);
|
||||||
|
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
|
||||||
|
|
||||||
const isExpanded = pinned || hovered;
|
const isExpanded = pinned || hovered;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,56 +119,59 @@ export default function Sidebar({ setPage }: SidebarProps) {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
{!isOperator && (
|
||||||
<button
|
<li>
|
||||||
onClick={() => setPage("auditoria")}
|
<button
|
||||||
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
onClick={() => setPage("auditoria")}
|
||||||
>
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
Auditoría
|
>
|
||||||
</button>
|
Auditoría
|
||||||
</li>
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* USERS MANAGEMENT */}
|
{!isOperator && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => isExpanded && setUsersOpen(!usersOpen)}
|
onClick={() => isExpanded && setUsersOpen(!usersOpen)}
|
||||||
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
|
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
|
||||||
>
|
>
|
||||||
<People className="w-5 h-5 shrink-0" />
|
<People className="w-5 h-5 shrink-0" />
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<span className="ml-3 flex-1 text-left">
|
<span className="ml-3 flex-1 text-left">
|
||||||
Users Management
|
Users Management
|
||||||
</span>
|
</span>
|
||||||
{usersOpen ? <ExpandLess /> : <ExpandMore />}
|
{usersOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && usersOpen && (
|
||||||
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("users")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("roles")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Roles
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
)}
|
)}
|
||||||
</button>
|
</li>
|
||||||
|
)}
|
||||||
{isExpanded && usersOpen && (
|
|
||||||
<ul className="mt-1 space-y-1 text-xs">
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage("users")}
|
|
||||||
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage("roles")}
|
|
||||||
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
|
||||||
>
|
|
||||||
Roles
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { Bell, User, LogOut } from "lucide-react";
|
import { Bell, User, LogOut } from "lucide-react";
|
||||||
import NotificationDropdown from "../NotificationDropdown";
|
import NotificationDropdown from "../NotificationDropdown";
|
||||||
import { useNotifications } from "../../hooks/useNotifications";
|
import { useNotifications } from "../../hooks/useNotifications";
|
||||||
|
import ProjectBadge from "./common/ProjectBadge";
|
||||||
|
|
||||||
interface TopMenuProps {
|
interface TopMenuProps {
|
||||||
page: string;
|
page: string;
|
||||||
@@ -81,7 +82,7 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* IZQUIERDA */}
|
{/* IZQUIERDA */}
|
||||||
<div className="flex items-center gap-2 text-sm font-medium opacity-90">
|
<div className="flex items-center gap-4 text-sm font-medium opacity-90">
|
||||||
{page !== "home" && (
|
{page !== "home" && (
|
||||||
<>
|
<>
|
||||||
<span className="capitalize">{page}</span>
|
<span className="capitalize">{page}</span>
|
||||||
@@ -93,6 +94,8 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ProjectBadge />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DERECHA */}
|
{/* DERECHA */}
|
||||||
|
|||||||
46
src/components/layout/common/ProjectBadge.tsx
Normal file
46
src/components/layout/common/ProjectBadge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Building2 } from "lucide-react";
|
||||||
|
import { getCurrentUserProjectId, getCurrentUserRole } from "../../../api/auth";
|
||||||
|
import { fetchProject } from "../../../api/projects";
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectBadge() {
|
||||||
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadProject = async () => {
|
||||||
|
const projectId = getCurrentUserProjectId();
|
||||||
|
const role = getCurrentUserRole();
|
||||||
|
|
||||||
|
if (role?.toUpperCase() !== 'ADMIN' && projectId) {
|
||||||
|
try {
|
||||||
|
const projectData = await fetchProject(projectId);
|
||||||
|
setProject(projectData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading user project:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadProject();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading || !project) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-lg text-sm">
|
||||||
|
<Building2 size={16} className="text-blue-600" />
|
||||||
|
<span className="text-blue-900 font-medium">
|
||||||
|
Proyecto: <span className="font-semibold">{project.name}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { fetchMeters, type Meter } from "../api/meters";
|
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 type { Page } from "../App";
|
||||||
import grhWatermark from "../assets/images/grhWatermark.png";
|
import grhWatermark from "../assets/images/grhWatermark.png";
|
||||||
|
|
||||||
@@ -46,6 +48,10 @@ export default function Home({
|
|||||||
setPage: (page: Page) => void;
|
setPage: (page: Page) => void;
|
||||||
navigateToMetersWithProject: (projectName: string) => void;
|
navigateToMetersWithProject: (projectName: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
|
const userRole = useMemo(() => getCurrentUserRole(), []);
|
||||||
|
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
|
||||||
|
|
||||||
/* ================= ORGANISMS (MOCK) ================= */
|
/* ================= ORGANISMS (MOCK) ================= */
|
||||||
|
|
||||||
const organismsData: Organism[] = [
|
const organismsData: Organism[] = [
|
||||||
@@ -111,6 +117,29 @@ export default function Home({
|
|||||||
return "CESPT TIJUANA";
|
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(
|
const filteredMeters = useMemo(
|
||||||
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism),
|
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism),
|
||||||
[meters, selectedOrganism]
|
[meters, selectedOrganism]
|
||||||
@@ -146,46 +175,76 @@ export default function Home({
|
|||||||
return organismsData.filter((o) => o.name.toLowerCase().includes(q));
|
return organismsData.filter((o) => o.name.toLowerCase().includes(q));
|
||||||
}, [organismQuery]);
|
}, [organismQuery]);
|
||||||
|
|
||||||
/* ================= MOCK ALERTS / HISTORY ================= */
|
|
||||||
|
|
||||||
const alerts: AlertItem[] = [
|
const alerts: AlertItem[] = [
|
||||||
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
|
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
|
||||||
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
|
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
|
||||||
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" },
|
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const history: HistoryItem[] = [
|
const formatAuditAction = (action: string): string => {
|
||||||
{
|
const actionMap: Record<string, string> = {
|
||||||
user: "GRH",
|
CREATE: "creó",
|
||||||
action: "Creó un nuevo medidor",
|
UPDATE: "actualizó",
|
||||||
target: "SN001",
|
DELETE: "eliminó",
|
||||||
time: "Hace 5 minutos",
|
READ: "consultó",
|
||||||
},
|
LOGIN: "inició sesión",
|
||||||
{
|
LOGOUT: "cerró sesión",
|
||||||
user: "CESPT",
|
EXPORT: "exportó",
|
||||||
action: "Actualizó concentrador",
|
BULK_UPLOAD: "cargó masivamente",
|
||||||
target: "Planta 1",
|
STATUS_CHANGE: "cambió estado de",
|
||||||
time: "Hace 20 minutos",
|
PERMISSION_CHANGE: "cambió permisos de",
|
||||||
},
|
};
|
||||||
{
|
return actionMap[action] || action.toLowerCase();
|
||||||
user: "GRH",
|
};
|
||||||
action: "Eliminó un usuario",
|
|
||||||
target: "Juan Pérez",
|
const formatTableName = (tableName: string): string => {
|
||||||
time: "Hace 1 hora",
|
const tableMap: Record<string, string> = {
|
||||||
},
|
meters: "medidor",
|
||||||
{
|
concentrators: "concentrador",
|
||||||
user: "CESPT",
|
projects: "proyecto",
|
||||||
action: "Creó un payload",
|
users: "usuario",
|
||||||
target: "Payload 12",
|
roles: "rol",
|
||||||
time: "Hace 2 horas",
|
gateways: "gateway",
|
||||||
},
|
devices: "dispositivo",
|
||||||
{
|
readings: "lectura",
|
||||||
user: "GRH",
|
webhooks: "webhook",
|
||||||
action: "Actualizó medidor",
|
};
|
||||||
target: "SN002",
|
return tableMap[tableName] || tableName;
|
||||||
time: "Hace 3 horas",
|
};
|
||||||
},
|
|
||||||
];
|
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) ================= */
|
/* ================= KPIs (Optional) ================= */
|
||||||
|
|
||||||
@@ -458,24 +517,35 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Historial */}
|
{!isOperator && (
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white rounded-xl shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Historial Reciente</h2>
|
<h2 className="text-lg font-semibold mb-4">Historial Reciente de Auditoría</h2>
|
||||||
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
|
{loadingAuditLogs ? (
|
||||||
{history.map((h, i) => (
|
<div className="flex items-center justify-center py-8">
|
||||||
<li key={i} className="py-2 flex items-start gap-3">
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<span className="text-gray-400 mt-1">•</span>
|
</div>
|
||||||
<div className="flex-1">
|
) : history.length === 0 ? (
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-500 text-center py-8">
|
||||||
<span className="font-semibold">{h.user}</span> {h.action}{" "}
|
No hay registros de auditoría disponibles
|
||||||
<span className="font-medium">{h.target}</span>
|
</p>
|
||||||
</p>
|
) : (
|
||||||
<p className="text-xs text-gray-400">{h.time}</p>
|
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
|
||||||
</div>
|
{history.map((h, i) => (
|
||||||
</li>
|
<li key={i} className="py-2 flex items-start gap-3">
|
||||||
))}
|
<span className="text-gray-400 mt-1">•</span>
|
||||||
</ul>
|
<div className="flex-1">
|
||||||
</div>
|
<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 */}
|
{/* Últimas alertas */}
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white rounded-xl shadow p-6">
|
||||||
|
|||||||
@@ -28,18 +28,8 @@ export type ProjectCard = {
|
|||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
type User = {
|
|
||||||
role: "SUPER_ADMIN" | "USER";
|
|
||||||
project?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ConcentratorsPage() {
|
export default function ConcentratorsPage() {
|
||||||
const currentUser: User = {
|
const c = useConcentrators();
|
||||||
role: "SUPER_ADMIN",
|
|
||||||
project: "CESPT",
|
|
||||||
};
|
|
||||||
|
|
||||||
const c = useConcentrators(currentUser);
|
|
||||||
|
|
||||||
const [typesMenuOpen, setTypesMenuOpen] = useState(false);
|
const [typesMenuOpen, setTypesMenuOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import {
|
|||||||
} from "../../api/concentrators";
|
} from "../../api/concentrators";
|
||||||
import { fetchProjects, type Project } from "../../api/projects";
|
import { fetchProjects, type Project } from "../../api/projects";
|
||||||
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
|
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
|
||||||
|
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
|
||||||
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
|
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
|
||||||
|
|
||||||
type User = {
|
export function useConcentrators() {
|
||||||
role: "SUPER_ADMIN" | "USER";
|
|
||||||
project?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useConcentrators(currentUser: User) {
|
const userRole = getCurrentUserRole();
|
||||||
|
const userProjectId = getCurrentUserProjectId();
|
||||||
|
const isAdmin = userRole?.toUpperCase() === 'ADMIN';
|
||||||
const [sampleView, setSampleView] = useState<SampleView>("GENERAL");
|
const [sampleView, setSampleView] = useState<SampleView>("GENERAL");
|
||||||
|
|
||||||
const [loadingProjects, setLoadingProjects] = useState(true);
|
const [loadingProjects, setLoadingProjects] = useState(true);
|
||||||
@@ -51,12 +51,12 @@ export function useConcentrators(currentUser: User) {
|
|||||||
|
|
||||||
const visibleProjects = useMemo(
|
const visibleProjects = useMemo(
|
||||||
() =>
|
() =>
|
||||||
currentUser.role === "SUPER_ADMIN"
|
isAdmin
|
||||||
? allProjects
|
? allProjects
|
||||||
: currentUser.project
|
: userProjectId
|
||||||
? [currentUser.project]
|
? [userProjectId]
|
||||||
: [],
|
: [],
|
||||||
[allProjects, currentUser.role, currentUser.project]
|
[allProjects, isAdmin, userProjectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadMeterTypes = async () => {
|
const loadMeterTypes = async () => {
|
||||||
@@ -82,8 +82,8 @@ export function useConcentrators(currentUser: User) {
|
|||||||
|
|
||||||
setSelectedProject((prev) => {
|
setSelectedProject((prev) => {
|
||||||
if (prev) return prev;
|
if (prev) return prev;
|
||||||
if (currentUser.role !== "SUPER_ADMIN" && currentUser.project) {
|
if (!isAdmin && userProjectId) {
|
||||||
return currentUser.project;
|
return userProjectId;
|
||||||
}
|
}
|
||||||
return projectIds[0] ?? "";
|
return projectIds[0] ?? "";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { fetchMeters, type Meter } from "../../api/meters";
|
import { fetchMeters, type Meter } from "../../api/meters";
|
||||||
import { fetchProjects } from "../../api/projects";
|
import { fetchProjects } from "../../api/projects";
|
||||||
|
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
|
||||||
|
|
||||||
type UseMetersArgs = {
|
type UseMetersArgs = {
|
||||||
initialProject?: string;
|
initialProject?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useMeters({ initialProject }: UseMetersArgs) {
|
export function useMeters({ initialProject }: UseMetersArgs) {
|
||||||
|
const userRole = getCurrentUserRole();
|
||||||
|
const userProjectId = getCurrentUserProjectId();
|
||||||
|
const isAdmin = userRole?.toUpperCase() === 'ADMIN';
|
||||||
const [allProjects, setAllProjects] = useState<string[]>([]);
|
const [allProjects, setAllProjects] = useState<string[]>([]);
|
||||||
const [loadingProjects, setLoadingProjects] = useState(true);
|
const [loadingProjects, setLoadingProjects] = useState(true);
|
||||||
|
|
||||||
@@ -26,6 +30,12 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
|||||||
setSelectedProject((prev) => {
|
setSelectedProject((prev) => {
|
||||||
if (prev) return prev;
|
if (prev) return prev;
|
||||||
if (initialProject) return initialProject;
|
if (initialProject) return initialProject;
|
||||||
|
|
||||||
|
if (!isAdmin && userProjectId) {
|
||||||
|
const userProject = projects.find(p => p.id === userProjectId);
|
||||||
|
if (userProject) return userProject.name;
|
||||||
|
}
|
||||||
|
|
||||||
return projectNames[0] ?? "";
|
return projectNames[0] ?? "";
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -62,7 +72,6 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
|||||||
if (initialProject) setSelectedProject(initialProject);
|
if (initialProject) setSelectedProject(initialProject);
|
||||||
}, [initialProject]);
|
}, [initialProject]);
|
||||||
|
|
||||||
// filter by project
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProject) {
|
if (!selectedProject) {
|
||||||
setFilteredMeters([]);
|
setFilteredMeters([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user