Operator permissions
This commit is contained in:
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
if (!isOperator) {
|
||||||
setSelectedProject("");
|
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}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user