first commit
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Search, Edit2, ShieldAlert, ShieldCheck, ChevronUp, ChevronDown, RotateCcw, Box } from 'lucide-react';
|
||||
import { fetchCompanies, setCompanyBanned } from '../api/sysadmin';
|
||||
import { EditCompanyModal } from '../components/ui/EditCompanyModal';
|
||||
import { CompanyServicesModal } from '../components/ui/CompanyServicesModal';
|
||||
|
||||
export const CompaniesPage = () => {
|
||||
const [companies, setCompanies] = useState<any[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Filters
|
||||
const [filters, setFilters] = useState({
|
||||
name: '',
|
||||
banned: 'all', // all | true | false
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc'
|
||||
});
|
||||
|
||||
// Modals state
|
||||
const [editingCompany, setEditingCompany] = useState<any>(null);
|
||||
const [servicesCompany, setServicesCompany] = useState<any>(null);
|
||||
|
||||
const loadCompanies = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload: any = { page, limit: 10, sortBy: filters.sortBy, sortOrder: filters.sortOrder };
|
||||
if (filters.name) payload.name = filters.name;
|
||||
if (filters.banned !== 'all') payload.banned = filters.banned === 'true';
|
||||
|
||||
const res = await fetchCompanies(payload);
|
||||
setCompanies(res.data);
|
||||
setTotalPages(res.pages);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error cargando organizaciones');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCompanies();
|
||||
}, [page, filters.sortBy, filters.sortOrder, filters.banned]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setPage(1);
|
||||
loadCompanies();
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setFilters({
|
||||
name: '',
|
||||
banned: 'all',
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc'
|
||||
});
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (filters.name === '' && filters.banned === 'all' && filters.sortBy === 'name' && filters.sortOrder === 'asc') {
|
||||
loadCompanies();
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
const toggleSort = (field: string) => {
|
||||
setFilters(prev => {
|
||||
if (prev.sortBy === field) {
|
||||
return { ...prev, sortOrder: prev.sortOrder === 'asc' ? 'desc' : 'asc' };
|
||||
}
|
||||
return { ...prev, sortBy: field, sortOrder: 'asc' };
|
||||
});
|
||||
};
|
||||
|
||||
const renderSortIcon = (field: string) => {
|
||||
if (filters.sortBy !== field) return null;
|
||||
return filters.sortOrder === 'asc' ? <ChevronUp size={16} /> : <ChevronDown size={16} />;
|
||||
};
|
||||
|
||||
const handleToggleBanned = async (company: any) => {
|
||||
if (window.confirm(`¿Deseas ${company.banned ? 'quitar el ban' : 'banear'} a la organización ${company.name}?`)) {
|
||||
try {
|
||||
await setCompanyBanned(company.id || company._id, !company.banned);
|
||||
loadCompanies();
|
||||
} catch (e: any) {
|
||||
alert('Error al actualizar estado de la organización');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="animate-fade-in companies-page-container" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', flexShrink: 0 }}>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: '700' }}>Gestión de Organizaciones</h1>
|
||||
</div>
|
||||
|
||||
{/* Filters Bar */}
|
||||
<div className="glass-panel filters-bar" style={{ padding: '1.5rem', marginBottom: '1rem', flexShrink: 0 }}>
|
||||
<form onSubmit={handleSearch} style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
<div style={{ flex: '1', minWidth: '200px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>Nombre</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-glass"
|
||||
placeholder="Buscar por nombre..."
|
||||
value={filters.name}
|
||||
onChange={(e) => setFilters({...filters, name: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '150px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>Baneado</label>
|
||||
<select
|
||||
className="input-glass"
|
||||
value={filters.banned}
|
||||
onChange={(e) => setFilters({...filters, banned: e.target.value})}
|
||||
>
|
||||
<option value="all">Todos</option>
|
||||
<option value="true">Sí</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button type="submit" className="btn-primary" style={{ display: 'flex', alignItems: 'center', gap: '8px', height: '100%' }}>
|
||||
<Search size={18} /> Buscar
|
||||
</button>
|
||||
<button type="button" onClick={handleResetFilters} className="btn-secondary" style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '0.75rem', height: '100%' }} title="Limpiar Filtros">
|
||||
<RotateCcw size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="glass-panel" style={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<div style={{ overflow: 'auto', flex: 1 }} className="table-scroll-container">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', minWidth: '700px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="sticky-header sortable-header" onClick={() => toggleSort('name')} style={{ padding: '1rem', color: 'var(--text-muted)', fontWeight: '600', cursor: 'pointer', userSelect: 'none' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>Organización {renderSortIcon('name')}</div>
|
||||
</th>
|
||||
<th className="sticky-header sortable-header" onClick={() => toggleSort('categoryId')} style={{ padding: '1rem', color: 'var(--text-muted)', fontWeight: '600', cursor: 'pointer', userSelect: 'none' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>Categoría {renderSortIcon('categoryId')}</div>
|
||||
</th>
|
||||
<th className="sticky-header" style={{ padding: '1rem', color: 'var(--text-muted)', fontWeight: '600' }}>Estado</th>
|
||||
<th className="sticky-header" style={{ padding: '1rem', color: 'var(--text-muted)', fontWeight: '600', textAlign: 'right' }}>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={4} style={{ padding: '2rem', textAlign: 'center' }}>Cargando...</td></tr>
|
||||
) : companies.length === 0 ? (
|
||||
<tr><td colSpan={4} style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}>No se encontraron organizaciones</td></tr>
|
||||
) : (
|
||||
companies.map(c => (
|
||||
<tr key={c.id} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', transition: 'background 0.2s' }} className="table-row-hover">
|
||||
<td style={{ padding: '1rem' }}>
|
||||
<div style={{ fontWeight: '600', color: 'var(--text-main)' }}>{c.name}</div>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>{c.description || '-'}</div>
|
||||
</td>
|
||||
<td style={{ padding: '1rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
|
||||
{c.categoryId}
|
||||
</td>
|
||||
<td style={{ padding: '1rem' }}>
|
||||
<button
|
||||
onClick={() => handleToggleBanned(c)}
|
||||
style={{ background: 'transparent', display: 'flex', alignItems: 'center', gap: '5px', color: c.banned ? 'var(--danger)' : 'var(--success)', cursor: 'pointer' }}
|
||||
title={c.banned ? 'Click para desbanear' : 'Click para banear'}
|
||||
>
|
||||
{c.banned ? <ShieldAlert size={18} /> : <ShieldCheck size={18} />}
|
||||
{c.banned ? 'Baneada' : 'Activa'}
|
||||
</button>
|
||||
</td>
|
||||
<td style={{ padding: '1rem', textAlign: 'right' }}>
|
||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
||||
<button className="icon-btn" title="Ver Servicios" onClick={() => setServicesCompany(c)}><Box size={18} /></button>
|
||||
<button className="icon-btn" title="Editar Detalles" onClick={() => setEditingCompany(c)}><Edit2 size={18} /></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="pagination-bar" style={{ padding: '1rem', borderTop: '1px solid var(--glass-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexShrink: 0, background: 'rgba(0,0,0,0.2)' }}>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>Página {page} de {totalPages || 1}</span>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button className="btn-secondary" disabled={page === 1} onClick={() => setPage(p => p - 1)}>Anterior</button>
|
||||
<button className="btn-secondary" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Siguiente</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(10, 10, 15, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.sortable-header:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.table-row-hover:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.icon-btn {
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: var(--text-muted);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
.icon-btn.danger:hover {
|
||||
background: var(--danger);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--glass-border);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.companies-page-container {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.filters-bar form > div {
|
||||
flex: 1 1 100% !important;
|
||||
}
|
||||
.table-scroll-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Modals */}
|
||||
{editingCompany && <EditCompanyModal company={editingCompany} onClose={() => setEditingCompany(null)} onSaved={loadCompanies} />}
|
||||
{servicesCompany && <CompanyServicesModal company={servicesCompany} onClose={() => setServicesCompany(null)} />}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user