Meter tyoe logic for projects in concentrators page

This commit is contained in:
2026-02-02 19:01:02 -06:00
parent f921b20e15
commit b273003366
3 changed files with 111 additions and 30 deletions

View File

@@ -170,6 +170,15 @@ export default function ConcentratorsPage() {
projects={c.projectsData} projects={c.projectsData}
onRefresh={c.loadConcentrators} onRefresh={c.loadConcentrators}
refreshDisabled={c.loadingProjects} refreshDisabled={c.loadingProjects}
meterTypes={c.meterTypes}
selectedMeterTypeId={c.selectedMeterTypeId}
onSelectMeterTypeId={(id: string) => {
c.setSelectedMeterTypeId(id);
c.setSelectedProject("");
setActiveConcentrator(null);
setSearch("");
}}
loadingMeterTypes={c.loadingMeterTypes}
/> />
</aside> </aside>
@@ -181,7 +190,9 @@ 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.selectedProject {c.projectsData.length === 0 && c.selectedMeterTypeId
? `No hay proyectos disponibles con el filtro seleccionado`
: c.selectedProject
? `Proyecto: ${c.selectedProject} • Tipo: ${c.sampleViewLabel}` ? `Proyecto: ${c.selectedProject} • Tipo: ${c.sampleViewLabel}`
: "Selecciona un proyecto desde el panel izquierdo"} : "Selecciona un proyecto desde el panel izquierdo"}
</p> </p>
@@ -190,7 +201,7 @@ export default function ConcentratorsPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={openCreateModal} onClick={openCreateModal}
disabled={c.allProjects.length === 0} disabled={c.allProjects.length === 0 || c.projectsData.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
@@ -214,6 +225,7 @@ export default function ConcentratorsPage() {
<button <button
onClick={c.loadConcentrators} onClick={c.loadConcentrators}
disabled={c.projectsData.length === 0}
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
@@ -222,11 +234,11 @@ export default function ConcentratorsPage() {
</div> </div>
<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 disabled:opacity-60 disabled:cursor-not-allowed"
placeholder="Buscar concentrador..." placeholder="Buscar concentrador..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
disabled={!c.selectedProject} disabled={!c.selectedProject || c.projectsData.length === 0}
/> />
<div className={!c.selectedProject ? "opacity-60 pointer-events-none" : ""}> <div className={!c.selectedProject ? "opacity-60 pointer-events-none" : ""}>
@@ -236,7 +248,11 @@ export default function ConcentratorsPage() {
activeRowId={activeConcentrator?.id} activeRowId={activeConcentrator?.id}
onRowClick={(row) => setActiveConcentrator(row)} onRowClick={(row) => setActiveConcentrator(row)}
emptyMessage={ emptyMessage={
!c.selectedProject c.projectsData.length === 0 && c.selectedMeterTypeId
? `No hay proyectos con el tipo de toma seleccionado (${
c.meterTypes.find((mt) => mt.id === c.selectedMeterTypeId)?.name ?? "desconocido"
}) que tengan concentradores.`
: !c.selectedProject
? "Selecciona un proyecto para ver los concentradores." ? "Selecciona un proyecto para ver los concentradores."
: c.loadingConcentrators : c.loadingConcentrators
? "Cargando concentradores..." ? "Cargando concentradores..."

View File

@@ -1,7 +1,8 @@
// src/pages/concentrators/ConcentratorsSidebar.tsx // src/pages/concentrators/ConcentratorsSidebar.tsx
import { useMemo } from "react"; import { useMemo } from "react";
import { ChevronDown, Check, RefreshCcw } from "lucide-react"; import { Check, RefreshCcw } from "lucide-react";
import type { ProjectCard, SampleView } from "./ConcentratorsPage"; import type { ProjectCard, SampleView } from "./ConcentratorsPage";
import type { MeterType } from "../../api/meterTypes";
type Props = { type Props = {
loadingProjects: boolean; loadingProjects: boolean;
@@ -23,6 +24,11 @@ type Props = {
onRefresh: () => void; onRefresh: () => void;
refreshDisabled: boolean; refreshDisabled: boolean;
meterTypes: MeterType[];
selectedMeterTypeId: string;
onSelectMeterTypeId: (id: string) => void;
loadingMeterTypes: boolean;
}; };
export default function ConcentratorsSidebar({ export default function ConcentratorsSidebar({
@@ -30,13 +36,16 @@ export default function ConcentratorsSidebar({
sampleView, sampleView,
sampleViewLabel, sampleViewLabel,
typesMenuOpen, typesMenuOpen,
setTypesMenuOpen,
onChangeSampleView, onChangeSampleView,
selectedProject, selectedProject,
onSelectProject, onSelectProject,
projects, projects,
onRefresh, onRefresh,
refreshDisabled, refreshDisabled,
meterTypes,
selectedMeterTypeId,
onSelectMeterTypeId,
loadingMeterTypes,
}: Props) { }: Props) {
const options = useMemo( const options = useMemo(
() => () =>
@@ -76,24 +85,7 @@ export default function ConcentratorsSidebar({
</button> </button>
</div> </div>
{/* Tipos de tomas (dropdown) */}
<div className="mt-4 relative"> <div className="mt-4 relative">
<button
type="button"
onClick={() => setTypesMenuOpen((v) => !v)}
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"
>
<span className="flex items-center gap-2">
Tipos de tomas
<span className="text-xs font-semibold text-gray-500">
({sampleViewLabel})
</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">
@@ -124,15 +116,36 @@ export default function ConcentratorsSidebar({
)} )}
</div> </div>
<div className="mt-3">
<label className="block text-xs font-semibold text-gray-700 mb-1.5">
Filtrar por Tipo de Toma
</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">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">
{sampleView === "GENERAL" {selectedMeterTypeId
? "No projects available. Please contact your administrator." ? `No hay proyectos con el tipo de toma seleccionado y concentradores ${sampleViewLabel}.`
: `No projects with ${sampleViewLabel} concentrators found.` : sampleView === "GENERAL"
? "No hay proyectos con concentradores disponibles."
: `No hay proyectos con concentradores ${sampleViewLabel}.`
} }
</div> </div>
) : ( ) : (

View File

@@ -5,6 +5,7 @@ import {
type ConcentratorType, type ConcentratorType,
} from "../../api/concentrators"; } from "../../api/concentrators";
import { fetchProjects, type Project } from "../../api/projects"; import { fetchProjects, type Project } from "../../api/projects";
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
import type { ProjectCard, SampleView } from "./ConcentratorsPage"; import type { ProjectCard, SampleView } from "./ConcentratorsPage";
type User = { type User = {
@@ -17,11 +18,15 @@ export function useConcentrators(currentUser: User) {
const [loadingProjects, setLoadingProjects] = useState(true); const [loadingProjects, setLoadingProjects] = useState(true);
const [loadingConcentrators, setLoadingConcentrators] = useState(true); const [loadingConcentrators, setLoadingConcentrators] = useState(true);
const [loadingMeterTypes, setLoadingMeterTypes] = useState(true);
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [allProjects, setAllProjects] = useState<string[]>([]); const [allProjects, setAllProjects] = useState<string[]>([]);
const [selectedProject, setSelectedProject] = useState(""); const [selectedProject, setSelectedProject] = useState("");
const [meterTypes, setMeterTypes] = useState<MeterType[]>([]);
const [selectedMeterTypeId, setSelectedMeterTypeId] = useState<string>("");
const [concentrators, setConcentrators] = useState<Concentrator[]>([]); const [concentrators, setConcentrators] = useState<Concentrator[]>([]);
const [filteredConcentrators, setFilteredConcentrators] = useState< const [filteredConcentrators, setFilteredConcentrators] = useState<
Concentrator[] Concentrator[]
@@ -54,6 +59,19 @@ export function useConcentrators(currentUser: User) {
[allProjects, currentUser.role, currentUser.project] [allProjects, currentUser.role, currentUser.project]
); );
const loadMeterTypes = async () => {
setLoadingMeterTypes(true);
try {
const meterTypesData = await fetchMeterTypes();
setMeterTypes(meterTypesData);
} catch (err) {
console.error("Error loading meter types:", err);
setMeterTypes([]);
} finally {
setLoadingMeterTypes(false);
}
};
const loadProjects = async () => { const loadProjects = async () => {
setLoadingProjects(true); setLoadingProjects(true);
try { try {
@@ -92,8 +110,8 @@ export function useConcentrators(currentUser: User) {
} }
}; };
// init - load projects and concentrators
useEffect(() => { useEffect(() => {
loadMeterTypes();
loadProjects(); loadProjects();
loadConcentrators(); loadConcentrators();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -163,7 +181,21 @@ export function useConcentrators(currentUser: User) {
const baseContact = "Operaciones"; const baseContact = "Operaciones";
const baseLastSync = "Hace 1 h"; const baseLastSync = "Hace 1 h";
return visibleProjects.map((projectId) => ({ 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);
return project?.meterTypeId === selectedMeterTypeId;
});
}
return filteredProjects.map((projectId) => ({
id: projectId, id: projectId,
name: projectNameMap[projectId] ?? projectId, name: projectNameMap[projectId] ?? projectId,
region: baseRegion, region: baseRegion,
@@ -174,12 +206,27 @@ export function useConcentrators(currentUser: User) {
contact: baseContact, contact: baseContact,
status: "ACTIVO" as const, status: "ACTIVO" as const,
})); }));
}, [concentrators, visibleProjects, projects, isGeneral, sampleView]); }, [concentrators, visibleProjects, projects, isGeneral, sampleView, selectedMeterTypeId]);
const projectsData: ProjectCard[] = useMemo(() => { const projectsData: ProjectCard[] = useMemo(() => {
return projectsDataGeneral; return projectsDataGeneral;
}, [projectsDataGeneral]); }, [projectsDataGeneral]);
useEffect(() => {
if (projectsData.length > 0) {
const firstProject = projectsData[0];
const currentProjectExists = projectsData.find((p) => p.id === selectedProject);
if (!selectedProject || !currentProjectExists) {
setSelectedProject(firstProject.id);
}
} else {
if (selectedProject) {
setSelectedProject("");
}
}
}, [projectsData]);
return { return {
// view // view
sampleView, sampleView,
@@ -190,6 +237,7 @@ export function useConcentrators(currentUser: User) {
// loading // loading
loadingProjects, loadingProjects,
loadingConcentrators, loadingConcentrators,
loadingMeterTypes,
// projects // projects
allProjects, allProjects,
@@ -198,6 +246,10 @@ export function useConcentrators(currentUser: User) {
selectedProject, selectedProject,
setSelectedProject, setSelectedProject,
meterTypes,
selectedMeterTypeId,
setSelectedMeterTypeId,
// data // data
concentrators, concentrators,
setConcentrators, setConcentrators,