Operator permissions

This commit is contained in:
2026-02-03 00:28:58 -06:00
parent 5a062ce3a1
commit 6124bedb8a
6 changed files with 115 additions and 47 deletions

View File

@@ -65,8 +65,6 @@ export default function ConcentratorsSidebar({
<div> <div>
<p className="text-sm text-gray-500">Proyectos</p> <p className="text-sm text-gray-500">Proyectos</p>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
Tipo: <span className="font-semibold">{sampleViewLabel}</span>
{" • "}
Seleccionado:{" "} Seleccionado:{" "}
<span className="font-semibold"> <span className="font-semibold">
{projects.find((p) => p.id === selectedProject)?.name || "—"} {projects.find((p) => p.id === selectedProject)?.name || "—"}
@@ -142,10 +140,8 @@ export default function ConcentratorsSidebar({
) : projects.length === 0 ? ( ) : projects.length === 0 ? (
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{selectedMeterTypeId {selectedMeterTypeId
? `No hay proyectos con el tipo de toma seleccionado y concentradores ${sampleViewLabel}.` ? `No hay proyectos con el tipo de toma seleccionado.`
: sampleView === "GENERAL" : "No hay proyectos disponibles."
? "No hay proyectos con concentradores disponibles."
: `No hay proyectos con concentradores ${sampleViewLabel}.`
} }
</div> </div>
) : ( ) : (

View File

@@ -183,11 +183,6 @@ export function useConcentrators() {
let filteredProjects = visibleProjects; let filteredProjects = visibleProjects;
filteredProjects = filteredProjects.filter((projectId) => {
const count = counts[projectId] ?? 0;
return count > 0;
});
if (selectedMeterTypeId) { if (selectedMeterTypeId) {
filteredProjects = filteredProjects.filter((projectId) => { filteredProjects = filteredProjects.filter((projectId) => {
const project = projects.find((p) => p.id === projectId); const project = projects.find((p) => p.id === projectId);

View File

@@ -22,9 +22,14 @@ import {
type Pagination, type Pagination,
} from "../../api/readings"; } from "../../api/readings";
import { fetchProjects, type Project } from "../../api/projects"; import { fetchProjects, type Project } from "../../api/projects";
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
import ReadingsBulkUploadModal from "./ReadingsBulkUploadModal"; import ReadingsBulkUploadModal from "./ReadingsBulkUploadModal";
export default function ConsumptionPage() { export default function ConsumptionPage() {
const userRole = useMemo(() => getCurrentUserRole(), []);
const userProjectId = useMemo(() => getCurrentUserProjectId(), []);
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
const [readings, setReadings] = useState<MeterReading[]>([]); const [readings, setReadings] = useState<MeterReading[]>([]);
const [summary, setSummary] = useState<ConsumptionSummary | null>(null); const [summary, setSummary] = useState<ConsumptionSummary | null>(null);
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
@@ -49,12 +54,23 @@ export default function ConsumptionPage() {
const loadProjects = async () => { const loadProjects = async () => {
try { try {
const data = await fetchProjects(); const data = await fetchProjects();
setProjects(data);
let visibleProjects = data;
if (isOperator && userProjectId) {
visibleProjects = data.filter(p => p.id === userProjectId);
if (visibleProjects.length > 0) {
setSelectedProject(visibleProjects[0].id);
}
}
setProjects(visibleProjects);
} catch (error) { } catch (error) {
console.error("Error loading projects:", error); console.error("Error loading projects:", error);
} }
}; };
loadProjects(); loadProjects();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const loadData = async (page = 1) => { const loadData = async (page = 1) => {
@@ -167,7 +183,9 @@ export default function ConsumptionPage() {
}; };
const clearFilters = () => { const clearFilters = () => {
setSelectedProject(""); if (!isOperator) {
setSelectedProject("");
}
setStartDate(""); setStartDate("");
setEndDate(""); setEndDate("");
setSearch(""); setSearch("");
@@ -342,8 +360,9 @@ export default function ConsumptionPage() {
value={selectedProject} value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)} onChange={(e) => setSelectedProject(e.target.value)}
className="px-3 py-1.5 text-sm bg-white border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20" className="px-3 py-1.5 text-sm bg-white border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20"
disabled={isOperator}
> >
<option value="">Todos</option> {!isOperator && <option value="">Todos</option>}
{projects.map((p) => ( {projects.map((p) => (
<option key={p.id} value={p.id}> <option key={p.id} value={p.id}>
{p.name} {p.name}

View File

@@ -66,23 +66,22 @@ export default function MetersPage({
const [form, setForm] = useState<MeterInput>(emptyForm); const [form, setForm] = useState<MeterInput>(emptyForm);
const [errors, setErrors] = useState<Record<string, boolean>>({}); const [errors, setErrors] = useState<Record<string, boolean>>({});
// Projects cards (from real data)
const projectsDataReal: ProjectCard[] = useMemo(() => { const projectsDataReal: ProjectCard[] = useMemo(() => {
const baseRegion = "Baja California"; const baseRegion = "Baja California";
const baseContact = "Operaciones"; const baseContact = "Operaciones";
const baseLastSync = "Hace 1 h"; const baseLastSync = "Hace 1 h";
return m.allProjects.map((name) => ({ return m.filteredProjects.map((project) => ({
name, name: project.name,
region: baseRegion, region: baseRegion,
projects: 1, projects: 1,
meters: m.projectsCounts[name] ?? 0, meters: m.projectsCounts[project.name] ?? 0,
activeAlerts: 0, activeAlerts: 0,
lastSync: baseLastSync, lastSync: baseLastSync,
contact: baseContact, contact: baseContact,
status: "ACTIVO" as ProjectStatus, status: "ACTIVO" as ProjectStatus,
})); }));
}, [m.allProjects, m.projectsCounts]); }, [m.filteredProjects, m.projectsCounts]);
const sidebarProjects = projectsDataReal; const sidebarProjects = projectsDataReal;
@@ -217,8 +216,17 @@ export default function MetersPage({
projects={sidebarProjects} projects={sidebarProjects}
onRefresh={handleRefresh} onRefresh={handleRefresh}
refreshDisabled={false} refreshDisabled={false}
allProjects={m.allProjects} allProjects={m.allProjects.map(p => p.name)}
onResetSelection={resetSelection} onResetSelection={resetSelection}
meterTypes={m.meterTypes}
selectedMeterTypeId={m.selectedMeterTypeId}
onSelectMeterTypeId={(id: string) => {
m.setSelectedMeterTypeId(id);
m.setSelectedProject("");
setActiveMeter(null);
setSearch("");
}}
loadingMeterTypes={m.loadingMeterTypes}
/> />
{/* MAIN */} {/* MAIN */}

View File

@@ -1,8 +1,9 @@
// src/pages/meters/MetersSidebar.tsx // src/pages/meters/MetersSidebar.tsx
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { ChevronDown, RefreshCcw, Check } from "lucide-react"; import { RefreshCcw, Check } from "lucide-react";
import type React from "react"; import type React from "react";
import type { ProjectCard, TakeType } from "./MeterPage"; import type { ProjectCard, TakeType } from "./MeterPage";
import type { MeterType } from "../../api/meterTypes";
type Props = { type Props = {
loadingProjects: boolean; loadingProjects: boolean;
@@ -21,6 +22,11 @@ type Props = {
allProjects: string[]; allProjects: string[];
onResetSelection?: () => void; onResetSelection?: () => void;
meterTypes: MeterType[];
selectedMeterTypeId: string;
onSelectMeterTypeId: (id: string) => void;
loadingMeterTypes: boolean;
}; };
type TakeTypeOption = { key: TakeType; label: string }; type TakeTypeOption = { key: TakeType; label: string };
@@ -44,6 +50,10 @@ export default function MetersSidebar({
refreshDisabled, refreshDisabled,
allProjects, allProjects,
onResetSelection, onResetSelection,
meterTypes,
selectedMeterTypeId,
onSelectMeterTypeId,
loadingMeterTypes,
}: Props) { }: Props) {
const [typesMenuOpen, setTypesMenuOpen] = useState(false); const [typesMenuOpen, setTypesMenuOpen] = useState(false);
@@ -74,8 +84,6 @@ export default function MetersSidebar({
<div> <div>
<p className="text-sm text-gray-500">Proyectos</p> <p className="text-sm text-gray-500">Proyectos</p>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
Tipo: <span className="font-semibold">{takeTypeLabel}</span>
{" • "}
Seleccionado:{" "} Seleccionado:{" "}
<span className="font-semibold"> <span className="font-semibold">
{selectedProject || "—"} {selectedProject || "—"}
@@ -96,24 +104,6 @@ export default function MetersSidebar({
{/* ✅ Tipos de tomas (dropdown) — mismo UI que Concentrators */} {/* ✅ Tipos de tomas (dropdown) — mismo UI que Concentrators */}
<div className="mt-4 relative" ref={menuRef}> <div className="mt-4 relative" ref={menuRef}>
<button
type="button"
onClick={() => setTypesMenuOpen((v) => !v)}
disabled={loadingProjects}
className="w-full inline-flex items-center justify-between rounded-xl border border-gray-200 bg-white px-3 py-2 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 disabled:opacity-60"
>
<span className="flex items-center gap-2">
Tipos de tomas
<span className="text-xs font-semibold text-gray-500">
({takeTypeLabel})
</span>
</span>
<ChevronDown
size={16}
className={`${typesMenuOpen ? "rotate-180" : ""} transition`}
/>
</button>
{typesMenuOpen && ( {typesMenuOpen && (
<div className="absolute z-50 mt-2 w-full rounded-xl border border-gray-200 bg-white shadow-lg overflow-hidden"> <div className="absolute z-50 mt-2 w-full rounded-xl border border-gray-200 bg-white shadow-lg overflow-hidden">
@@ -155,13 +145,35 @@ export default function MetersSidebar({
)} )}
</div> </div>
<div className="mt-3">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">
Filtrar por Tipo de Toma del Proyecto
</label>
<select
value={selectedMeterTypeId}
onChange={(e) => onSelectMeterTypeId(e.target.value)}
disabled={loadingMeterTypes}
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed"
>
<option value="">Todos los tipos de toma</option>
{meterTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
</div>
{/* 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 ? ( {loadingProjects ? (
<div className="text-sm text-gray-500">Cargando proyectos...</div> <div className="text-sm text-gray-500">Cargando proyectos...</div>
) : projects.length === 0 ? ( ) : projects.length === 0 ? (
<div className="text-sm text-gray-500 text-center py-10"> <div className="text-sm text-gray-500 text-center py-10">
No se encontraron proyectos. {selectedMeterTypeId
? "No hay proyectos con el tipo de toma seleccionado."
: "No se encontraron proyectos."
}
</div> </div>
) : ( ) : (
projects.map((p) => { projects.map((p) => {

View File

@@ -1,7 +1,8 @@
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, type Project } from "../../api/projects";
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth"; import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
type UseMetersArgs = { type UseMetersArgs = {
initialProject?: string; initialProject?: string;
@@ -11,7 +12,8 @@ export function useMeters({ initialProject }: UseMetersArgs) {
const userRole = getCurrentUserRole(); const userRole = getCurrentUserRole();
const userProjectId = getCurrentUserProjectId(); const userProjectId = getCurrentUserProjectId();
const isAdmin = userRole?.toUpperCase() === 'ADMIN'; const isAdmin = userRole?.toUpperCase() === 'ADMIN';
const [allProjects, setAllProjects] = useState<string[]>([]);
const [allProjects, setAllProjects] = useState<Project[]>([]);
const [loadingProjects, setLoadingProjects] = useState(true); const [loadingProjects, setLoadingProjects] = useState(true);
const [selectedProject, setSelectedProject] = useState(initialProject || ""); const [selectedProject, setSelectedProject] = useState(initialProject || "");
@@ -20,12 +22,21 @@ export function useMeters({ initialProject }: UseMetersArgs) {
const [filteredMeters, setFilteredMeters] = useState<Meter[]>([]); const [filteredMeters, setFilteredMeters] = useState<Meter[]>([]);
const [loadingMeters, setLoadingMeters] = useState(true); const [loadingMeters, setLoadingMeters] = useState(true);
const [meterTypes, setMeterTypes] = useState<MeterType[]>([]);
const [selectedMeterTypeId, setSelectedMeterTypeId] = useState<string>("");
const [loadingMeterTypes, setLoadingMeterTypes] = useState(true);
const loadProjects = async () => { const loadProjects = async () => {
setLoadingProjects(true); setLoadingProjects(true);
try { try {
const projects = await fetchProjects(); const projects = await fetchProjects();
const projectNames = projects.map((p) => p.name);
setAllProjects(projectNames); let visibleProjects = projects;
if (!isAdmin && userProjectId) {
visibleProjects = projects.filter(p => p.id === userProjectId);
}
setAllProjects(visibleProjects);
setSelectedProject((prev) => { setSelectedProject((prev) => {
if (prev) return prev; if (prev) return prev;
@@ -36,6 +47,7 @@ export function useMeters({ initialProject }: UseMetersArgs) {
if (userProject) return userProject.name; if (userProject) return userProject.name;
} }
const projectNames = visibleProjects.map((p) => p.name);
return projectNames[0] ?? ""; return projectNames[0] ?? "";
}); });
} catch (error) { } catch (error) {
@@ -46,6 +58,19 @@ export function useMeters({ initialProject }: UseMetersArgs) {
} }
}; };
const loadMeterTypes = async () => {
setLoadingMeterTypes(true);
try {
const types = await fetchMeterTypes();
setMeterTypes(types);
} catch (error) {
console.error("Error loading meter types:", error);
setMeterTypes([]);
} finally {
setLoadingMeterTypes(false);
}
};
const loadMeters = async () => { const loadMeters = async () => {
setLoadingMeters(true); setLoadingMeters(true);
@@ -60,10 +85,10 @@ export function useMeters({ initialProject }: UseMetersArgs) {
} }
}; };
// init - load projects and meters
useEffect(() => { useEffect(() => {
loadProjects(); loadProjects();
loadMeters(); loadMeters();
loadMeterTypes();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -80,6 +105,13 @@ export function useMeters({ initialProject }: UseMetersArgs) {
setFilteredMeters(meters.filter((m) => m.projectName === selectedProject)); setFilteredMeters(meters.filter((m) => m.projectName === selectedProject));
}, [selectedProject, meters]); }, [selectedProject, meters]);
const filteredProjects = useMemo(() => {
if (!selectedMeterTypeId) {
return allProjects;
}
return allProjects.filter(p => p.meterTypeId === selectedMeterTypeId);
}, [allProjects, selectedMeterTypeId]);
const projectsCounts = useMemo(() => { const projectsCounts = useMemo(() => {
return meters.reduce<Record<string, number>>((acc, m) => { return meters.reduce<Record<string, number>>((acc, m) => {
const project = m.projectName ?? "SIN PROYECTO"; const project = m.projectName ?? "SIN PROYECTO";
@@ -92,13 +124,19 @@ export function useMeters({ initialProject }: UseMetersArgs) {
// loading // loading
loadingProjects, loadingProjects,
loadingMeters, loadingMeters,
loadingMeterTypes,
// projects // projects
allProjects, allProjects,
filteredProjects,
projectsCounts, projectsCounts,
selectedProject, selectedProject,
setSelectedProject, setSelectedProject,
meterTypes,
selectedMeterTypeId,
setSelectedMeterTypeId,
// data // data
meters, meters,
setMeters, setMeters,