feat(F-076): completed feature

This commit is contained in:
chattie
2026-08-19 19:31:46 +02:00
parent a17dad6bd0
commit 5a31f3945e
11 changed files with 283 additions and 67 deletions

View File

@@ -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