Add dark mode support for tables and data pages
- Add CSS overrides for MaterialTable in dark mode - Update page containers with dark:bg-zinc-950 - Update sidebars with dark mode (MetersSidebar, ConcentratorsSidebar) - Update tables in AuditoriaPage, UsersPage, RolesPage - Update ConsumptionPage with dark gradient background - Update search inputs, select elements, and modals - Add dark borders for card separation Affected pages: - MeterPage, ConcentratorsPage, ProjectsPage - UsersPage, RolesPage, AuditoriaPage - ConsumptionPage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,3 +21,99 @@ body {
|
|||||||
body:where(.dark *) {
|
body:where(.dark *) {
|
||||||
@apply bg-zinc-950 text-zinc-100;
|
@apply bg-zinc-950 text-zinc-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MaterialTable Dark Mode Overrides */
|
||||||
|
.dark .MuiPaper-root {
|
||||||
|
background-color: #18181b !important; /* zinc-900 */
|
||||||
|
color: #fafafa !important; /* zinc-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableCell-root {
|
||||||
|
color: #e4e4e7 !important; /* zinc-200 */
|
||||||
|
border-bottom-color: #3f3f46 !important; /* zinc-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableCell-head {
|
||||||
|
background-color: #18181b !important; /* zinc-900 */
|
||||||
|
color: #fafafa !important; /* zinc-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableRow-root:hover {
|
||||||
|
background-color: #27272a !important; /* zinc-800 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableRow-root.Mui-selected,
|
||||||
|
.dark .MuiTableRow-root.Mui-selected:hover {
|
||||||
|
background-color: #3f3f46 !important; /* zinc-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiToolbar-root {
|
||||||
|
background-color: #18181b !important; /* zinc-900 */
|
||||||
|
color: #fafafa !important; /* zinc-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTypography-root {
|
||||||
|
color: #fafafa !important; /* zinc-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTablePagination-root {
|
||||||
|
color: #a1a1aa !important; /* zinc-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTablePagination-selectIcon {
|
||||||
|
color: #a1a1aa !important; /* zinc-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiIconButton-root {
|
||||||
|
color: #a1a1aa !important; /* zinc-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiIconButton-root:hover {
|
||||||
|
background-color: #3f3f46 !important; /* zinc-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiIconButton-root.Mui-disabled {
|
||||||
|
color: #52525b !important; /* zinc-600 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiInputBase-root {
|
||||||
|
color: #e4e4e7 !important; /* zinc-200 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiInput-underline:before {
|
||||||
|
border-bottom-color: #3f3f46 !important; /* zinc-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiSelect-icon {
|
||||||
|
color: #a1a1aa !important; /* zinc-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableSortLabel-root {
|
||||||
|
color: #fafafa !important; /* zinc-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableSortLabel-root:hover {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableSortLabel-root.Mui-active {
|
||||||
|
color: #60a5fa !important; /* blue-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .MuiTableSortLabel-icon {
|
||||||
|
color: #60a5fa !important; /* blue-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for table row active/selected state */
|
||||||
|
.dark .MuiTableBody-root .MuiTableRow-root[style*="background-color: rgb(238, 242, 255)"],
|
||||||
|
.dark .MuiTableBody-root .MuiTableRow-root[style*="#EEF2FF"] {
|
||||||
|
background-color: #3f3f46 !important; /* zinc-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for inline styles - override white backgrounds */
|
||||||
|
.dark [style*="background-color: rgb(255, 255, 255)"],
|
||||||
|
.dark [style*="background-color: #FFFFFF"],
|
||||||
|
.dark [style*="background-color: #fff"],
|
||||||
|
.dark [style*="backgroundColor: rgb(255, 255, 255)"] {
|
||||||
|
background-color: #18181b !important; /* zinc-900 */
|
||||||
|
}
|
||||||
@@ -114,20 +114,20 @@ export default function AuditoriaPage() {
|
|||||||
const uniqueTables = Array.from(new Set(logs.map((log) => log.table_name)));
|
const uniqueTables = Array.from(new Set(logs.map((log) => log.table_name)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full dark:bg-zinc-950">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-64 border-r border-gray-200 bg-white p-4">
|
<aside className="w-64 border-r border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4">
|
||||||
<h2 className="text-lg font-semibold mb-4">Filtros</h2>
|
<h2 className="text-lg font-semibold mb-4 dark:text-white">Filtros</h2>
|
||||||
|
|
||||||
{/* Action Filter */}
|
{/* Action Filter */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Acción
|
Acción
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedAction}
|
value={selectedAction}
|
||||||
onChange={(e) => setSelectedAction(e.target.value as AuditAction | "")}
|
onChange={(e) => setSelectedAction(e.target.value as AuditAction | "")}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
|
className="w-full border border-gray-300 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 rounded-md px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Todas las acciones</option>
|
<option value="">Todas las acciones</option>
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
@@ -140,13 +140,13 @@ export default function AuditoriaPage() {
|
|||||||
|
|
||||||
{/* Table Filter */}
|
{/* Table Filter */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Tabla
|
Tabla
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedTable}
|
value={selectedTable}
|
||||||
onChange={(e) => setSelectedTable(e.target.value)}
|
onChange={(e) => setSelectedTable(e.target.value)}
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
|
className="w-full border border-gray-300 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 rounded-md px-3 py-2 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Todas las tablas</option>
|
<option value="">Todas las tablas</option>
|
||||||
{uniqueTables.map((table) => (
|
{uniqueTables.map((table) => (
|
||||||
@@ -160,17 +160,17 @@ export default function AuditoriaPage() {
|
|||||||
{/* Clear Filters */}
|
{/* Clear Filters */}
|
||||||
<button
|
<button
|
||||||
onClick={handleClearFilters}
|
onClick={handleClearFilters}
|
||||||
className="w-full bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-md text-sm"
|
className="w-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-zinc-200 px-3 py-2 rounded-md text-sm"
|
||||||
>
|
>
|
||||||
Limpiar filtros
|
Limpiar filtros
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-zinc-700">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 mb-2">
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Estadísticas
|
Estadísticas
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600 dark:text-zinc-400">
|
||||||
<p>Total de registros: {total}</p>
|
<p>Total de registros: {total}</p>
|
||||||
<p>Página actual: {currentPage} de {totalPages}</p>
|
<p>Página actual: {currentPage} de {totalPages}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,9 +180,9 @@ export default function AuditoriaPage() {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 flex flex-col">
|
<main className="flex-1 flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b border-gray-200 bg-white px-6 py-4">
|
<div className="border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-6 py-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
Auditoría del Sistema
|
Auditoría del Sistema
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -205,7 +205,7 @@ export default function AuditoriaPage() {
|
|||||||
placeholder="Buscar por usuario, email, tabla o descripción..."
|
placeholder="Buscar por usuario, email, tabla o descripción..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 rounded-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,52 +213,52 @@ export default function AuditoriaPage() {
|
|||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="flex-1 overflow-auto p-6">
|
<div className="flex-1 overflow-auto p-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500 dark:text-zinc-400">
|
||||||
Cargando registros...
|
Cargando registros...
|
||||||
</div>
|
</div>
|
||||||
) : filteredLogs.length === 0 ? (
|
) : filteredLogs.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500 dark:text-zinc-400">
|
||||||
No se encontraron registros de auditoría
|
No se encontraron registros de auditoría
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow dark:border dark:border-zinc-800 overflow-hidden">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-zinc-700">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 dark:bg-zinc-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Fecha/Hora
|
Fecha/Hora
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Usuario
|
Usuario
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Acción
|
Acción
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Tabla
|
Tabla
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Descripción
|
Descripción
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Estado
|
Estado
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-zinc-400 uppercase">
|
||||||
Acciones
|
Acciones
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200 dark:divide-zinc-700">
|
||||||
{filteredLogs.map((log) => (
|
{filteredLogs.map((log) => (
|
||||||
<tr key={log.id} className="hover:bg-gray-50">
|
<tr key={log.id} className="hover:bg-gray-50 dark:hover:bg-zinc-800">
|
||||||
<td className="px-4 py-3 text-sm text-gray-900 whitespace-nowrap">
|
<td className="px-4 py-3 text-sm text-gray-900 dark:text-zinc-100 whitespace-nowrap">
|
||||||
{new Date(log.created_at).toLocaleString("es-MX")}
|
{new Date(log.created_at).toLocaleString("es-MX")}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900 dark:text-zinc-100">
|
||||||
{log.user_name}
|
{log.user_name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-500">{log.user_email}</div>
|
<div className="text-gray-500 dark:text-zinc-400">{log.user_email}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<span
|
<span
|
||||||
@@ -269,10 +269,10 @@ export default function AuditoriaPage() {
|
|||||||
{log.action}
|
{log.action}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-900">
|
<td className="px-4 py-3 text-sm text-gray-900 dark:text-zinc-100">
|
||||||
{log.table_name}
|
{log.table_name}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-600">
|
<td className="px-4 py-3 text-sm text-gray-600 dark:text-zinc-400">
|
||||||
{log.description || "-"}
|
{log.description || "-"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
@@ -305,7 +305,7 @@ export default function AuditoriaPage() {
|
|||||||
{!loading && logs.length > 0 && (
|
{!loading && logs.length > 0 && (
|
||||||
<div className="mt-4 flex flex-wrap items-center justify-between gap-4 px-4">
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-4 px-4">
|
||||||
{/* Page Info */}
|
{/* Page Info */}
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600 dark:text-zinc-400">
|
||||||
Mostrando{" "}
|
Mostrando{" "}
|
||||||
<span className="font-semibold text-gray-800">
|
<span className="font-semibold text-gray-800">
|
||||||
{(currentPage - 1) * limit + 1}
|
{(currentPage - 1) * limit + 1}
|
||||||
@@ -322,11 +322,11 @@ export default function AuditoriaPage() {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Page Size Selector */}
|
{/* Page Size Selector */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-600">Filas por página:</span>
|
<span className="text-sm text-gray-600 dark:text-zinc-400">Filas por página:</span>
|
||||||
<select
|
<select
|
||||||
value={limit}
|
value={limit}
|
||||||
onChange={(e) => handleLimitChange(Number(e.target.value))}
|
onChange={(e) => handleLimitChange(Number(e.target.value))}
|
||||||
className="px-3 py-1.5 text-sm bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="px-3 py-1.5 text-sm bg-white dark:bg-zinc-800 dark:text-zinc-100 border border-gray-300 dark:border-zinc-700 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
<option value={20}>20</option>
|
<option value={20}>20</option>
|
||||||
@@ -340,7 +340,7 @@ export default function AuditoriaPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-md hover:bg-gray-50 dark:hover:bg-zinc-800 dark:text-zinc-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
@@ -350,7 +350,7 @@ export default function AuditoriaPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-md hover:bg-gray-50 dark:hover:bg-zinc-800 dark:text-zinc-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
Siguiente
|
Siguiente
|
||||||
</button>
|
</button>
|
||||||
@@ -375,7 +375,7 @@ export default function AuditoriaPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
ID
|
ID
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900 font-mono">
|
<p className="text-sm text-gray-900 dark:text-zinc-100 font-mono">
|
||||||
{selectedLog.id}
|
{selectedLog.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +383,7 @@ export default function AuditoriaPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Fecha/Hora
|
Fecha/Hora
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-900 dark:text-zinc-100">
|
||||||
{new Date(selectedLog.created_at).toLocaleString("es-MX")}
|
{new Date(selectedLog.created_at).toLocaleString("es-MX")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +391,7 @@ export default function AuditoriaPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">{selectedLog.user_name}</p>
|
<p className="text-sm text-gray-900 dark:text-zinc-100">{selectedLog.user_name}</p>
|
||||||
<p className="text-xs text-gray-500">{selectedLog.user_email}</p>
|
<p className="text-xs text-gray-500">{selectedLog.user_email}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -410,13 +410,13 @@ export default function AuditoriaPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Tabla
|
Tabla
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">{selectedLog.table_name}</p>
|
<p className="text-sm text-gray-900 dark:text-zinc-100">{selectedLog.table_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Record ID
|
Record ID
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900 font-mono">
|
<p className="text-sm text-gray-900 dark:text-zinc-100 font-mono">
|
||||||
{selectedLog.record_id || "-"}
|
{selectedLog.record_id || "-"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,7 +424,7 @@ export default function AuditoriaPage() {
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
IP Address
|
IP Address
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-900 dark:text-zinc-100">
|
||||||
{selectedLog.ip_address || "-"}
|
{selectedLog.ip_address || "-"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,16 +446,16 @@ export default function AuditoriaPage() {
|
|||||||
|
|
||||||
{selectedLog.description && (
|
{selectedLog.description && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Descripción
|
Descripción
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900">{selectedLog.description}</p>
|
<p className="text-sm text-gray-900 dark:text-zinc-100">{selectedLog.description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedLog.old_values && (
|
{selectedLog.old_values && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Valores Anteriores
|
Valores Anteriores
|
||||||
</label>
|
</label>
|
||||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto">
|
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto">
|
||||||
@@ -466,7 +466,7 @@ export default function AuditoriaPage() {
|
|||||||
|
|
||||||
{selectedLog.new_values && (
|
{selectedLog.new_values && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-zinc-300 mb-2">
|
||||||
Valores Nuevos
|
Valores Nuevos
|
||||||
</label>
|
</label>
|
||||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto">
|
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto">
|
||||||
@@ -486,7 +486,7 @@ export default function AuditoriaPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 border-t border-gray-200 flex justify-end">
|
<div className="p-6 border-t border-gray-200 dark:border-zinc-700 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDetails(false)}
|
onClick={() => setShowDetails(false)}
|
||||||
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-md"
|
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-md"
|
||||||
|
|||||||
@@ -110,34 +110,34 @@ export default function RolesPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
<div className="flex gap-6 p-6 w-full bg-gray-100 dark:bg-zinc-950">
|
||||||
{/* LEFT INFO SIDEBAR */}
|
{/* LEFT INFO SIDEBAR */}
|
||||||
<div className="w-72 bg-white rounded-xl shadow p-4">
|
<div className="w-72 bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-4">
|
||||||
<h3 className="text-xs font-semibold text-gray-500 mb-3">Role Information</h3>
|
<h3 className="text-xs font-semibold text-gray-500 dark:text-zinc-400 mb-3">Role Information</h3>
|
||||||
<p className="text-sm text-gray-700 mb-4">
|
<p className="text-sm text-gray-700 dark:text-zinc-300 mb-4">
|
||||||
Manage system roles and their permissions. Roles define what actions users can perform.
|
Manage system roles and their permissions. Roles define what actions users can perform.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{activeRole && (
|
{activeRole && (
|
||||||
<div className="mt-6 pt-4 border-t">
|
<div className="mt-6 pt-4 border-t">
|
||||||
<h4 className="text-xs font-semibold text-gray-500 mb-2">Selected Role</h4>
|
<h4 className="text-xs font-semibold text-gray-500 dark:text-zinc-400 mb-2">Selected Role</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Name</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">Name</p>
|
||||||
<p className="text-sm font-medium">{activeRole.name}</p>
|
<p className="text-sm font-medium dark:text-zinc-200">{activeRole.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Description</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">Description</p>
|
||||||
<p className="text-sm">{activeRole.description || "No description"}</p>
|
<p className="text-sm dark:text-zinc-300">{activeRole.description || "No description"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Created</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">Created</p>
|
||||||
<p className="text-sm">{new Date(activeRole.created_at).toLocaleDateString()}</p>
|
<p className="text-sm dark:text-zinc-300">{new Date(activeRole.created_at).toLocaleDateString()}</p>
|
||||||
</div>
|
</div>
|
||||||
{activeRole.permissions && Object.keys(activeRole.permissions).length > 0 && (
|
{activeRole.permissions && Object.keys(activeRole.permissions).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500">Permissions</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">Permissions</p>
|
||||||
<p className="text-sm">{Object.keys(activeRole.permissions).length} permission groups</p>
|
<p className="text-sm dark:text-zinc-300">{Object.keys(activeRole.permissions).length} permission groups</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +206,7 @@ export default function RolesPage() {
|
|||||||
|
|
||||||
{/* SEARCH */}
|
{/* SEARCH */}
|
||||||
<input
|
<input
|
||||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 dark:text-zinc-100 rounded-lg shadow px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:placeholder-zinc-500"
|
||||||
placeholder="Search role by name or description..."
|
placeholder="Search role by name or description..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
|||||||
@@ -246,17 +246,17 @@ export default function UsersPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
<div className="flex gap-6 p-6 w-full bg-gray-100 dark:bg-zinc-950">
|
||||||
{/* LEFT INFO SIDEBAR */}
|
{/* LEFT INFO SIDEBAR */}
|
||||||
<div className="w-72 bg-white rounded-xl shadow p-4">
|
<div className="w-72 bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-4">
|
||||||
<h3 className="text-xs font-semibold text-gray-500 mb-3">Filter Options</h3>
|
<h3 className="text-xs font-semibold text-gray-500 dark:text-zinc-400 mb-3">Filter Options</h3>
|
||||||
<p className="text-sm text-gray-700 mb-4">Filter users by role</p>
|
<p className="text-sm text-gray-700 dark:text-zinc-300 mb-4">Filter users by role</p>
|
||||||
|
|
||||||
<label className="text-xs font-semibold text-gray-500 mb-2 block">Role</label>
|
<label className="text-xs font-semibold text-gray-500 mb-2 block">Role</label>
|
||||||
<select
|
<select
|
||||||
value={selectedRoleFilter}
|
value={selectedRoleFilter}
|
||||||
onChange={e => setSelectedRoleFilter(e.target.value)}
|
onChange={e => setSelectedRoleFilter(e.target.value)}
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
disabled={loadingUsers}
|
disabled={loadingUsers}
|
||||||
>
|
>
|
||||||
<option value="">All Roles</option>
|
<option value="">All Roles</option>
|
||||||
@@ -294,12 +294,12 @@ export default function UsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SEARCH */}
|
{/* SEARCH */}
|
||||||
<input className="bg-white rounded-lg shadow px-4 py-2 text-sm" placeholder="Search user..." value={search} onChange={e => setSearch(e.target.value)} />
|
<input className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500 rounded-lg shadow px-4 py-2 text-sm" placeholder="Search user..." value={search} onChange={e => setSearch(e.target.value)} />
|
||||||
|
|
||||||
{/* TABLE */}
|
{/* TABLE */}
|
||||||
{loadingUsers ? (
|
{loadingUsers ? (
|
||||||
<div className="bg-white rounded-xl shadow p-8 text-center">
|
<div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-8 text-center">
|
||||||
<p className="text-gray-500">Loading users...</p>
|
<p className="text-gray-500 dark:text-zinc-400">Loading users...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MaterialTable
|
<MaterialTable
|
||||||
@@ -329,8 +329,8 @@ export default function UsersPage() {
|
|||||||
{/* MODAL */}
|
{/* MODAL */}
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
|
<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">
|
<div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl p-6 w-96 space-y-3">
|
||||||
<h2 className="text-lg font-semibold">{editingId ? "Edit User" : "Add User"}</h2>
|
<h2 className="text-lg font-semibold dark:text-white">{editingId ? "Edit User" : "Add User"}</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm">
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm">
|
||||||
@@ -339,7 +339,7 @@ export default function UsersPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
placeholder="Full Name *"
|
placeholder="Full Name *"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={e => setForm({...form, name: e.target.value})}
|
onChange={e => setForm({...form, name: e.target.value})}
|
||||||
@@ -347,7 +347,7 @@ export default function UsersPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email *"
|
placeholder="Email *"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
@@ -357,7 +357,7 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
{!editingId && (
|
{!editingId && (
|
||||||
<input
|
<input
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password * (min 8 characters)"
|
placeholder="Password * (min 8 characters)"
|
||||||
value={form.password || ""}
|
value={form.password || ""}
|
||||||
@@ -369,7 +369,7 @@ export default function UsersPage() {
|
|||||||
<select
|
<select
|
||||||
value={form.roleId}
|
value={form.roleId}
|
||||||
onChange={e => setForm({...form, roleId: e.target.value, projectId: ""})}
|
onChange={e => setForm({...form, roleId: e.target.value, projectId: ""})}
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
disabled={loadingModalRoles || saving}
|
disabled={loadingModalRoles || saving}
|
||||||
>
|
>
|
||||||
<option value="">{loadingModalRoles ? "Loading roles..." : "Select Role *"}</option>
|
<option value="">{loadingModalRoles ? "Loading roles..." : "Select Role *"}</option>
|
||||||
@@ -380,7 +380,7 @@ export default function UsersPage() {
|
|||||||
<select
|
<select
|
||||||
value={form.projectId || ""}
|
value={form.projectId || ""}
|
||||||
onChange={e => setForm({...form, projectId: e.target.value})}
|
onChange={e => setForm({...form, projectId: e.target.value})}
|
||||||
className="w-full border px-3 py-2 rounded"
|
className="w-full border dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 px-3 py-2 rounded"
|
||||||
disabled={loadingProjects || saving}
|
disabled={loadingProjects || saving}
|
||||||
>
|
>
|
||||||
<option value="">{loadingProjects ? "Loading projects..." : "Select Project *"}</option>
|
<option value="">{loadingProjects ? "Loading projects..." : "Select Project *"}</option>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export default function ConcentratorsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
<div className="flex gap-6 p-6 w-full bg-gray-100 dark:bg-zinc-950">
|
||||||
<aside className="w-[420px] shrink-0">
|
<aside className="w-[420px] shrink-0">
|
||||||
<ConcentratorsSidebar
|
<ConcentratorsSidebar
|
||||||
loadingProjects={c.loadingProjects}
|
loadingProjects={c.loadingProjects}
|
||||||
@@ -224,7 +224,7 @@ export default function ConcentratorsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm disabled:opacity-60 disabled:cursor-not-allowed"
|
className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 dark:text-zinc-100 rounded-lg shadow px-4 py-2 text-sm disabled:opacity-60 disabled:cursor-not-allowed dark:placeholder-zinc-500"
|
||||||
placeholder="Buscar concentrador..."
|
placeholder="Buscar concentrador..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
|||||||
@@ -59,12 +59,12 @@ export default function ConcentratorsSidebar({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow p-4 flex flex-col h-[calc(100vh-48px)]">
|
<div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-4 flex flex-col h-[calc(100vh-48px)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Proyectos</p>
|
<p className="text-sm text-gray-500 dark:text-zinc-400">Proyectos</p>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400 dark:text-zinc-500">
|
||||||
Seleccionado:{" "}
|
Seleccionado:{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{projects.find((p) => p.id === selectedProject)?.name || "—"}
|
{projects.find((p) => p.id === selectedProject)?.name || "—"}
|
||||||
@@ -86,7 +86,7 @@ export default function ConcentratorsSidebar({
|
|||||||
<div className="mt-4 relative">
|
<div className="mt-4 relative">
|
||||||
|
|
||||||
{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 dark:border-zinc-700 bg-white dark:bg-zinc-800 shadow-lg overflow-hidden">
|
||||||
{options.map((opt) => {
|
{options.map((opt) => {
|
||||||
const active = sampleView === opt.key;
|
const active = sampleView === opt.key;
|
||||||
return (
|
return (
|
||||||
@@ -95,13 +95,13 @@ export default function ConcentratorsSidebar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChangeSampleView(opt.key)}
|
onClick={() => onChangeSampleView(opt.key)}
|
||||||
className={[
|
className={[
|
||||||
"w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-50",
|
"w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-50 dark:hover:bg-zinc-700",
|
||||||
active ? "bg-blue-50/60" : "bg-white",
|
active ? "bg-blue-50/60" : "bg-white",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`font-semibold ${
|
className={`font-semibold ${
|
||||||
active ? "text-blue-700" : "text-gray-700"
|
active ? "text-blue-700" : "text-gray-700 dark:text-zinc-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -122,7 +122,7 @@ export default function ConcentratorsSidebar({
|
|||||||
value={selectedMeterTypeId}
|
value={selectedMeterTypeId}
|
||||||
onChange={(e) => onSelectMeterTypeId(e.target.value)}
|
onChange={(e) => onSelectMeterTypeId(e.target.value)}
|
||||||
disabled={loadingMeterTypes}
|
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"
|
className="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 dark:text-zinc-100 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>
|
<option value="">Todos los tipos de toma</option>
|
||||||
{meterTypes.map((type) => (
|
{meterTypes.map((type) => (
|
||||||
@@ -136,9 +136,9 @@ export default function ConcentratorsSidebar({
|
|||||||
{/* 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 dark:text-zinc-400">Loading projects...</div>
|
||||||
) : projects.length === 0 ? (
|
) : projects.length === 0 ? (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500 dark:text-zinc-400">
|
||||||
{selectedMeterTypeId
|
{selectedMeterTypeId
|
||||||
? `No hay proyectos con el tipo de toma seleccionado.`
|
? `No hay proyectos con el tipo de toma seleccionado.`
|
||||||
: "No hay proyectos disponibles."
|
: "No hay proyectos disponibles."
|
||||||
@@ -155,16 +155,16 @@ export default function ConcentratorsSidebar({
|
|||||||
className={[
|
className={[
|
||||||
"rounded-xl border p-4 transition cursor-pointer",
|
"rounded-xl border p-4 transition cursor-pointer",
|
||||||
active
|
active
|
||||||
? "border-blue-600 bg-blue-50/40"
|
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/30"
|
||||||
: "border-gray-200 bg-white hover:bg-gray-50",
|
: "border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-800">
|
<p className="text-sm font-semibold text-gray-800 dark:text-zinc-100">
|
||||||
{p.name}
|
{p.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{p.region}</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">{p.region}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@@ -172,7 +172,7 @@ export default function ConcentratorsSidebar({
|
|||||||
"text-xs font-semibold px-2 py-1 rounded-full",
|
"text-xs font-semibold px-2 py-1 rounded-full",
|
||||||
p.status === "ACTIVO"
|
p.status === "ACTIVO"
|
||||||
? "bg-green-100 text-green-700"
|
? "bg-green-100 text-green-700"
|
||||||
: "bg-gray-200 text-gray-700",
|
: "bg-gray-200 text-gray-700 dark:text-zinc-200",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{p.status}
|
{p.status}
|
||||||
@@ -181,34 +181,34 @@ export default function ConcentratorsSidebar({
|
|||||||
|
|
||||||
<div className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
<div className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Subproyectos</span>
|
<span className="text-gray-500 dark:text-zinc-400">Subproyectos</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.projects}
|
{p.projects}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Concentradores</span>
|
<span className="text-gray-500 dark:text-zinc-400">Concentradores</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.concentrators}
|
{p.concentrators}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Alertas activas</span>
|
<span className="text-gray-500 dark:text-zinc-400">Alertas activas</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.activeAlerts}
|
{p.activeAlerts}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Última sync</span>
|
<span className="text-gray-500 dark:text-zinc-400">Última sync</span>
|
||||||
<span className="font-medium text-gray-800">{p.lastSync}</span>
|
<span className="font-medium text-gray-800 dark:text-zinc-100">{p.lastSync}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2 flex justify-between gap-2">
|
<div className="col-span-2 flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Responsable</span>
|
<span className="text-gray-500 dark:text-zinc-400">Responsable</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.contact}
|
{p.contact}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +237,7 @@ export default function ConcentratorsSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-3 border-t text-xs text-gray-500">
|
<div className="pt-3 border-t text-xs text-gray-500 dark:text-zinc-400">
|
||||||
Nota: region/alertas/última sync están en modo demostración hasta integrar
|
Nota: region/alertas/última sync están en modo demostración hasta integrar
|
||||||
backend.
|
backend.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -211,13 +211,13 @@ export default function ConsumptionPage() {
|
|||||||
const activeFiltersCount = [selectedProject, startDate, endDate].filter(Boolean).length;
|
const activeFiltersCount = [selectedProject, startDate, endDate].filter(Boolean).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 p-6">
|
<div className="min-h-full bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-zinc-950 dark:via-zinc-950 dark:to-zinc-950 p-6">
|
||||||
<div className="max-w-[1600px] mx-auto space-y-6">
|
<div className="max-w-[1600px] mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-800">Consumo de Agua</h1>
|
<h1 className="text-2xl font-bold text-slate-800 dark:text-white">Consumo de Agua</h1>
|
||||||
<p className="text-slate-500 text-sm mt-0.5">
|
<p className="text-slate-500 dark:text-zinc-400 text-sm mt-0.5">
|
||||||
Monitoreo en tiempo real de lecturas
|
Monitoreo en tiempo real de lecturas
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +231,7 @@ export default function ConsumptionPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => loadData(pagination.page)}
|
onClick={() => loadData(pagination.page)}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-slate-600 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
|
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-slate-600 dark:text-zinc-300 bg-white dark:bg-zinc-800 border border-slate-200 dark:border-zinc-700 rounded-xl hover:bg-slate-50 dark:hover:bg-zinc-700 hover:border-slate-300 dark:hover:border-zinc-600 transition-all shadow-sm"
|
||||||
>
|
>
|
||||||
<RefreshCcw size={16} />
|
<RefreshCcw size={16} />
|
||||||
Actualizar
|
Actualizar
|
||||||
@@ -288,9 +288,9 @@ export default function ConsumptionPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Card */}
|
{/* Table Card */}
|
||||||
<div className="bg-white rounded-2xl shadow-sm shadow-slate-200/50 border border-slate-200/60 overflow-hidden">
|
<div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-sm shadow-slate-200/50 dark:shadow-none border border-slate-200/60 dark:border-zinc-800 overflow-hidden">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<div className="px-5 py-4 border-b border-slate-100 flex flex-wrap items-center justify-between gap-4">
|
<div className="px-5 py-4 border-b border-slate-100 dark:border-zinc-800 flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search
|
<Search
|
||||||
@@ -302,7 +302,7 @@ export default function ConsumptionPage() {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Buscar lecturas..."
|
placeholder="Buscar lecturas..."
|
||||||
className="w-64 pl-10 pr-4 py-2 text-sm bg-slate-50 border-0 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:bg-white transition-all"
|
className="w-64 pl-10 pr-4 py-2 text-sm bg-slate-50 dark:bg-zinc-800 dark:text-zinc-100 border-0 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:bg-white transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,9 +334,9 @@ export default function ConsumptionPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-slate-500">
|
<div className="flex items-center gap-4 text-sm text-slate-500 dark:text-zinc-400">
|
||||||
<span>
|
<span>
|
||||||
<span className="font-semibold text-slate-700">{filteredReadings.length}</span>{" "}
|
<span className="font-semibold text-slate-700 dark:text-zinc-200">{filteredReadings.length}</span>{" "}
|
||||||
{pagination.total > filteredReadings.length && `de ${pagination.total} `}
|
{pagination.total > filteredReadings.length && `de ${pagination.total} `}
|
||||||
lecturas
|
lecturas
|
||||||
</span>
|
</span>
|
||||||
@@ -367,7 +367,7 @@ export default function ConsumptionPage() {
|
|||||||
|
|
||||||
{/* Filters Panel */}
|
{/* Filters Panel */}
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<div className="px-5 py-4 bg-slate-50/50 border-b border-slate-100 flex flex-wrap items-center gap-4">
|
<div className="px-5 py-4 bg-slate-50/50 border-b border-slate-100 dark:border-zinc-800 flex flex-wrap items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
<label className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
Proyecto
|
Proyecto
|
||||||
@@ -634,7 +634,7 @@ function StatCard({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="h-8 w-24 bg-slate-100 rounded-lg animate-pulse" />
|
<div className="h-8 w-24 bg-slate-100 rounded-lg animate-pulse" />
|
||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-bold text-slate-800">{value}</p>
|
<p className="text-2xl font-bold text-slate-800 dark:text-white">{value}</p>
|
||||||
)}
|
)}
|
||||||
{trend && !loading && (
|
{trend && !loading && (
|
||||||
<div className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">
|
<div className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export default function MetersPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
<div className="flex gap-6 p-6 w-full bg-gray-100 dark:bg-zinc-950">
|
||||||
{/* SIDEBAR */}
|
{/* SIDEBAR */}
|
||||||
<MetersSidebar
|
<MetersSidebar
|
||||||
loadingProjects={m.loadingProjects}
|
loadingProjects={m.loadingProjects}
|
||||||
@@ -289,7 +289,7 @@ export default function MetersPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 dark:text-zinc-100 rounded-lg shadow px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:placeholder-zinc-500"
|
||||||
placeholder="Buscar por nombre, serial, ubicación o concentrador..."
|
placeholder="Buscar por nombre, serial, ubicación o concentrador..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ export default function MetersSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-[420px] shrink-0">
|
<aside className="w-[420px] shrink-0">
|
||||||
<div className="bg-white rounded-xl shadow p-4 flex flex-col h-[calc(100vh-48px)]">
|
<div className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 rounded-xl shadow p-4 flex flex-col h-[calc(100vh-48px)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Proyectos</p>
|
<p className="text-sm text-gray-500 dark:text-zinc-400">Proyectos</p>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400 dark:text-zinc-500">
|
||||||
Seleccionado:{" "}
|
Seleccionado:{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{selectedProject || "—"}
|
{selectedProject || "—"}
|
||||||
@@ -106,7 +106,7 @@ export default function MetersSidebar({
|
|||||||
<div className="mt-4 relative" ref={menuRef}>
|
<div className="mt-4 relative" ref={menuRef}>
|
||||||
|
|
||||||
{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 dark:border-zinc-700 bg-white dark:bg-zinc-800 shadow-lg overflow-hidden">
|
||||||
{TAKE_TYPE_OPTIONS.map((opt) => {
|
{TAKE_TYPE_OPTIONS.map((opt) => {
|
||||||
const active = takeType === opt.key;
|
const active = takeType === opt.key;
|
||||||
|
|
||||||
@@ -125,13 +125,13 @@ export default function MetersSidebar({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={[
|
className={[
|
||||||
"w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-50",
|
"w-full px-3 py-2 text-left text-sm flex items-center justify-between hover:bg-gray-50 dark:hover:bg-zinc-700",
|
||||||
active ? "bg-blue-50/60" : "bg-white",
|
active ? "bg-blue-50/60" : "bg-white",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`font-semibold ${
|
className={`font-semibold ${
|
||||||
active ? "text-blue-700" : "text-gray-700"
|
active ? "text-blue-700" : "text-gray-700 dark:text-zinc-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -153,7 +153,7 @@ export default function MetersSidebar({
|
|||||||
value={selectedMeterTypeId}
|
value={selectedMeterTypeId}
|
||||||
onChange={(e) => onSelectMeterTypeId(e.target.value)}
|
onChange={(e) => onSelectMeterTypeId(e.target.value)}
|
||||||
disabled={loadingMeterTypes}
|
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"
|
className="w-full rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 dark:text-zinc-100 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>
|
<option value="">Todos los tipos de toma</option>
|
||||||
{meterTypes.map((type) => (
|
{meterTypes.map((type) => (
|
||||||
@@ -167,7 +167,7 @@ export default function MetersSidebar({
|
|||||||
{/* 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 dark:text-zinc-400">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">
|
||||||
{selectedMeterTypeId
|
{selectedMeterTypeId
|
||||||
@@ -190,8 +190,8 @@ export default function MetersSidebar({
|
|||||||
className={[
|
className={[
|
||||||
"rounded-xl border p-4 transition cursor-pointer",
|
"rounded-xl border p-4 transition cursor-pointer",
|
||||||
active
|
active
|
||||||
? "border-blue-600 bg-blue-50/40"
|
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/30"
|
||||||
: "border-gray-200 bg-white hover:bg-gray-50",
|
: "border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700",
|
||||||
isMockMode ? "opacity-90" : "",
|
isMockMode ? "opacity-90" : "",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
title={
|
title={
|
||||||
@@ -202,10 +202,10 @@ export default function MetersSidebar({
|
|||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-800">
|
<p className="text-sm font-semibold text-gray-800 dark:text-zinc-100">
|
||||||
{p.name}
|
{p.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{p.region}</p>
|
<p className="text-xs text-gray-500 dark:text-zinc-400">{p.region}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@@ -213,7 +213,7 @@ export default function MetersSidebar({
|
|||||||
"text-xs font-semibold px-2 py-1 rounded-full",
|
"text-xs font-semibold px-2 py-1 rounded-full",
|
||||||
p.status === "ACTIVO"
|
p.status === "ACTIVO"
|
||||||
? "bg-green-100 text-green-700"
|
? "bg-green-100 text-green-700"
|
||||||
: "bg-gray-200 text-gray-700",
|
: "bg-gray-200 text-gray-700 dark:text-zinc-200",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{p.status}
|
{p.status}
|
||||||
@@ -222,36 +222,36 @@ export default function MetersSidebar({
|
|||||||
|
|
||||||
<div className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
<div className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Subproyectos</span>
|
<span className="text-gray-500 dark:text-zinc-400">Subproyectos</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.projects}
|
{p.projects}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Medidores</span>
|
<span className="text-gray-500 dark:text-zinc-400">Medidores</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.meters}
|
{p.meters}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Alertas activas</span>
|
<span className="text-gray-500 dark:text-zinc-400">Alertas activas</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.activeAlerts}
|
{p.activeAlerts}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Última sync</span>
|
<span className="text-gray-500 dark:text-zinc-400">Última sync</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.lastSync}
|
{p.lastSync}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2 flex justify-between gap-2">
|
<div className="col-span-2 flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Responsable</span>
|
<span className="text-gray-500 dark:text-zinc-400">Responsable</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-zinc-100">
|
||||||
{p.contact}
|
{p.contact}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +288,7 @@ export default function MetersSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-3 border-t text-xs text-gray-500">
|
<div className="pt-3 border-t text-xs text-gray-500 dark:text-zinc-400">
|
||||||
Nota: region/alertas/última sync están en modo demostración hasta
|
Nota: region/alertas/última sync están en modo demostración hasta
|
||||||
integrar backend.
|
integrar backend.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default function ProjectsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
<div className="flex gap-6 p-6 w-full bg-gray-100 dark:bg-zinc-950">
|
||||||
<div className="flex-1 flex flex-col gap-6">
|
<div className="flex-1 flex flex-col gap-6">
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<div
|
<div
|
||||||
@@ -214,7 +214,7 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
{/* SEARCH */}
|
{/* SEARCH */}
|
||||||
<input
|
<input
|
||||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
|
className="bg-white dark:bg-zinc-900 dark:border dark:border-zinc-800 dark:text-zinc-100 rounded-lg shadow px-4 py-2 text-sm dark:placeholder-zinc-500"
|
||||||
placeholder="Buscar proyecto..."
|
placeholder="Buscar proyecto..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
|||||||
Reference in New Issue
Block a user