feat(F-100): completed feature

This commit is contained in:
chattie
2026-08-21 13:28:14 +02:00
parent 7418624df1
commit da919d705f
16 changed files with 413 additions and 49 deletions

View File

@@ -49,6 +49,12 @@ export function PriceStockSection({ productId }: { productId: string }) {
const [savingEan, setSavingEan] = useState(false);
const [eanMsg, setEanMsg] = useState('');
// SKU (F-100)
const [sku, setSku] = useState('');
const [savingSku, setSavingSku] = useState(false);
const [skuMsg, setSkuMsg] = useState('');
const [regeneratingSku, setRegeneratingSku] = useState(false);
// Peso y compra mínima (F-102)
const [unitWeightKg, setUnitWeightKg] = useState('1');
const [minPurchaseQty, setMinPurchaseQty] = useState('1');
@@ -77,6 +83,7 @@ export function PriceStockSection({ productId }: { productId: string }) {
setVariant(first);
setExtraVariants(Math.max(0, (items?.length ?? 0) - 1));
setEan(first?.ean ?? '');
setSku(first?.sku ?? '');
if (!first) return;
// Precio vigente
try {
@@ -172,6 +179,46 @@ export function PriceStockSection({ productId }: { productId: string }) {
}
};
const saveSku = async () => {
if (!variant) return;
const next = sku.trim();
if (!next || next === (variant.sku ?? '')) return;
if (!/^[A-Za-z0-9-]+$/.test(next)) {
setSkuMsg('SKU inválido (solo letras, números y guiones)');
return;
}
setSavingSku(true);
setSkuMsg('');
try {
const updated = await productsApi.updateVariant(productId, variant.id, { sku: next });
setVariant((prev) => (prev ? { ...prev, sku: updated.sku } : prev));
setSku(updated.sku);
setSkuMsg('✓');
setTimeout(() => setSkuMsg(''), 3000);
} catch (error) {
setSkuMsg(error instanceof Error && error.message.includes('409') ? 'SKU duplicado' : 'Error');
} finally {
setSavingSku(false);
}
};
const regenerateSku = async () => {
if (!variant) return;
setRegeneratingSku(true);
setSkuMsg('');
try {
const product = await productsApi.get(productId);
const { sku: suggestion } = await productsApi.generateSku(product.name);
setSku(suggestion);
setSkuMsg('Pulsa intro para guardar');
setTimeout(() => setSkuMsg(''), 3000);
} catch {
setSkuMsg('Error');
} finally {
setRegeneratingSku(false);
}
};
const saveEan = async () => {
if (!variant) return;
const next = ean.trim();
@@ -300,6 +347,34 @@ export function PriceStockSection({ productId }: { productId: string }) {
{stockMsg && <span className={`text-xs shrink-0 ${stockMsg.startsWith('✓') ? 'text-green-600' : 'text-red-600'}`}>{stockMsg}</span>}
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">SKU</label>
<div className="flex items-center gap-2">
<input
type="text" value={sku}
onChange={(e) => setSku(e.target.value)}
onBlur={saveSku}
onKeyDown={(e) => { if (e.key === 'Enter') saveSku(); }}
disabled={savingSku}
placeholder="MV-ESPELTA-ECOLOGICA"
className="w-full px-3 py-2 border border-gray-300 rounded-xl text-sm font-mono focus:ring-2 focus:ring-[#2D6A4F] outline-none bg-white disabled:opacity-50"
/>
<button
type="button"
onClick={regenerateSku}
disabled={regeneratingSku}
className="shrink-0 px-3 py-2 text-xs font-semibold text-[#2D6A4F] border border-[#2D6A4F] rounded-xl hover:bg-[#2D6A4F] hover:text-white transition-colors disabled:opacity-50"
title="Regenerar sugerencia desde el nombre del producto"
>
{regeneratingSku ? '...' : '↻'}
</button>
{skuMsg && (
<span className={`text-xs shrink-0 ${skuMsg.startsWith('✓') || skuMsg.startsWith('Pulsa') ? 'text-green-600' : 'text-red-600'}`}>
{skuMsg}
</span>
)}
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-600 mb-1">EAN</label>
<div className="flex items-center gap-2">

View File

@@ -84,6 +84,8 @@ export const productsApi = {
api.patch<import('@/types').Product>(`/api/products/${id}`, data),
generateSeo: (id: string) =>
api.post<import('@/types').Product>(`/api/products/${id}/generate-seo`),
generateSku: (title: string) =>
api.post<{ sku: string }>(`/api/products/sku:generate`, { title }),
setState: (id: string, state: 'active' | 'archived') =>
api.patch(`/api/products/${id}/state`, { state }),
delete: (id: string) => api.delete(`/api/products/${id}`),

File diff suppressed because one or more lines are too long