Changes
This commit is contained in:
@@ -3,6 +3,7 @@ import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
|
|||||||
import MaterialTable from "@material-table/core";
|
import MaterialTable from "@material-table/core";
|
||||||
import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users";
|
import { createUser, updateUser, deleteUser, getAllUsers, CreateUserInput, UpdateUserInput, User as ApiUser } from "../api/users";
|
||||||
import { getAllRoles, Role as ApiRole } from "../api/roles";
|
import { getAllRoles, Role as ApiRole } from "../api/roles";
|
||||||
|
import { fetchProjects, type Project } from "../api/projects";
|
||||||
|
|
||||||
interface RoleOption {
|
interface RoleOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -24,6 +25,7 @@ interface UserForm {
|
|||||||
email: string;
|
email: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
|
projectId?: string;
|
||||||
status: "ACTIVE" | "INACTIVE";
|
status: "ACTIVE" | "INACTIVE";
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -37,12 +39,14 @@ export default function UsersPage() {
|
|||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [roles, setRoles] = useState<RoleOption[]>([]);
|
const [roles, setRoles] = useState<RoleOption[]>([]);
|
||||||
const [modalRoles, setModalRoles] = useState<ApiRole[]>([]);
|
const [modalRoles, setModalRoles] = useState<ApiRole[]>([]);
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loadingUsers, setLoadingUsers] = useState(true);
|
const [loadingUsers, setLoadingUsers] = useState(true);
|
||||||
const [loadingModalRoles, setLoadingModalRoles] = useState(false);
|
const [loadingModalRoles, setLoadingModalRoles] = useState(false);
|
||||||
|
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const emptyUser: UserForm = { name: "", email: "", roleId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) };
|
const emptyUser: UserForm = { name: "", email: "", roleId: "", projectId: "", password: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) };
|
||||||
const [form, setForm] = useState<UserForm>(emptyUser);
|
const [form, setForm] = useState<UserForm>(emptyUser);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -96,6 +100,14 @@ export default function UsersPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedRole = modalRoles.find(r => r.id === form.roleId);
|
||||||
|
const isOperatorRole = selectedRole?.name === "OPERATOR";
|
||||||
|
|
||||||
|
if (isOperatorRole && !form.projectId) {
|
||||||
|
setError("Project is required for OPERATOR role");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!editingId && !form.password) {
|
if (!editingId && !form.password) {
|
||||||
setError("Password is required for new users");
|
setError("Password is required for new users");
|
||||||
return;
|
return;
|
||||||
@@ -181,12 +193,26 @@ export default function UsersPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchModalProjects = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingProjects(true);
|
||||||
|
const projectsData = await fetchProjects();
|
||||||
|
console.log('Projects fetched:', projectsData);
|
||||||
|
setProjects(projectsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch projects:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingProjects(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenAddModal = () => {
|
const handleOpenAddModal = () => {
|
||||||
setForm(emptyUser);
|
setForm(emptyUser);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
fetchModalRoles();
|
fetchModalRoles();
|
||||||
|
fetchModalProjects();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenEditModal = (user: User) => {
|
const handleOpenEditModal = (user: User) => {
|
||||||
@@ -195,6 +221,7 @@ export default function UsersPage() {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
roleId: user.roleId,
|
roleId: user.roleId,
|
||||||
|
projectId: "",
|
||||||
status: user.status,
|
status: user.status,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
password: ""
|
password: ""
|
||||||
@@ -202,6 +229,7 @@ export default function UsersPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
fetchModalRoles();
|
fetchModalRoles();
|
||||||
|
fetchModalProjects();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter users by search and selected role
|
// Filter users by search and selected role
|
||||||
@@ -327,7 +355,7 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
<select
|
<select
|
||||||
value={form.roleId}
|
value={form.roleId}
|
||||||
onChange={e => setForm({...form, roleId: e.target.value})}
|
onChange={e => setForm({...form, roleId: e.target.value, projectId: ""})}
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border px-3 py-2 rounded"
|
||||||
disabled={loadingModalRoles || saving}
|
disabled={loadingModalRoles || saving}
|
||||||
>
|
>
|
||||||
@@ -335,6 +363,18 @@ export default function UsersPage() {
|
|||||||
{modalRoles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
{modalRoles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{modalRoles.find(r => r.id === form.roleId)?.name === "OPERATOR" && (
|
||||||
|
<select
|
||||||
|
value={form.projectId || ""}
|
||||||
|
onChange={e => setForm({...form, projectId: e.target.value})}
|
||||||
|
className="w-full border px-3 py-2 rounded"
|
||||||
|
disabled={loadingProjects || saving}
|
||||||
|
>
|
||||||
|
<option value="">{loadingProjects ? "Loading projects..." : "Select Project *"}</option>
|
||||||
|
{projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})}
|
onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})}
|
||||||
className="w-full border rounded px-3 py-2"
|
className="w-full border rounded px-3 py-2"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export default function ConcentratorsPage() {
|
|||||||
const [errors, setErrors] = useState<Record<string, boolean>>({});
|
const [errors, setErrors] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const searchFiltered = useMemo(() => {
|
const searchFiltered = useMemo(() => {
|
||||||
if (!c.isGeneral) return [];
|
|
||||||
return c.filteredConcentrators.filter((row) => {
|
return c.filteredConcentrators.filter((row) => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
@@ -72,7 +71,7 @@ export default function ConcentratorsPage() {
|
|||||||
const sn = (row.serialNumber ?? "").toLowerCase();
|
const sn = (row.serialNumber ?? "").toLowerCase();
|
||||||
return name.includes(q) || sn.includes(q);
|
return name.includes(q) || sn.includes(q);
|
||||||
});
|
});
|
||||||
}, [c.filteredConcentrators, c.isGeneral, search]);
|
}, [c.filteredConcentrators, search]);
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const next: Record<string, boolean> = {};
|
const next: Record<string, boolean> = {};
|
||||||
@@ -86,7 +85,6 @@ export default function ConcentratorsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!c.isGeneral) return;
|
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -110,7 +108,6 @@ export default function ConcentratorsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!c.isGeneral) return;
|
|
||||||
if (!activeConcentrator) return;
|
if (!activeConcentrator) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -124,7 +121,7 @@ export default function ConcentratorsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
if (!c.isGeneral || !activeConcentrator) return;
|
if (!activeConcentrator) return;
|
||||||
|
|
||||||
setEditingId(activeConcentrator.id);
|
setEditingId(activeConcentrator.id);
|
||||||
setForm({
|
setForm({
|
||||||
@@ -142,8 +139,6 @@ export default function ConcentratorsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
if (!c.isGeneral) return;
|
|
||||||
|
|
||||||
setForm(getEmptyForm());
|
setForm(getEmptyForm());
|
||||||
setErrors({});
|
setErrors({});
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
@@ -174,7 +169,7 @@ export default function ConcentratorsPage() {
|
|||||||
}}
|
}}
|
||||||
projects={c.projectsData}
|
projects={c.projectsData}
|
||||||
onRefresh={c.loadConcentrators}
|
onRefresh={c.loadConcentrators}
|
||||||
refreshDisabled={c.loadingProjects || !c.isGeneral}
|
refreshDisabled={c.loadingProjects}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -186,10 +181,8 @@ export default function ConcentratorsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">Concentrator Management</h1>
|
<h1 className="text-2xl font-bold">Concentrator Management</h1>
|
||||||
<p className="text-sm text-blue-100">
|
<p className="text-sm text-blue-100">
|
||||||
{!c.isGeneral
|
{c.selectedProject
|
||||||
? `Vista: ${c.sampleViewLabel} (mock)`
|
? `Proyecto: ${c.selectedProject} • Tipo: ${c.sampleViewLabel}`
|
||||||
: c.selectedProject
|
|
||||||
? `Proyecto: ${c.selectedProject}`
|
|
||||||
: "Selecciona un proyecto desde el panel izquierdo"}
|
: "Selecciona un proyecto desde el panel izquierdo"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +190,7 @@ export default function ConcentratorsPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={openCreateModal}
|
onClick={openCreateModal}
|
||||||
disabled={!c.isGeneral || c.allProjects.length === 0}
|
disabled={c.allProjects.length === 0}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<Plus size={16} /> Agregar
|
<Plus size={16} /> Agregar
|
||||||
@@ -205,7 +198,7 @@ export default function ConcentratorsPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={openEditModal}
|
onClick={openEditModal}
|
||||||
disabled={!c.isGeneral || !activeConcentrator}
|
disabled={!activeConcentrator}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<Pencil size={16} /> Editar
|
<Pencil size={16} /> Editar
|
||||||
@@ -213,7 +206,7 @@ export default function ConcentratorsPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmOpen(true)}
|
onClick={() => setConfirmOpen(true)}
|
||||||
disabled={!c.isGeneral || !activeConcentrator}
|
disabled={!activeConcentrator}
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} /> Eliminar
|
<Trash2 size={16} /> Eliminar
|
||||||
@@ -221,7 +214,6 @@ export default function ConcentratorsPage() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={c.loadConcentrators}
|
onClick={c.loadConcentrators}
|
||||||
disabled={!c.isGeneral}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<RefreshCcw size={16} /> Actualizar
|
<RefreshCcw size={16} /> Actualizar
|
||||||
@@ -231,22 +223,20 @@ export default function ConcentratorsPage() {
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
|
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
|
||||||
placeholder={c.isGeneral ? "Buscar concentrador..." : "Search disabled in mock views"}
|
placeholder="Buscar concentrador..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
disabled={!c.isGeneral || !c.selectedProject}
|
disabled={!c.selectedProject}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={!c.isGeneral || !c.selectedProject ? "opacity-60 pointer-events-none" : ""}>
|
<div className={!c.selectedProject ? "opacity-60 pointer-events-none" : ""}>
|
||||||
<ConcentratorsTable
|
<ConcentratorsTable
|
||||||
isLoading={c.isGeneral ? c.loadingConcentrators : false}
|
isLoading={c.loadingConcentrators}
|
||||||
data={c.isGeneral ? searchFiltered : []}
|
data={searchFiltered}
|
||||||
activeRowId={activeConcentrator?.id}
|
activeRowId={activeConcentrator?.id}
|
||||||
onRowClick={(row) => setActiveConcentrator(row)}
|
onRowClick={(row) => setActiveConcentrator(row)}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
!c.isGeneral
|
!c.selectedProject
|
||||||
? `Vista "${c.sampleViewLabel}" está en modo mock (sin backend todavía).`
|
|
||||||
: !c.selectedProject
|
|
||||||
? "Selecciona un proyecto para ver los concentradores."
|
? "Selecciona un proyecto para ver los concentradores."
|
||||||
: c.loadingConcentrators
|
: c.loadingConcentrators
|
||||||
? "Cargando concentradores..."
|
? "Cargando concentradores..."
|
||||||
@@ -267,7 +257,6 @@ export default function ConcentratorsPage() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
onClose={() => setConfirmOpen(false)}
|
onClose={() => setConfirmOpen(false)}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
if (!c.isGeneral) return;
|
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
await handleDelete();
|
await handleDelete();
|
||||||
@@ -279,7 +268,7 @@ export default function ConcentratorsPage() {
|
|||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{showModal && c.isGeneral && (
|
{showModal && (
|
||||||
<ConcentratorsModal
|
<ConcentratorsModal
|
||||||
editingId={editingId}
|
editingId={editingId}
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -126,11 +126,14 @@ export default function ConcentratorsSidebar({
|
|||||||
|
|
||||||
{/* List */}
|
{/* List */}
|
||||||
<div className="mt-4 overflow-y-auto flex-1 space-y-3 pr-1">
|
<div className="mt-4 overflow-y-auto flex-1 space-y-3 pr-1">
|
||||||
{loadingProjects && sampleView === "GENERAL" ? (
|
{loadingProjects ? (
|
||||||
<div className="text-sm text-gray-500">Loading projects...</div>
|
<div className="text-sm text-gray-500">Loading projects...</div>
|
||||||
) : projects.length === 0 ? (
|
) : projects.length === 0 ? (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
No projects available. Please contact your administrator.
|
{sampleView === "GENERAL"
|
||||||
|
? "No projects available. Please contact your administrator."
|
||||||
|
: `No projects with ${sampleViewLabel} concentrators found.`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map((p) => {
|
projects.map((p) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
fetchConcentrators,
|
fetchConcentrators,
|
||||||
type Concentrator,
|
type Concentrator,
|
||||||
|
type ConcentratorType,
|
||||||
} from "../../api/concentrators";
|
} from "../../api/concentrators";
|
||||||
import { fetchProjects, type Project } from "../../api/projects";
|
import { fetchProjects, type Project } from "../../api/projects";
|
||||||
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
|
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
|
||||||
@@ -78,8 +79,6 @@ export function useConcentrators(currentUser: User) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadConcentrators = async () => {
|
const loadConcentrators = async () => {
|
||||||
if (!isGeneral) return;
|
|
||||||
|
|
||||||
setLoadingConcentrators(true);
|
setLoadingConcentrators(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -102,14 +101,8 @@ export function useConcentrators(currentUser: User) {
|
|||||||
|
|
||||||
// view changes
|
// view changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isGeneral) {
|
loadProjects();
|
||||||
loadProjects();
|
loadConcentrators();
|
||||||
loadConcentrators();
|
|
||||||
} else {
|
|
||||||
setLoadingProjects(false);
|
|
||||||
setLoadingConcentrators(false);
|
|
||||||
setSelectedProject("");
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [sampleView]);
|
}, [sampleView]);
|
||||||
|
|
||||||
@@ -121,25 +114,41 @@ export function useConcentrators(currentUser: User) {
|
|||||||
}
|
}
|
||||||
}, [visibleProjects, selectedProject, isGeneral]);
|
}, [visibleProjects, selectedProject, isGeneral]);
|
||||||
|
|
||||||
// filter by project
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGeneral) {
|
let filtered = concentrators;
|
||||||
setFilteredConcentrators([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
setFilteredConcentrators(
|
filtered = filtered.filter((c) => c.projectId === selectedProject);
|
||||||
concentrators.filter((c) => c.projectId === selectedProject)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setFilteredConcentrators(concentrators);
|
|
||||||
}
|
}
|
||||||
}, [selectedProject, concentrators, isGeneral]);
|
|
||||||
|
if (!isGeneral) {
|
||||||
|
const typeMap: Record<Exclude<SampleView, "GENERAL">, ConcentratorType> = {
|
||||||
|
LORA: "LORA",
|
||||||
|
LORAWAN: "LORAWAN",
|
||||||
|
GRANDES: "GRANDES",
|
||||||
|
};
|
||||||
|
const targetType = typeMap[sampleView as Exclude<SampleView, "GENERAL">];
|
||||||
|
filtered = filtered.filter((c) => c.type === targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredConcentrators(filtered);
|
||||||
|
}, [selectedProject, concentrators, isGeneral, sampleView]);
|
||||||
|
|
||||||
// sidebar cards (general)
|
// sidebar cards (general)
|
||||||
const projectsDataGeneral: ProjectCard[] = useMemo(() => {
|
const projectsDataGeneral: ProjectCard[] = useMemo(() => {
|
||||||
const counts = concentrators.reduce<Record<string, number>>((acc, c) => {
|
let concentratorsToCount = concentrators;
|
||||||
|
|
||||||
|
if (!isGeneral) {
|
||||||
|
const typeMap: Record<Exclude<SampleView, "GENERAL">, ConcentratorType> = {
|
||||||
|
LORA: "LORA",
|
||||||
|
LORAWAN: "LORAWAN",
|
||||||
|
GRANDES: "GRANDES",
|
||||||
|
};
|
||||||
|
const targetType = typeMap[sampleView as Exclude<SampleView, "GENERAL">];
|
||||||
|
concentratorsToCount = concentrators.filter((c) => c.type === targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = concentratorsToCount.reduce<Record<string, number>>((acc, c) => {
|
||||||
const project = c.projectId ?? "SIN PROYECTO";
|
const project = c.projectId ?? "SIN PROYECTO";
|
||||||
acc[project] = (acc[project] ?? 0) + 1;
|
acc[project] = (acc[project] ?? 0) + 1;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -165,70 +174,11 @@ export function useConcentrators(currentUser: User) {
|
|||||||
contact: baseContact,
|
contact: baseContact,
|
||||||
status: "ACTIVO" as const,
|
status: "ACTIVO" as const,
|
||||||
}));
|
}));
|
||||||
}, [concentrators, visibleProjects, projects]);
|
}, [concentrators, visibleProjects, projects, isGeneral, sampleView]);
|
||||||
|
|
||||||
// sidebar cards (mock)
|
|
||||||
const projectsDataMock: Record<Exclude<SampleView, "GENERAL">, ProjectCard[]> =
|
|
||||||
useMemo(
|
|
||||||
() => ({
|
|
||||||
LORA: [
|
|
||||||
{
|
|
||||||
id: "mock-lora-centro",
|
|
||||||
name: "LoRa - Zona Centro",
|
|
||||||
region: "Baja California",
|
|
||||||
projects: 1,
|
|
||||||
concentrators: 12,
|
|
||||||
activeAlerts: 1,
|
|
||||||
lastSync: "Hace 15 min",
|
|
||||||
contact: "Operaciones",
|
|
||||||
status: "ACTIVO",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "mock-lora-este",
|
|
||||||
name: "LoRa - Zona Este",
|
|
||||||
region: "Baja California",
|
|
||||||
projects: 1,
|
|
||||||
concentrators: 8,
|
|
||||||
activeAlerts: 0,
|
|
||||||
lastSync: "Hace 40 min",
|
|
||||||
contact: "Operaciones",
|
|
||||||
status: "ACTIVO",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
LORAWAN: [
|
|
||||||
{
|
|
||||||
id: "mock-lorawan-industrial",
|
|
||||||
name: "LoRaWAN - Industrial",
|
|
||||||
region: "Baja California",
|
|
||||||
projects: 1,
|
|
||||||
concentrators: 5,
|
|
||||||
activeAlerts: 0,
|
|
||||||
lastSync: "Hace 1 h",
|
|
||||||
contact: "Operaciones",
|
|
||||||
status: "ACTIVO",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
GRANDES: [
|
|
||||||
{
|
|
||||||
id: "mock-grandes-convenios",
|
|
||||||
name: "Grandes - Convenios",
|
|
||||||
region: "Baja California",
|
|
||||||
projects: 1,
|
|
||||||
concentrators: 3,
|
|
||||||
activeAlerts: 0,
|
|
||||||
lastSync: "Hace 2 h",
|
|
||||||
contact: "Operaciones",
|
|
||||||
status: "ACTIVO",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectsData: ProjectCard[] = useMemo(() => {
|
const projectsData: ProjectCard[] = useMemo(() => {
|
||||||
if (isGeneral) return projectsDataGeneral;
|
return projectsDataGeneral;
|
||||||
return projectsDataMock[sampleView as Exclude<SampleView, "GENERAL">];
|
}, [projectsDataGeneral]);
|
||||||
}, [isGeneral, projectsDataGeneral, projectsDataMock, sampleView]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// view
|
// view
|
||||||
|
|||||||
@@ -18,31 +18,6 @@ function methodToAction(method: string): AuditAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSection(path: string): string {
|
|
||||||
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
|
||||||
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
|
||||||
|
|
||||||
if (segments.length === 0) return 'general';
|
|
||||||
|
|
||||||
const sectionMapping: Record<string, string> = {
|
|
||||||
'auth': 'authentication',
|
|
||||||
'me': 'profile',
|
|
||||||
'users': 'user-management',
|
|
||||||
'roles': 'role-management',
|
|
||||||
'meters': 'meters',
|
|
||||||
'concentrators': 'concentrators',
|
|
||||||
'gateways': 'gateways',
|
|
||||||
'devices': 'devices',
|
|
||||||
'projects': 'projects',
|
|
||||||
'readings': 'readings',
|
|
||||||
'bulk-upload': 'bulk-operations',
|
|
||||||
'audit-logs': 'audit',
|
|
||||||
'webhooks': 'webhooks',
|
|
||||||
};
|
|
||||||
|
|
||||||
return sectionMapping[segments[0]] || segments[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractTableName(path: string): string {
|
function extractTableName(path: string): string {
|
||||||
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
||||||
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
||||||
@@ -52,48 +27,22 @@ function extractTableName(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseTableMapping: Record<string, string> = {
|
const baseTableMapping: Record<string, string> = {
|
||||||
'auth': 'users',
|
|
||||||
'me': 'users',
|
|
||||||
'users': 'users',
|
|
||||||
'roles': 'roles',
|
|
||||||
'meters': 'meters',
|
'meters': 'meters',
|
||||||
'concentrators': 'concentrators',
|
'concentrators': 'concentrators',
|
||||||
'gateways': 'gateways',
|
|
||||||
'devices': 'devices',
|
|
||||||
'projects': 'projects',
|
|
||||||
'readings': 'readings',
|
|
||||||
'webhooks': 'webhooks',
|
|
||||||
'bulk-upload': 'bulk_operations',
|
|
||||||
'audit-logs': 'audit_logs',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const operations = new Set([
|
|
||||||
'password',
|
|
||||||
'stats',
|
|
||||||
'health',
|
|
||||||
'template',
|
|
||||||
'summary',
|
|
||||||
'statistics',
|
|
||||||
'my-activity',
|
|
||||||
'record',
|
|
||||||
'refresh',
|
|
||||||
'logout',
|
|
||||||
'uplink',
|
|
||||||
'join',
|
|
||||||
'ack',
|
|
||||||
'dev-eui',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const nestedResourceMapping: Record<string, string> = {
|
const nestedResourceMapping: Record<string, string> = {
|
||||||
'meters/readings': 'readings',
|
|
||||||
'concentrators/meters': 'meters',
|
'concentrators/meters': 'meters',
|
||||||
'gateways/devices': 'devices',
|
|
||||||
'projects/meters': 'meters',
|
'projects/meters': 'meters',
|
||||||
'projects/concentrators': 'concentrators',
|
'projects/concentrators': 'concentrators',
|
||||||
};
|
};
|
||||||
|
|
||||||
const firstSegment = segments[0];
|
const firstSegment = segments[0];
|
||||||
const baseTable = baseTableMapping[firstSegment] || firstSegment;
|
const baseTable = baseTableMapping[firstSegment];
|
||||||
|
|
||||||
|
if (!baseTable) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
if (segments.length === 1) {
|
if (segments.length === 1) {
|
||||||
return baseTable;
|
return baseTable;
|
||||||
@@ -109,36 +58,9 @@ function extractTableName(path: string): string {
|
|||||||
return nestedResourceMapping[nestedKey];
|
return nestedResourceMapping[nestedKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operations.has(thirdSegment)) {
|
|
||||||
return baseTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseTableMapping[thirdSegment] || isPluralResourceName(thirdSegment)) {
|
|
||||||
return baseTableMapping[thirdSegment] || thirdSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseTable;
|
return baseTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstSegment === 'bulk-upload' && secondSegment) {
|
|
||||||
return baseTableMapping[secondSegment] || secondSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstSegment === 'audit-logs') {
|
|
||||||
if (secondSegment === 'record' && segments.length >= 4) {
|
|
||||||
return segments[3];
|
|
||||||
}
|
|
||||||
return 'audit_logs';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstSegment === 'auth') {
|
|
||||||
return 'users';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstSegment === 'devices' && secondSegment === 'dev-eui') {
|
|
||||||
return 'devices';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segments.length === 2 && isUUID(secondSegment)) {
|
if (segments.length === 2 && isUUID(secondSegment)) {
|
||||||
return baseTable;
|
return baseTable;
|
||||||
}
|
}
|
||||||
@@ -151,10 +73,6 @@ function isUUID(str: string): boolean {
|
|||||||
return uuidRegex.test(str) || /^\d+$/.test(str);
|
return uuidRegex.test(str) || /^\d+$/.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPluralResourceName(str: string): boolean {
|
|
||||||
return str.endsWith('s') || str.endsWith('ies') || str.includes('-');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractRecordId(req: Request): string | undefined {
|
function extractRecordId(req: Request): string | undefined {
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
return req.params.id;
|
return req.params.id;
|
||||||
@@ -166,23 +84,10 @@ function extractRecordId(req: Request): string | undefined {
|
|||||||
return match ? match[0] : undefined;
|
return match ? match[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXCLUDED_PATHS = [
|
|
||||||
'/api/auth/refresh',
|
|
||||||
'/api/audit-logs',
|
|
||||||
'/webhooks',
|
|
||||||
'/health',
|
|
||||||
];
|
|
||||||
|
|
||||||
function shouldExclude(path: string): boolean {
|
|
||||||
return EXCLUDED_PATHS.some(excluded => path.startsWith(excluded));
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDescription(
|
function generateDescription(
|
||||||
fullPath: string,
|
|
||||||
action: AuditAction,
|
action: AuditAction,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
recordId?: string,
|
recordId?: string
|
||||||
section?: string
|
|
||||||
): string {
|
): string {
|
||||||
const actionDescriptions: Record<AuditAction, string> = {
|
const actionDescriptions: Record<AuditAction, string> = {
|
||||||
'CREATE': 'Created',
|
'CREATE': 'Created',
|
||||||
@@ -198,55 +103,17 @@ function generateDescription(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tableLabels: Record<string, string> = {
|
const tableLabels: Record<string, string> = {
|
||||||
'users': 'user',
|
|
||||||
'roles': 'role',
|
|
||||||
'meters': 'meter',
|
'meters': 'meter',
|
||||||
'concentrators': 'concentrator',
|
'concentrators': 'concentrator',
|
||||||
'gateways': 'gateway',
|
|
||||||
'devices': 'device',
|
|
||||||
'projects': 'project',
|
|
||||||
'readings': 'reading',
|
|
||||||
'bulk_operations': 'bulk operation',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionLabel = actionDescriptions[action] || action;
|
const actionLabel = actionDescriptions[action] || action;
|
||||||
const tableLabel = tableLabels[tableName] || tableName;
|
const tableLabel = tableLabels[tableName] || tableName;
|
||||||
|
|
||||||
if (fullPath === '/api/me' || fullPath === '/me' || fullPath.endsWith('/auth/me')) {
|
|
||||||
return 'Viewed own profile';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath.includes('/users') && action === 'READ' && !recordId) {
|
|
||||||
return 'Viewed users list';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath.includes('/meters') && action === 'READ' && !recordId) {
|
|
||||||
return 'Viewed meters list';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath.includes('/concentrators') && action === 'READ' && !recordId) {
|
|
||||||
return 'Viewed concentrators list';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath.includes('/projects') && action === 'READ' && !recordId) {
|
|
||||||
return 'Viewed projects list';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath.includes('/readings') && action === 'READ' && !recordId) {
|
|
||||||
return 'Viewed readings list';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recordId) {
|
if (recordId) {
|
||||||
if (tableName === 'unknown' && section) {
|
|
||||||
return `${actionLabel} record in ${section} (ID: ${recordId.substring(0, 8)}...)`;
|
|
||||||
}
|
|
||||||
return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`;
|
return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tableName === 'unknown' && section) {
|
|
||||||
return `${actionLabel} in ${section} section`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${actionLabel} ${tableLabel}`;
|
return `${actionLabel} ${tableLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +123,6 @@ export function auditMiddleware(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): void {
|
): void {
|
||||||
|
|
||||||
if (shouldExclude(req.path)) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.path.startsWith('/api')) {
|
if (!req.path.startsWith('/api')) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
@@ -286,24 +148,18 @@ export function auditMiddleware(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = methodToAction(req.method);
|
|
||||||
const fullPath = (req.originalUrl || req.url).split('?')[0];
|
const fullPath = (req.originalUrl || req.url).split('?')[0];
|
||||||
const section = extractSection(fullPath);
|
|
||||||
const tableName = extractTableName(fullPath);
|
const tableName = extractTableName(fullPath);
|
||||||
|
|
||||||
|
const allowedTables = ['concentrators', 'meters'];
|
||||||
|
if (!allowedTables.includes(tableName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = methodToAction(req.method);
|
||||||
const recordId = extractRecordId(req);
|
const recordId = extractRecordId(req);
|
||||||
const success = res.statusCode >= 200 && res.statusCode < 400;
|
const success = res.statusCode >= 200 && res.statusCode < 400;
|
||||||
|
const description = generateDescription(action, tableName, recordId);
|
||||||
let description = generateDescription(fullPath, action, tableName, recordId, section);
|
|
||||||
|
|
||||||
if (fullPath.includes('/login')) {
|
|
||||||
description = 'User logged in successfully';
|
|
||||||
} else if (fullPath.includes('/logout')) {
|
|
||||||
description = 'User logged out';
|
|
||||||
} else if (fullPath.includes('/password')) {
|
|
||||||
description = 'Password changed';
|
|
||||||
} else if (fullPath.includes('/bulk-upload')) {
|
|
||||||
description = `Bulk upload for ${tableName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newValues: unknown = undefined;
|
let newValues: unknown = undefined;
|
||||||
let oldValues: unknown = undefined;
|
let oldValues: unknown = undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user