Operator permissions
This commit is contained in:
@@ -65,8 +65,6 @@ export default function ConcentratorsSidebar({
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Proyectos</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Tipo: <span className="font-semibold">{sampleViewLabel}</span>
|
||||
{" • "}
|
||||
Seleccionado:{" "}
|
||||
<span className="font-semibold">
|
||||
{projects.find((p) => p.id === selectedProject)?.name || "—"}
|
||||
@@ -142,10 +140,8 @@ export default function ConcentratorsSidebar({
|
||||
) : projects.length === 0 ? (
|
||||
<div className="text-sm text-gray-500">
|
||||
{selectedMeterTypeId
|
||||
? `No hay proyectos con el tipo de toma seleccionado y concentradores ${sampleViewLabel}.`
|
||||
: sampleView === "GENERAL"
|
||||
? "No hay proyectos con concentradores disponibles."
|
||||
: `No hay proyectos con concentradores ${sampleViewLabel}.`
|
||||
? `No hay proyectos con el tipo de toma seleccionado.`
|
||||
: "No hay proyectos disponibles."
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -183,11 +183,6 @@ export function useConcentrators() {
|
||||
|
||||
let filteredProjects = visibleProjects;
|
||||
|
||||
filteredProjects = filteredProjects.filter((projectId) => {
|
||||
const count = counts[projectId] ?? 0;
|
||||
return count > 0;
|
||||
});
|
||||
|
||||
if (selectedMeterTypeId) {
|
||||
filteredProjects = filteredProjects.filter((projectId) => {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
|
||||
@@ -22,9 +22,14 @@ import {
|
||||
type Pagination,
|
||||
} from "../../api/readings";
|
||||
import { fetchProjects, type Project } from "../../api/projects";
|
||||
import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
|
||||
import ReadingsBulkUploadModal from "./ReadingsBulkUploadModal";
|
||||
|
||||
export default function ConsumptionPage() {
|
||||
const userRole = useMemo(() => getCurrentUserRole(), []);
|
||||
const userProjectId = useMemo(() => getCurrentUserProjectId(), []);
|
||||
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
|
||||
|
||||
const [readings, setReadings] = useState<MeterReading[]>([]);
|
||||
const [summary, setSummary] = useState<ConsumptionSummary | null>(null);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
@@ -49,12 +54,23 @@ export default function ConsumptionPage() {
|
||||
const loadProjects = async () => {
|
||||
try {
|
||||
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) {
|
||||
console.error("Error loading projects:", error);
|
||||
}
|
||||
};
|
||||
loadProjects();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const loadData = async (page = 1) => {
|
||||
@@ -167,7 +183,9 @@ export default function ConsumptionPage() {
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedProject("");
|
||||
if (!isOperator) {
|
||||
setSelectedProject("");
|
||||
}
|
||||
setStartDate("");
|
||||
setEndDate("");
|
||||
setSearch("");
|
||||
@@ -342,8 +360,9 @@ export default function ConsumptionPage() {
|
||||
value={selectedProject}
|
||||
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"
|
||||
disabled={isOperator}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{!isOperator && <option value="">Todos</option>}
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
|
||||
@@ -66,23 +66,22 @@ export default function MetersPage({
|
||||
const [form, setForm] = useState<MeterInput>(emptyForm);
|
||||
const [errors, setErrors] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Projects cards (from real data)
|
||||
const projectsDataReal: ProjectCard[] = useMemo(() => {
|
||||
const baseRegion = "Baja California";
|
||||
const baseContact = "Operaciones";
|
||||
const baseLastSync = "Hace 1 h";
|
||||
|
||||
return m.allProjects.map((name) => ({
|
||||
name,
|
||||
return m.filteredProjects.map((project) => ({
|
||||
name: project.name,
|
||||
region: baseRegion,
|
||||
projects: 1,
|
||||
meters: m.projectsCounts[name] ?? 0,
|
||||
meters: m.projectsCounts[project.name] ?? 0,
|
||||
activeAlerts: 0,
|
||||
lastSync: baseLastSync,
|
||||
contact: baseContact,
|
||||
status: "ACTIVO" as ProjectStatus,
|
||||
}));
|
||||
}, [m.allProjects, m.projectsCounts]);
|
||||
}, [m.filteredProjects, m.projectsCounts]);
|
||||
|
||||
const sidebarProjects = projectsDataReal;
|
||||
|
||||
@@ -217,8 +216,17 @@ export default function MetersPage({
|
||||
projects={sidebarProjects}
|
||||
onRefresh={handleRefresh}
|
||||
refreshDisabled={false}
|
||||
allProjects={m.allProjects}
|
||||
allProjects={m.allProjects.map(p => p.name)}
|
||||
onResetSelection={resetSelection}
|
||||
meterTypes={m.meterTypes}
|
||||
selectedMeterTypeId={m.selectedMeterTypeId}
|
||||
onSelectMeterTypeId={(id: string) => {
|
||||
m.setSelectedMeterTypeId(id);
|
||||
m.setSelectedProject("");
|
||||
setActiveMeter(null);
|
||||
setSearch("");
|
||||
}}
|
||||
loadingMeterTypes={m.loadingMeterTypes}
|
||||
/>
|
||||
|
||||
{/* MAIN */}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// src/pages/meters/MetersSidebar.tsx
|
||||
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 { ProjectCard, TakeType } from "./MeterPage";
|
||||
import type { MeterType } from "../../api/meterTypes";
|
||||
|
||||
type Props = {
|
||||
loadingProjects: boolean;
|
||||
@@ -21,6 +22,11 @@ type Props = {
|
||||
|
||||
allProjects: string[];
|
||||
onResetSelection?: () => void;
|
||||
|
||||
meterTypes: MeterType[];
|
||||
selectedMeterTypeId: string;
|
||||
onSelectMeterTypeId: (id: string) => void;
|
||||
loadingMeterTypes: boolean;
|
||||
};
|
||||
|
||||
type TakeTypeOption = { key: TakeType; label: string };
|
||||
@@ -44,6 +50,10 @@ export default function MetersSidebar({
|
||||
refreshDisabled,
|
||||
allProjects,
|
||||
onResetSelection,
|
||||
meterTypes,
|
||||
selectedMeterTypeId,
|
||||
onSelectMeterTypeId,
|
||||
loadingMeterTypes,
|
||||
}: Props) {
|
||||
const [typesMenuOpen, setTypesMenuOpen] = useState(false);
|
||||
|
||||
@@ -74,8 +84,6 @@ export default function MetersSidebar({
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Proyectos</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Tipo: <span className="font-semibold">{takeTypeLabel}</span>
|
||||
{" • "}
|
||||
Seleccionado:{" "}
|
||||
<span className="font-semibold">
|
||||
{selectedProject || "—"}
|
||||
@@ -96,24 +104,6 @@ export default function MetersSidebar({
|
||||
|
||||
{/* ✅ Tipos de tomas (dropdown) — mismo UI que Concentrators */}
|
||||
<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 && (
|
||||
<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 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 */}
|
||||
<div className="mt-4 overflow-y-auto flex-1 space-y-3 pr-1">
|
||||
{loadingProjects ? (
|
||||
<div className="text-sm text-gray-500">Cargando proyectos...</div>
|
||||
) : projects.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
projects.map((p) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
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 { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
|
||||
|
||||
type UseMetersArgs = {
|
||||
initialProject?: string;
|
||||
@@ -11,7 +12,8 @@ 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<Project[]>([]);
|
||||
const [loadingProjects, setLoadingProjects] = useState(true);
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState(initialProject || "");
|
||||
@@ -20,12 +22,21 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
||||
const [filteredMeters, setFilteredMeters] = useState<Meter[]>([]);
|
||||
const [loadingMeters, setLoadingMeters] = useState(true);
|
||||
|
||||
const [meterTypes, setMeterTypes] = useState<MeterType[]>([]);
|
||||
const [selectedMeterTypeId, setSelectedMeterTypeId] = useState<string>("");
|
||||
const [loadingMeterTypes, setLoadingMeterTypes] = useState(true);
|
||||
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true);
|
||||
try {
|
||||
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) => {
|
||||
if (prev) return prev;
|
||||
@@ -36,6 +47,7 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
||||
if (userProject) return userProject.name;
|
||||
}
|
||||
|
||||
const projectNames = visibleProjects.map((p) => p.name);
|
||||
return projectNames[0] ?? "";
|
||||
});
|
||||
} 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 () => {
|
||||
setLoadingMeters(true);
|
||||
|
||||
@@ -60,10 +85,10 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
||||
}
|
||||
};
|
||||
|
||||
// init - load projects and meters
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
loadMeters();
|
||||
loadMeterTypes();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -80,6 +105,13 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
||||
setFilteredMeters(meters.filter((m) => m.projectName === selectedProject));
|
||||
}, [selectedProject, meters]);
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (!selectedMeterTypeId) {
|
||||
return allProjects;
|
||||
}
|
||||
return allProjects.filter(p => p.meterTypeId === selectedMeterTypeId);
|
||||
}, [allProjects, selectedMeterTypeId]);
|
||||
|
||||
const projectsCounts = useMemo(() => {
|
||||
return meters.reduce<Record<string, number>>((acc, m) => {
|
||||
const project = m.projectName ?? "SIN PROYECTO";
|
||||
@@ -92,13 +124,19 @@ export function useMeters({ initialProject }: UseMetersArgs) {
|
||||
// loading
|
||||
loadingProjects,
|
||||
loadingMeters,
|
||||
loadingMeterTypes,
|
||||
|
||||
// projects
|
||||
allProjects,
|
||||
filteredProjects,
|
||||
projectsCounts,
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
|
||||
meterTypes,
|
||||
selectedMeterTypeId,
|
||||
setSelectedMeterTypeId,
|
||||
|
||||
// data
|
||||
meters,
|
||||
setMeters,
|
||||
|
||||
Reference in New Issue
Block a user