feat(F-075): completed feature
This commit is contained in:
@@ -13,6 +13,12 @@ interface VariantRow {
|
||||
editValue: string;
|
||||
saving: boolean;
|
||||
msg: string;
|
||||
editingSku: boolean;
|
||||
editSkuValue: string;
|
||||
savingSku: boolean;
|
||||
editingEan: boolean;
|
||||
editEanValue: string;
|
||||
savingEan: boolean;
|
||||
}
|
||||
|
||||
type StockFilter = 'all' | 'in_stock' | 'low_stock' | 'out_of_stock';
|
||||
@@ -69,6 +75,12 @@ export default function InventoryPage() {
|
||||
editValue: '',
|
||||
saving: false,
|
||||
msg: '',
|
||||
editingSku: false,
|
||||
editSkuValue: '',
|
||||
savingSku: false,
|
||||
editingEan: false,
|
||||
editEanValue: '',
|
||||
savingEan: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -104,6 +116,30 @@ export default function InventoryPage() {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// Save SKU inline
|
||||
const handleSaveSku = async (variantId: string, productId: string, newSku: string) => {
|
||||
if (!newSku.trim()) return;
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingSku: true } : r));
|
||||
try {
|
||||
const updated = await productsApi.updateVariant(productId, variantId, { sku: newSku.trim() });
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, variant: { ...r.variant, sku: updated.sku }, editingSku: false, savingSku: false, msg: '✓' } : r));
|
||||
setTimeout(() => setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, msg: '' } : r)), 3000);
|
||||
} catch {
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingSku: false, msg: 'Error' } : r));
|
||||
}
|
||||
};
|
||||
|
||||
// Save EAN inline
|
||||
const handleSaveEan = async (variantId: string, productId: string, newEan: string) => {
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingEan: true } : r));
|
||||
try {
|
||||
const updated = await productsApi.updateVariant(productId, variantId, { ean: newEan || null });
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, variant: { ...r.variant, ean: updated.ean }, editingEan: false, savingEan: false } : r));
|
||||
} catch {
|
||||
setRows(prev => prev.map(r => r.variant.id === variantId ? { ...r, savingEan: false } : r));
|
||||
}
|
||||
};
|
||||
|
||||
// Filter rows
|
||||
const filtered = rows.filter((r) => {
|
||||
if (filter === 'in_stock') return (r.stock?.availableQuantity ?? 0) >= 5;
|
||||
@@ -211,7 +247,6 @@ export default function InventoryPage() {
|
||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">EAN</th>
|
||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Stock</th>
|
||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Estado</th>
|
||||
<th className="px-4 py-3 font-semibold text-gray-600 text-xs uppercase tracking-wide">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
@@ -220,8 +255,49 @@ export default function InventoryPage() {
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm font-medium text-gray-900">{row.productName}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-gray-600">{row.variant.sku}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-gray-400">{row.variant.ean ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{row.editingSku ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={row.editSkuValue}
|
||||
onChange={e => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editSkuValue: e.target.value } : r))}
|
||||
onBlur={() => handleSaveSku(row.variant.id, row.productId, row.editSkuValue)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveSku(row.variant.id, row.productId, row.editSkuValue); if (e.key === 'Escape') setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingSku: false } : r)); }}
|
||||
disabled={row.savingSku}
|
||||
className="w-full px-2 py-1 border border-[#2D6A4F] rounded text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingSku: true, editSkuValue: row.variant.sku } : r))}
|
||||
title="Clic para editar SKU"
|
||||
className="font-mono text-xs text-gray-600 hover:text-[#2D6A4F] cursor-text text-left w-full truncate block disabled:opacity-50"
|
||||
>
|
||||
{row.variant.sku}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{row.editingEan ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={row.editEanValue}
|
||||
onChange={e => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editEanValue: e.target.value } : r))}
|
||||
onBlur={() => handleSaveEan(row.variant.id, row.productId, row.editEanValue)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveEan(row.variant.id, row.productId, row.editEanValue); if (e.key === 'Escape') setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingEan: false } : r)); }}
|
||||
disabled={row.savingEan}
|
||||
placeholder="—"
|
||||
className="w-full px-2 py-1 border border-[#2D6A4F] rounded text-xs focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setRows(prev => prev.map(r => r.variant.id === row.variant.id ? { ...r, editingEan: true, editEanValue: row.variant.ean ?? '' } : r))}
|
||||
title="Clic para editar EAN"
|
||||
className="font-mono text-xs text-gray-400 hover:text-[#2D6A4F] cursor-text text-left w-full truncate block disabled:opacity-50"
|
||||
>
|
||||
{row.variant.ean ?? '—'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{row.loading ? (
|
||||
<span className="text-gray-300">—</span>
|
||||
@@ -308,36 +384,21 @@ export default function InventoryPage() {
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-gray-900">
|
||||
{row.stock?.availableQuantity ?? '—'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id ? { ...r, editing: true } : r,
|
||||
),
|
||||
)
|
||||
}
|
||||
className="ml-1 text-gray-400 hover:text-[#2D6A4F] text-xs"
|
||||
title="Editar stock"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows(prev =>
|
||||
prev.map(r => r.variant.id === row.variant.id ? { ...r, editing: true } : r))
|
||||
}
|
||||
title="Clic para editar stock"
|
||||
className="font-medium text-gray-900 hover:text-[#2D6A4F] cursor-text disabled:opacity-50"
|
||||
>
|
||||
{row.stock?.availableQuantity ?? '—'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StockBadge qty={row.stock?.availableQuantity ?? 0} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{row.msg && (
|
||||
<span className={`text-xs ${row.msg === '✓' ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{row.msg}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -81,6 +81,8 @@ export const productsApi = {
|
||||
setState: (id: string, state: 'active' | 'archived') =>
|
||||
api.patch(`/api/products/${id}/state`, { state }),
|
||||
delete: (id: string) => api.delete(`/api/products/${id}`),
|
||||
updateVariant: (productId: string, variantId: string, data: unknown) =>
|
||||
api.patch<import('@/types').ProductVariant>(`/api/products/${productId}/variants/${variantId}`, data),
|
||||
};
|
||||
|
||||
// ── Orders ────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user