feat(F-076): completed feature
This commit is contained in:
@@ -35,6 +35,7 @@ export default function ProductsPage() {
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'archived'>('all');
|
||||
|
||||
// Debounce search
|
||||
useEffect(() => {
|
||||
@@ -53,11 +54,12 @@ export default function ProductsPage() {
|
||||
try {
|
||||
const data = await productsApi.list({
|
||||
limit: PAGE_SIZE,
|
||||
offset: page * PAGE_SIZE,
|
||||
offset: paginationEnabled ? page * PAGE_SIZE : 0,
|
||||
q: debouncedSearch || undefined,
|
||||
});
|
||||
setProducts(data.items ?? []);
|
||||
setTotal(data.items?.length ?? 0);
|
||||
if (!paginationEnabled) setPage(0);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar');
|
||||
} finally {
|
||||
@@ -67,7 +69,14 @@ export default function ProductsPage() {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE) || 1;
|
||||
const paginationEnabled = statusFilter === 'all' && !debouncedSearch;
|
||||
const totalPages = paginationEnabled ? (Math.ceil(total / PAGE_SIZE) || 1) : 1;
|
||||
|
||||
const filteredProducts = statusFilter === 'all'
|
||||
? products
|
||||
: products.filter(p => p.state === statusFilter);
|
||||
|
||||
const filteredTotal = filteredProducts.length;
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
@@ -75,7 +84,10 @@ export default function ProductsPage() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Productos</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">{total} productos</p>
|
||||
<p className="text-sm text-gray-500 mt-0.5">
|
||||
{filteredTotal} producto{filteredTotal !== 1 ? 's' : ''}
|
||||
{statusFilter !== 'all' ? ` (${statusFilter === 'active' ? 'activos' : 'archivados'})` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/products/new"
|
||||
@@ -87,6 +99,22 @@ export default function ProductsPage() {
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4 flex gap-2">
|
||||
{(['all', 'active', 'archived'] as const).map(f => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setStatusFilter(f)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||
statusFilter === f
|
||||
? 'bg-[#2D6A4F] text-white'
|
||||
: 'bg-white border border-gray-300 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{f === 'all' ? 'Todos' : f === 'active' ? 'Activos' : 'Archivados'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-md">
|
||||
<input
|
||||
type="search"
|
||||
@@ -148,10 +176,13 @@ export default function ProductsPage() {
|
||||
<th className="text-left text-xs font-semibold text-gray-500 uppercase tracking-wide px-4 py-3">
|
||||
Estado
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-gray-500 uppercase tracking-wide px-4 py-3">
|
||||
Tienda
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{products.map((p) => (
|
||||
{filteredProducts.map((p) => (
|
||||
<tr
|
||||
key={p.id}
|
||||
className="hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
@@ -189,7 +220,22 @@ export default function ProductsPage() {
|
||||
<StateBadge state={p.state} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-gray-300">→</span>
|
||||
{p.state === 'active' ? (
|
||||
<a
|
||||
href={`http://192.168.18.93:3003/productos/${p.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Ver en la tienda"
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-green-50 text-green-600 hover:bg-green-100 transition-colors"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<span className="w-8 block" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -200,24 +246,26 @@ export default function ProductsPage() {
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<p className="text-sm text-gray-500">
|
||||
Página {page + 1} de {totalPages}
|
||||
{paginationEnabled ? `Página ${page + 1} de ${totalPages}` : 'Mostrando resultados filtrados'}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg disabled:opacity-40 hover:bg-white transition-colors"
|
||||
>
|
||||
← Anterior
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg disabled:opacity-40 hover:bg-white transition-colors"
|
||||
>
|
||||
Siguiente →
|
||||
</button>
|
||||
</div>
|
||||
{paginationEnabled && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg disabled:opacity-40 hover:bg-white transition-colors"
|
||||
>
|
||||
← Anterior
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg disabled:opacity-40 hover:bg-white transition-colors"
|
||||
>
|
||||
Siguiente →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user