feat(F-094): completed feature

This commit is contained in:
chattie
2026-08-20 22:01:54 +02:00
parent c56037492d
commit 418bdca7f4
14 changed files with 305 additions and 58 deletions

View File

@@ -6,6 +6,7 @@ import { productsApi, brandsApi, categoriesApi } from '@/lib/api-client';
import { ImagesSection } from './sections/ImagesSection';
import { InventorySection } from './sections/InventorySection';
import { PricingSection } from './sections/PricingSection';
import { VariantManager } from './sections/VariantManager';
import LexicalEditor from '@/features/cms/components/LexicalEditor';
interface ProductEditorProps {
@@ -408,6 +409,13 @@ export function ProductEditor({ productId }: ProductEditorProps) {
{/* ── PUBLISH ── */}
{tab === 'publish' && (
<section className="space-y-5">
{productId ? (
<VariantManager productId={productId} />
) : (
<div className="p-6 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
Guarda primero el producto para crear variantes.
</div>
)}
<div>
<label className="block text-sm font-semibold text-gray-900 mb-3">Estado del producto</label>
<div className="grid grid-cols-3 gap-3">

View File

@@ -407,7 +407,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
<p className="text-4xl mb-3">📦</p>
<p className="text-gray-500 text-sm">Este producto no tiene variantes</p>
<p className="text-gray-400 text-xs mt-1">
Las variantes se crean desde la pestaña Publicar
Ve a Publicar para crear la primera variante
</p>
</div>
);

View File

@@ -114,7 +114,7 @@ export function PricingSection({ productId }: { productId: string }) {
if (variants.length === 0) {
return (
<div className="p-6 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
Este producto no tiene variantes. Las variantes se crean desde la pestaña Publicar.
Este producto no tiene variantes. Ve a Publicar para crear la primera variante.
</div>
);
}

View File

@@ -0,0 +1,127 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { ApiError, type ProductVariant } from '@/types';
import { productsApi } from '@/lib/api-client';
interface VariantManagerProps {
productId: string;
}
export function VariantManager({ productId }: VariantManagerProps) {
const [variants, setVariants] = useState<ProductVariant[]>([]);
const [sku, setSku] = useState('');
const [ean, setEan] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const load = useCallback(async () => {
setLoading(true);
try {
const result = await productsApi.getVariants(productId);
setVariants(result.items ?? []);
} catch {
setError('No se pudieron cargar las variantes.');
} finally {
setLoading(false);
}
}, [productId]);
useEffect(() => { load(); }, [load]);
const createVariant = async () => {
const normalizedSku = sku.trim();
const normalizedEan = ean.trim();
if (!normalizedSku) {
setError('El SKU es obligatorio.');
return;
}
setSaving(true);
setError('');
setMessage('');
try {
const created = await productsApi.createVariant(productId, {
sku: normalizedSku,
ean: normalizedEan || null,
});
setVariants((prev) => [...prev, created]);
setSku('');
setEan('');
setMessage('Variante creada correctamente.');
} catch (cause) {
setError(
cause instanceof ApiError && cause.statusCode === 409
? 'El SKU o EAN ya existe en otra variante.'
: 'No se pudo crear la variante.',
);
} finally {
setSaving(false);
}
};
return (
<div className="space-y-5">
<div className="p-4 bg-blue-50 border border-blue-100 rounded-xl text-sm text-blue-800">
<p className="font-semibold mb-1">¿Qué es una variante?</p>
<p>
Es una presentación vendible del producto. Cada formato, tamaño o referencia tiene su propio
SKU y, opcionalmente, EAN; sobre la variante se gestionan precios, stock e inventario.
</p>
</div>
<div className="p-5 bg-white border border-gray-200 rounded-xl space-y-4">
<div>
<h3 className="font-semibold text-gray-900">Crear variante</h3>
<p className="text-xs text-gray-500 mt-1">Por ejemplo, un formato o tamaño distinto del mismo producto.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<label className="text-sm font-medium text-gray-700">
SKU <span className="text-red-500">*</span>
<input
value={sku}
onChange={(event) => setSku(event.target.value)}
placeholder="Ej: PROTEINA-GUISANTE-500G"
className="mt-1 w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none"
/>
</label>
<label className="text-sm font-medium text-gray-700">
EAN <span className="text-gray-400 font-normal">(opcional)</span>
<input
value={ean}
onChange={(event) => setEan(event.target.value)}
placeholder="Código de barras"
className="mt-1 w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none"
/>
</label>
</div>
<button
type="button"
onClick={createVariant}
disabled={saving}
className="px-4 py-2 bg-[#2D6A4F] text-white text-sm font-semibold rounded-lg hover:bg-[#1B4332] disabled:opacity-50"
>
{saving ? 'Creando...' : 'Crear variante'}
</button>
{error && <p className="text-sm text-red-600">{error}</p>}
{message && <p className="text-sm text-green-600">{message}</p>}
</div>
<div className="p-5 bg-white border border-gray-200 rounded-xl">
<h3 className="font-semibold text-gray-900 mb-3">Variantes existentes</h3>
{loading ? <p className="text-sm text-gray-400">Cargando...</p> : variants.length === 0 ? (
<p className="text-sm text-gray-500">Todavía no hay variantes. Crea la primera arriba.</p>
) : (
<div className="space-y-2">
{variants.map((variant) => (
<div key={variant.id} className="flex items-center justify-between px-3 py-2 bg-gray-50 rounded-lg text-sm">
<span className="font-mono text-gray-700">{variant.sku}</span>
<span className="font-mono text-gray-500">{variant.ean ?? 'Sin EAN'}</span>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -75,6 +75,8 @@ export const productsApi = {
get: (id: string) => api.get<import('@/types').Product>(`/api/products/${id}`),
getVariants: (id: string) =>
api.get<{ items: import('@/types').ProductVariant[] }>(`/api/products/${id}/variants`),
createVariant: (productId: string, data: { sku: string; ean?: string | null }) =>
api.post<import('@/types').ProductVariant>(`/api/products/${productId}/variants`, data),
create: (data: unknown) => api.post<import('@/types').Product>('/api/products', data),
update: (id: string, data: unknown) =>
api.patch<import('@/types').Product>(`/api/products/${id}`, data),

File diff suppressed because one or more lines are too long