Project id for user

This commit is contained in:
2026-02-02 23:55:41 -06:00
parent 9ab1beeef7
commit 5a062ce3a1
4 changed files with 136 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ import {
} 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 { getAuditLogs, type AuditLog } from "../api/audit";
import { fetchNotifications, type Notification } from "../api/notifications";
import { getCurrentUserRole } from "../api/auth"; 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";
@@ -175,11 +176,62 @@ export default function Home({
return organismsData.filter((o) => o.name.toLowerCase().includes(q)); return organismsData.filter((o) => o.name.toLowerCase().includes(q));
}, [organismQuery]); }, [organismQuery]);
const alerts: AlertItem[] = [ const [notifications, setNotifications] = useState<Notification[]>([]);
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" }, const [loadingNotifications, setLoadingNotifications] = useState(false);
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" }, const loadNotifications = async () => {
]; setLoadingNotifications(true);
try {
const response = await fetchNotifications({ limit: 10, page: 1 });
setNotifications(response.data);
} catch (err) {
console.error("Error loading notifications:", err);
setNotifications([]);
} finally {
setLoadingNotifications(false);
}
};
useEffect(() => {
if (!isOperator) {
loadNotifications();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formatNotificationType = (type: string): string => {
const typeMap: Record<string, string> = {
NEGATIVE_FLOW: "Flujo Negativo",
SYSTEM_ALERT: "Alerta del Sistema",
MAINTENANCE: "Mantenimiento",
};
return typeMap[type] || type;
};
const formatNotificationTime = (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 alerts: AlertItem[] = useMemo(
() =>
notifications.map((n) => ({
company: n.meter_serial_number || "Sistema",
type: formatNotificationType(n.notification_type),
time: formatNotificationTime(n.created_at),
})),
[notifications]
);
const formatAuditAction = (action: string): string => { const formatAuditAction = (action: string): string => {
const actionMap: Record<string, string> = { const actionMap: Record<string, string> = {
@@ -547,20 +599,35 @@ export default function Home({
</div> </div>
)} )}
{/* Últimas alertas */} {!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">Últimas Alertas</h2> <h2 className="text-lg font-semibold mb-4">Últimas Alertas</h2>
{loadingNotifications ? (
<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>
) : alerts.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-8">
No hay alertas disponibles
</p>
) : (
<ul className="divide-y divide-gray-200"> <ul className="divide-y divide-gray-200">
{alerts.map((a, i) => ( {alerts.map((a, i) => (
<li key={i} className="py-2 flex justify-between"> <li key={i} className="py-2 flex justify-between items-start">
<span> <div className="flex-1">
{a.company} - {a.type} <span className="text-sm text-gray-700">
<span className="font-semibold">{a.company}</span> - {a.type}
</span>
</div>
<span className="text-xs text-red-500 font-medium whitespace-nowrap ml-4">
{a.time}
</span> </span>
<span className="text-red-500 font-medium">{a.time}</span>
</li> </li>
))} ))}
</ul> </ul>
)}
</div> </div>
)}
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useMemo } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core"; import MaterialTable from "@material-table/core";
import { import {
@@ -10,8 +10,13 @@ import {
deleteProject as apiDeleteProject, deleteProject as apiDeleteProject,
} from "../../api/projects"; } from "../../api/projects";
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes"; import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
export default function ProjectsPage() { export default function ProjectsPage() {
const userRole = useMemo(() => getCurrentUserRole(), []);
const userProjectId = useMemo(() => getCurrentUserProjectId(), []);
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeProject, setActiveProject] = useState<Project | null>(null); const [activeProject, setActiveProject] = useState<Project | null>(null);
@@ -46,6 +51,18 @@ export default function ProjectsPage() {
} }
}; };
const visibleProjects = useMemo(() => {
if (!isOperator) {
return projects;
}
if (userProjectId) {
return projects.filter(p => p.id === userProjectId);
}
return [];
}, [projects, isOperator, userProjectId]);
const loadMeterTypesData = async () => { const loadMeterTypesData = async () => {
try { try {
const types = await fetchMeterTypes(); const types = await fetchMeterTypes();
@@ -130,7 +147,7 @@ export default function ProjectsPage() {
setShowModal(true); setShowModal(true);
}; };
const filtered = projects.filter((p) => const filtered = visibleProjects.filter((p) =>
`${p.name} ${p.areaName} ${p.description ?? ""}` `${p.name} ${p.areaName} ${p.description ?? ""}`
.toLowerCase() .toLowerCase()
.includes(search.toLowerCase()) .includes(search.toLowerCase())
@@ -152,13 +169,16 @@ export default function ProjectsPage() {
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
{!isOperator && (
<button <button
onClick={openCreateModal} onClick={openCreateModal}
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg" className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg"
> >
<Plus size={16} /> Agregar <Plus size={16} /> Agregar
</button> </button>
)}
{!isOperator && (
<button <button
onClick={openEditModal} onClick={openEditModal}
disabled={!activeProject} disabled={!activeProject}
@@ -166,7 +186,9 @@ export default function ProjectsPage() {
> >
<Pencil size={16} /> Editar <Pencil size={16} /> Editar
</button> </button>
)}
{!isOperator && (
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={!activeProject} disabled={!activeProject}
@@ -174,6 +196,7 @@ export default function ProjectsPage() {
> >
<Trash2 size={16} /> Eliminar <Trash2 size={16} /> Eliminar
</button> </button>
)}
<button <button
onClick={loadProjects} onClick={loadProjects}

View File

@@ -114,7 +114,6 @@ export async function getMe(req: AuthenticatedRequest, res: Response): Promise<v
const profile = await authService.getMe(userId); const profile = await authService.getMe(userId);
// Transform avatarUrl to avatar_url for frontend compatibility
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: { data: {
@@ -123,6 +122,7 @@ export async function getMe(req: AuthenticatedRequest, res: Response): Promise<v
name: profile.name, name: profile.name,
role: profile.role, role: profile.role,
avatar_url: profile.avatarUrl, avatar_url: profile.avatarUrl,
project_id: profile.projectId,
}, },
}); });
} catch (error) { } catch (error) {

View File

@@ -28,6 +28,7 @@ export interface UserProfile {
name: string; name: string;
role: string; role: string;
avatarUrl?: string | null; avatarUrl?: string | null;
projectId?: string | null;
createdAt: Date; createdAt: Date;
} }
@@ -230,9 +231,10 @@ export async function getMe(userId: string): Promise<UserProfile> {
name: string; name: string;
avatar_url: string | null; avatar_url: string | null;
role_name: string; role_name: string;
project_id: string | null;
created_at: Date; created_at: Date;
}>( }>(
`SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.created_at `SELECT u.id, u.email, u.name, u.avatar_url, r.name as role_name, u.project_id, u.created_at
FROM users u FROM users u
JOIN roles r ON u.role_id = r.id JOIN roles r ON u.role_id = r.id
WHERE u.id = $1 AND u.is_active = true WHERE u.id = $1 AND u.is_active = true
@@ -252,6 +254,7 @@ export async function getMe(userId: string): Promise<UserProfile> {
name: user.name, name: user.name,
role: user.role_name, role: user.role_name,
avatarUrl: user.avatar_url, avatarUrl: user.avatar_url,
projectId: user.project_id,
createdAt: user.created_at, createdAt: user.created_at,
}; };
} }