Project id for user
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user