Se agrego la interfaz ProjectsPage

This commit is contained in:
Marlene-Angel
2025-12-18 14:27:56 -08:00
parent ae43042ac6
commit 0f0328f41b
4 changed files with 434 additions and 77 deletions

View File

@@ -5,22 +5,33 @@ import TopMenu from "./components/layout/TopMenu";
import Home from "./pages/Home";
import MetersPage from "./pages/meters/MeterPage";
import ConcentratorsPage from "./pages/concentrators/ConcentratorsPage";
import UsersPage from "./pages/UsersPage"; // nueva página
import RolesPage from "./pages/RolesPage"; // nueva página
import ProjectsPage from "./pages/projects/ProjectsPage";
import UsersPage from "./pages/UsersPage";
import RolesPage from "./pages/RolesPage";
export type Page =
| "home"
| "projects"
| "meters"
| "concentrators"
| "users"
| "roles";
export default function App() {
const [page, setPage] = useState("home");
const [page, setPage] = useState<Page>("home");
const renderPage = () => {
switch (page) {
case "projects":
return <ProjectsPage />;
case "meters":
return <MetersPage />;
case "concentrators":
return <ConcentratorsPage />;
case "users":
return <UsersPage />; // nueva
return <UsersPage />;
case "roles":
return <RolesPage />; // nueva
return <RolesPage />;
case "home":
default:
return <Home />;
@@ -32,7 +43,9 @@ export default function App() {
<Sidebar setPage={setPage} />
<div className="flex-1 flex flex-col">
<TopMenu />
<main className="flex-1 overflow-auto">{renderPage()}</main>
<main className="flex-1 overflow-auto">
{renderPage()}
</main>
</div>
</div>
);

View File

@@ -7,17 +7,16 @@ import {
ExpandLess,
Menu,
People,
Key
} from "@mui/icons-material";
import { Page } from "../../App";
interface SidebarProps {
setPage: (page: string) => void;
setPage: (page: Page) => void;
}
export default function Sidebar({ setPage }: SidebarProps) {
const [systemOpen, setSystemOpen] = useState(true);
const [waterOpen, setWaterOpen] = useState(true);
const [usersOpen, setUsersOpen] = useState(true); // Nuevo
const [usersOpen, setUsersOpen] = useState(true);
const [pinned, setPinned] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -51,7 +50,6 @@ export default function Sidebar({ setPage }: SidebarProps) {
{/* MENU */}
<div className="flex-1 py-4 px-2 overflow-y-auto">
<ul className="space-y-1 text-white text-sm">
{/* DASHBOARD */}
<li>
<button
@@ -72,7 +70,9 @@ export default function Sidebar({ setPage }: SidebarProps) {
<Settings className="w-5 h-5 shrink-0" />
{isExpanded && (
<>
<span className="ml-3 flex-1 text-left">Project Management</span>
<span className="ml-3 flex-1 text-left">
Project Management
</span>
{systemOpen ? <ExpandLess /> : <ExpandMore />}
</>
)}
@@ -80,66 +80,37 @@ export default function Sidebar({ setPage }: SidebarProps) {
{isExpanded && systemOpen && (
<ul className="mt-1 space-y-1 text-xs">
<li>
<button
onClick={() => setPage("projects")}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
Projects
</button>
</li>
<li>
<button
onClick={() => setPage("concentrators")}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
Concentradores
Concentrators
</button>
</li>
<li>
<button
onClick={() => setPage("meters")}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
Medidores
Meters
</button>
</li>
</ul>
)}
</li>
{/* WATER METER SYSTEM
<li>
<button
onClick={() => isExpanded && setWaterOpen(!waterOpen)}
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
>
<WaterDrop className="w-5 h-5 shrink-0" />
{isExpanded && (
<>
<span className="ml-3 flex-1 text-left">Water Meter System Management</span>
{waterOpen ? <ExpandLess /> : <ExpandMore />}
</>
)}
</button>
{isExpanded && waterOpen && (
<ul className="mt-1 space-y-1 text-xs">
{[
["water-install", "Water Meter Installation"],
["device-install", "Device Installation"],
["meter-management", "Meter Management"],
["device-management", "Device Management"],
["data-monitoring", "Data Monitoring"],
["data-query", "Data Query"],
].map(([key, label]) => (
<li key={key}>
<button
onClick={() => setPage(key)}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
{label}
</button>
</li>
))}
</ul>
)}
</li>
*}
{/* SYSTEM USERS */}
{/* USERS MANAGEMENT */}
<li>
<button
onClick={() => isExpanded && setUsersOpen(!usersOpen)}
@@ -148,7 +119,9 @@ export default function Sidebar({ setPage }: SidebarProps) {
<People className="w-5 h-5 shrink-0" />
{isExpanded && (
<>
<span className="ml-3 flex-1 text-left">Users Management</span>
<span className="ml-3 flex-1 text-left">
Users Management
</span>
{usersOpen ? <ExpandLess /> : <ExpandMore />}
</>
)}
@@ -175,8 +148,6 @@ export default function Sidebar({ setPage }: SidebarProps) {
</ul>
)}
</li>
</ul>
</div>
</aside>

View File

@@ -0,0 +1,366 @@
import { useEffect, useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
/* ================= TYPES ================= */
interface Project {
id: string;
areaName: string;
deviceSN: string;
deviceName: string;
deviceType: string;
deviceStatus: "ACTIVE" | "INACTIVE";
operator: string;
installedTime: string;
communicationTime: string;
instructionManual: string;
}
/* ================= MOCK DATA ================= */
const mockProjects: Project[] = [
{
id: "1",
areaName: "Zona Norte",
deviceSN: "SN-001",
deviceName: "Sensor Alpha",
deviceType: "Flow Meter",
deviceStatus: "ACTIVE",
operator: "Juan Pérez",
installedTime: "2024-01-10",
communicationTime: "2024-01-11",
instructionManual: "Manual Alpha",
},
{
id: "2",
areaName: "Zona Centro",
deviceSN: "SN-002",
deviceName: "Sensor Beta",
deviceType: "Pressure Meter",
deviceStatus: "INACTIVE",
operator: "María López",
installedTime: "2024-02-05",
communicationTime: "2024-02-06",
instructionManual: "Manual Beta",
},
{
id: "3",
areaName: "Zona Sur",
deviceSN: "SN-003",
deviceName: "Sensor Gamma",
deviceType: "Flow Meter",
deviceStatus: "ACTIVE",
operator: "Carlos Ruiz",
installedTime: "2024-03-01",
communicationTime: "2024-03-02",
instructionManual: "Manual Gamma",
},
];
/* ================= API ================= */
const API_URL = "/api/v2/tables/m05u6wpquvdbv3c/records";
const fetchProjects = async (): Promise<Project[]> => {
const res = await fetch(API_URL);
const data = await res.json();
return data.records.map((r: any) => ({
id: r.id,
areaName: r.fields["Area Name"] ?? "",
deviceSN: r.fields["Device S/N"] ?? "",
deviceName: r.fields["Device Name"] ?? "",
deviceType: r.fields["Device Type"] ?? "",
deviceStatus:
r.fields["Device Status"] === "INACTIVE"
? "INACTIVE"
: "ACTIVE",
operator: r.fields["Operator"] ?? "",
installedTime: r.fields["Installed Time"] ?? "",
communicationTime: r.fields["Communication Time"] ?? "",
instructionManual: r.fields["Instruction Manual"] ?? "",
}));
};
/* ================= COMPONENT ================= */
export default function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [activeProject, setActiveProject] =
useState<Project | null>(null);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] =
useState<string | null>(null);
const emptyProject: Omit<Project, "id"> = {
areaName: "",
deviceSN: "",
deviceName: "",
deviceType: "",
deviceStatus: "ACTIVE",
operator: "",
installedTime: "",
communicationTime: "",
instructionManual: "",
};
const [form, setForm] =
useState<Omit<Project, "id">>(emptyProject);
/* ================= LOAD ================= */
const loadProjects = async () => {
try {
const data = await fetchProjects();
if (data.length === 0) {
setProjects(mockProjects);
} else {
setProjects(data);
}
} catch {
setProjects(mockProjects);
}
};
useEffect(() => {
loadProjects();
}, []);
/* ================= CRUD ================= */
const handleSave = () => {
if (editingId) {
setProjects((prev) =>
prev.map((p) =>
p.id === editingId
? { ...p, ...form }
: p
)
);
} else {
setProjects((prev) => [
...prev,
{ id: Date.now().toString(), ...form },
]);
}
setShowModal(false);
setEditingId(null);
setForm(emptyProject);
setActiveProject(null);
};
const handleDelete = () => {
if (!activeProject) return;
setProjects((prev) =>
prev.filter(
(p) => p.id !== activeProject.id
)
);
setActiveProject(null);
};
/* ================= FILTER ================= */
const filtered = projects.filter((p) =>
`${p.areaName} ${p.deviceName} ${p.deviceSN}`
.toLowerCase()
.includes(search.toLowerCase())
);
/* ================= UI ================= */
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<div
className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{
background:
"linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8)",
}}
>
<div>
<h1 className="text-2xl font-bold">
Project Management
</h1>
<p className="text-sm text-blue-100">
Projects registered
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setForm(emptyProject);
setEditingId(null);
setShowModal(true);
}}
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg"
>
<Plus size={16} /> Add
</button>
<button
onClick={() => {
if (!activeProject) return;
setEditingId(activeProject.id);
setForm({
areaName: activeProject.areaName,
deviceSN: activeProject.deviceSN,
deviceName: activeProject.deviceName,
deviceType: activeProject.deviceType,
deviceStatus: activeProject.deviceStatus,
operator: activeProject.operator,
installedTime: activeProject.installedTime,
communicationTime: activeProject.communicationTime,
instructionManual: activeProject.instructionManual,
});
setShowModal(true);
}}
disabled={!activeProject}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Pencil size={16} /> Edit
</button>
<button
onClick={handleDelete}
disabled={!activeProject}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Trash2 size={16} /> Delete
</button>
<button
onClick={loadProjects}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg"
>
<RefreshCcw size={16} /> Refresh
</button>
</div>
</div>
{/* SEARCH */}
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search project..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* TABLE */}
<MaterialTable
title="Projects"
columns={[
{ title: "Area Name", field: "areaName" },
{ title: "Device S/N", field: "deviceSN" },
{ title: "Device Name", field: "deviceName" },
{ title: "Device Type", field: "deviceType" },
{
title: "Status",
field: "deviceStatus",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData.deviceStatus === "ACTIVE"
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
{rowData.deviceStatus}
</span>
),
},
{ title: "Operator", field: "operator" },
{ title: "Installed Time", field: "installedTime" },
{ title: "Communication Time", field: "communicationTime" },
{ title: "Instruction Manual", field: "instructionManual" },
]}
data={filtered}
onRowClick={(_, rowData) =>
setActiveProject(rowData as Project)
}
options={{
search: false,
paging: true,
sorting: true,
rowStyle: (rowData) => ({
backgroundColor:
activeProject?.id ===
(rowData as Project).id
? "#EEF2FF"
: "#FFFFFF",
}),
}}
/>
</div>
{/* MODAL */}
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-xl p-6 w-96 space-y-3">
<h2 className="text-lg font-semibold">
{editingId ? "Edit Project" : "Add Project"}
</h2>
<input className="w-full border px-3 py-2 rounded" placeholder="Area Name"
value={form.areaName}
onChange={(e) => setForm({ ...form, areaName: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Device S/N"
value={form.deviceSN}
onChange={(e) => setForm({ ...form, deviceSN: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Device Name"
value={form.deviceName}
onChange={(e) => setForm({ ...form, deviceName: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Device Type"
value={form.deviceType}
onChange={(e) => setForm({ ...form, deviceType: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Operator"
value={form.operator}
onChange={(e) => setForm({ ...form, operator: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Installed Time"
value={form.installedTime}
onChange={(e) => setForm({ ...form, installedTime: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Communication Time"
value={form.communicationTime}
onChange={(e) => setForm({ ...form, communicationTime: e.target.value })} />
<input className="w-full border px-3 py-2 rounded" placeholder="Instruction Manual"
value={form.instructionManual}
onChange={(e) => setForm({ ...form, instructionManual: e.target.value })} />
<button
onClick={() =>
setForm({
...form,
deviceStatus:
form.deviceStatus === "ACTIVE"
? "INACTIVE"
: "ACTIVE",
})
}
className="w-full border rounded px-3 py-2"
>
Status: {form.deviceStatus}
</button>
<div className="flex justify-end gap-2 pt-3">
<button onClick={() => setShowModal(false)}>
Cancel
</button>
<button
onClick={handleSave}
className="bg-[#4c5f9e] text-white px-4 py-2 rounded"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -5,4 +5,11 @@ import tailwindcss from "@tailwindcss/vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(),tailwindcss()],
server: {
host: '0.0.0.0', // Esto permite que el servidor escuche en todas las IP disponibles
port: 5173, // Puerto por defecto de Vite (puedes cambiarlo si lo deseas)
},
})