feat(F-082): completed feature
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { productsApi, inventoryApi } from '@/lib/api-client';
|
||||
import type { Product, ProductVariant, StockAvailability } from '@/types';
|
||||
import type { ProductVariant, StockAvailability } from '@/types';
|
||||
|
||||
interface VariantRow {
|
||||
productId: string;
|
||||
@@ -140,6 +140,56 @@ export default function InventoryPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Save Stock inline (used by Stock cell on blur/Enter)
|
||||
const saveStockInline = async (variantId: string, value: string) => {
|
||||
const qty = parseInt(value, 10);
|
||||
if (isNaN(qty) || qty < 0) {
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === variantId
|
||||
? { ...r, editing: false, editValue: String(r.stock?.availableQuantity ?? 0), msg: 'Error' }
|
||||
: r,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setRows((prev) => prev.map((r) => (r.variant.id === variantId ? { ...r, saving: true } : r)));
|
||||
try {
|
||||
const result = await inventoryApi.setStock(variantId, qty);
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === variantId
|
||||
? {
|
||||
...r,
|
||||
stock: { available: result.available > 0, availableQuantity: result.available },
|
||||
editing: false,
|
||||
saving: false,
|
||||
editValue: String(result.available),
|
||||
msg: '✓ Guardado',
|
||||
}
|
||||
: 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,
|
||||
saving: false,
|
||||
editing: false,
|
||||
editValue: String(r.stock?.availableQuantity ?? 0),
|
||||
msg: 'Error',
|
||||
}
|
||||
: r,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter rows
|
||||
const filtered = rows.filter((r) => {
|
||||
if (filter === 'in_stock') return (r.stock?.availableQuantity ?? 0) >= 5;
|
||||
@@ -302,8 +352,9 @@ export default function InventoryPage() {
|
||||
{row.loading ? (
|
||||
<span className="text-gray-300">—</span>
|
||||
) : row.editing ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
autoFocus
|
||||
type="number"
|
||||
min={0}
|
||||
value={row.editValue}
|
||||
@@ -316,78 +367,41 @@ export default function InventoryPage() {
|
||||
),
|
||||
)
|
||||
}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded-lg text-sm focus:ring-1 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const qty = parseInt(row.editValue, 10);
|
||||
if (isNaN(qty) || qty < 0) return;
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id ? { ...r, saving: true } : r,
|
||||
),
|
||||
);
|
||||
try {
|
||||
const result = await inventoryApi.setStock(row.variant.id, qty);
|
||||
onBlur={() => saveStockInline(row.variant.id, row.editValue)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveStockInline(row.variant.id, row.editValue);
|
||||
if (e.key === 'Escape')
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id
|
||||
? {
|
||||
...r,
|
||||
stock: { available: result.available > 0, availableQuantity: result.available },
|
||||
editing: false,
|
||||
saving: false,
|
||||
msg: '✓',
|
||||
editValue: String(r.stock?.availableQuantity ?? 0),
|
||||
}
|
||||
: r,
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id ? { ...r, msg: '' } : r,
|
||||
),
|
||||
);
|
||||
}, 3000);
|
||||
} catch {
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id
|
||||
? { ...r, saving: false, msg: 'Error' }
|
||||
: r,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={row.saving}
|
||||
className="px-2 py-1 bg-[#2D6A4F] text-white text-xs rounded-lg hover:bg-[#1B4332] disabled:opacity-50"
|
||||
>
|
||||
{row.saving ? '...' : 'OK'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id
|
||||
? {
|
||||
...r,
|
||||
editing: false,
|
||||
editValue: String(r.stock?.availableQuantity ?? 0),
|
||||
}
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
className="text-gray-400 hover:text-gray-600 text-xs"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded-lg text-sm focus:ring-1 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
{row.msg && (
|
||||
<span className={`text-xs ${row.msg === '✓' || row.msg === '✓ Guardado' ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{row.msg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows(prev =>
|
||||
prev.map(r => r.variant.id === row.variant.id ? { ...r, editing: true } : r))
|
||||
setRows((prev) =>
|
||||
prev.map((r) =>
|
||||
r.variant.id === row.variant.id
|
||||
? { ...r, editing: true, editValue: String(r.stock?.availableQuantity ?? 0) }
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
title="Clic para editar stock"
|
||||
className="font-medium text-gray-900 hover:text-[#2D6A4F] cursor-text disabled:opacity-50"
|
||||
|
||||
@@ -11,8 +11,14 @@ interface VariantRow {
|
||||
loadingPrice: boolean;
|
||||
editingStock: boolean;
|
||||
editingPrice: boolean;
|
||||
editingSku: boolean;
|
||||
editingEan: boolean;
|
||||
savingSku: boolean;
|
||||
savingEan: boolean;
|
||||
stockValue: string;
|
||||
priceValue: string;
|
||||
skuValue: string;
|
||||
eanValue: string;
|
||||
vatRate: 'general' | 'reduced' | 'super-reduced';
|
||||
}
|
||||
|
||||
@@ -87,8 +93,14 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
loadingPrice: true,
|
||||
editingStock: false,
|
||||
editingPrice: false,
|
||||
editingSku: false,
|
||||
editingEan: false,
|
||||
savingSku: false,
|
||||
savingEan: false,
|
||||
stockValue: '',
|
||||
priceValue: '',
|
||||
skuValue: variant.sku,
|
||||
eanValue: variant.ean ?? '',
|
||||
vatRate: 'general',
|
||||
};
|
||||
}
|
||||
@@ -195,7 +207,18 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
const saveStock = async (variantId: string) => {
|
||||
const r = rows[variantId];
|
||||
const qty = parseInt(r.stockValue, 10);
|
||||
if (isNaN(qty) || qty < 0) return;
|
||||
if (isNaN(qty) || qty < 0) {
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...r,
|
||||
editingStock: false,
|
||||
stockValue: String(r.stock?.availableQuantity ?? 0),
|
||||
},
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
return;
|
||||
}
|
||||
setSavingVariant(variantId);
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
@@ -209,12 +232,21 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
availableQuantity: result.available,
|
||||
},
|
||||
editingStock: false,
|
||||
stockValue: String(result.available),
|
||||
},
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...prev[variantId],
|
||||
editingStock: false,
|
||||
stockValue: String(r.stock?.availableQuantity ?? 0),
|
||||
},
|
||||
}));
|
||||
} finally {
|
||||
setSavingVariant(null);
|
||||
}
|
||||
@@ -223,7 +255,18 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
const savePrice = async (variantId: string) => {
|
||||
const r = rows[variantId];
|
||||
const cents = eurToCents(r.priceValue);
|
||||
if (cents < 0) return;
|
||||
if (cents < 0) {
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...r,
|
||||
editingPrice: false,
|
||||
priceValue: centsToEur(r.price?.netUnitAmountCents ?? 0),
|
||||
},
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
return;
|
||||
}
|
||||
setSavingVariant(variantId);
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
@@ -241,11 +284,89 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...prev[variantId],
|
||||
editingPrice: false,
|
||||
priceValue: centsToEur(r.price?.netUnitAmountCents ?? 0),
|
||||
},
|
||||
}));
|
||||
} finally {
|
||||
setSavingVariant(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSku = async (variantId: string, productId: string) => {
|
||||
const r = rows[variantId];
|
||||
const newSku = r.skuValue.trim();
|
||||
if (!newSku || newSku === r.variant.sku) {
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...r, editingSku: false, skuValue: r.variant.sku },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingSku: true } }));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
const updated = await productsApi.updateVariant(productId, variantId, { sku: newSku });
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...prev[variantId],
|
||||
variant: { ...prev[variantId].variant, sku: updated.sku },
|
||||
editingSku: false,
|
||||
savingSku: false,
|
||||
skuValue: updated.sku,
|
||||
},
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...prev[variantId], editingSku: false, savingSku: false, skuValue: prev[variantId].variant.sku },
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const saveEan = async (variantId: string, productId: string) => {
|
||||
const r = rows[variantId];
|
||||
const newEan = r.eanValue.trim();
|
||||
if (newEan === (r.variant.ean ?? '')) {
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...r, editingEan: false, eanValue: r.variant.ean ?? '' },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingEan: true } }));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
const updated = await productsApi.updateVariant(productId, variantId, { ean: newEan || null });
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: {
|
||||
...prev[variantId],
|
||||
variant: { ...prev[variantId].variant, ean: updated.ean },
|
||||
editingEan: false,
|
||||
savingEan: false,
|
||||
eanValue: updated.ean ?? '',
|
||||
},
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...prev[variantId], editingEan: false, savingEan: false, eanValue: prev[variantId].variant.ean ?? '' },
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingVariants) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-8 text-gray-400 text-sm">
|
||||
@@ -302,10 +423,97 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
return (
|
||||
<tr key={variant.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
{/* SKU */}
|
||||
<td className="px-4 py-3 font-mono text-xs text-gray-600">{variant.sku}</td>
|
||||
<td className="px-4 py-3">
|
||||
{r.editingSku ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={r.skuValue}
|
||||
onChange={(e) =>
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: { ...prev[variant.id], skuValue: e.target.value },
|
||||
}))
|
||||
}
|
||||
onBlur={() => saveSku(variant.id, productId)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveSku(variant.id, productId);
|
||||
if (e.key === 'Escape')
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: {
|
||||
...prev[variant.id],
|
||||
editingSku: false,
|
||||
skuValue: prev[variant.id].variant.sku,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
disabled={r.savingSku}
|
||||
className="w-full px-2 py-1 border border-[#2D6A4F] rounded text-xs font-mono focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: { ...prev[variant.id], editingSku: true, skuValue: variant.sku },
|
||||
}))
|
||||
}
|
||||
title="Clic para editar SKU"
|
||||
className="font-mono text-xs text-gray-600 hover:text-[#2D6A4F] cursor-text text-left w-full truncate block"
|
||||
>
|
||||
{variant.sku}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* EAN */}
|
||||
<td className="px-4 py-3 font-mono text-xs text-gray-500">{variant.ean ?? '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{r.editingEan ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={r.eanValue}
|
||||
onChange={(e) =>
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: { ...prev[variant.id], eanValue: e.target.value },
|
||||
}))
|
||||
}
|
||||
onBlur={() => saveEan(variant.id, productId)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEan(variant.id, productId);
|
||||
if (e.key === 'Escape')
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: {
|
||||
...prev[variant.id],
|
||||
editingEan: false,
|
||||
eanValue: prev[variant.id].variant.ean ?? '',
|
||||
},
|
||||
}));
|
||||
}}
|
||||
disabled={r.savingEan}
|
||||
placeholder="—"
|
||||
className="w-full px-2 py-1 border border-[#2D6A4F] rounded text-xs font-mono focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() =>
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variant.id]: {
|
||||
...prev[variant.id],
|
||||
editingEan: true,
|
||||
eanValue: variant.ean ?? '',
|
||||
},
|
||||
}))
|
||||
}
|
||||
title="Clic para editar EAN"
|
||||
className="font-mono text-xs text-gray-500 hover:text-[#2D6A4F] cursor-text text-left w-full truncate block"
|
||||
>
|
||||
{variant.ean ?? '—'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Precio */}
|
||||
<td className="px-4 py-3">
|
||||
@@ -315,6 +523,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-400">€</span>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={r.priceValue}
|
||||
@@ -324,24 +533,23 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
[variant.id]: { ...prev[variant.id], priceValue: e.target.value },
|
||||
}))
|
||||
}
|
||||
onBlur={() => savePrice(variant.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') savePrice(variant.id);
|
||||
if (e.key === 'Escape')
|
||||
cancelEditPrice(variant.id);
|
||||
}}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded-lg text-sm focus:ring-1 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-gray-900">
|
||||
{r.price ? formatCents(r.price.netUnitAmountCents) : '—'}
|
||||
</span>
|
||||
{r.price && (
|
||||
<button
|
||||
onClick={() => startEditPrice(variant.id)}
|
||||
className="ml-1 text-gray-400 hover:text-[#2D6A4F] text-xs"
|
||||
title="Editar precio"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => startEditPrice(variant.id)}
|
||||
title="Clic para editar precio neto"
|
||||
className="font-medium text-gray-900 hover:text-[#2D6A4F] cursor-text text-left"
|
||||
>
|
||||
{r.price ? formatCents(r.price.netUnitAmountCents) : '—'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -379,6 +587,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
) : r.editingStock ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
autoFocus
|
||||
type="number"
|
||||
min={0}
|
||||
value={r.stockValue}
|
||||
@@ -388,35 +597,23 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
[variant.id]: { ...prev[variant.id], stockValue: e.target.value },
|
||||
}))
|
||||
}
|
||||
className="w-16 px-2 py-1 border border-gray-300 rounded-lg text-sm focus:ring-1 focus:ring-[#2D6A4F] outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveStock(variant.id)}
|
||||
onBlur={() => saveStock(variant.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveStock(variant.id);
|
||||
if (e.key === 'Escape') cancelEditStock(variant.id);
|
||||
}}
|
||||
disabled={savingVariant === variant.id}
|
||||
className="px-2 py-1 bg-[#2D6A4F] text-white text-xs rounded-lg hover:bg-[#1B4332] disabled:opacity-50"
|
||||
>
|
||||
{savingVariant === variant.id ? '...' : 'OK'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => cancelEditStock(variant.id)}
|
||||
className="text-gray-400 hover:text-gray-600 text-xs"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
className="w-16 px-2 py-1 border border-gray-300 rounded-lg text-sm focus:ring-1 focus:ring-[#2D6A4F] outline-none disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-gray-900">
|
||||
{r.stock?.availableQuantity ?? '—'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => startEditStock(variant.id)}
|
||||
className="ml-1 text-gray-400 hover:text-[#2D6A4F] text-xs"
|
||||
title="Editar stock"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => startEditStock(variant.id)}
|
||||
title="Clic para editar stock"
|
||||
className="font-medium text-gray-900 hover:text-[#2D6A4F] cursor-text"
|
||||
>
|
||||
{r.stock?.availableQuantity ?? '—'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -427,24 +624,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
available={r.stock?.available ?? false}
|
||||
quantity={r.stock?.availableQuantity ?? 0}
|
||||
/>
|
||||
{r.editingPrice && (
|
||||
<button
|
||||
onClick={() => savePrice(variant.id)}
|
||||
disabled={savingVariant === variant.id}
|
||||
className="px-2 py-1 bg-[#2D6A4F] text-white text-xs rounded-lg hover:bg-[#1B4332] disabled:opacity-50"
|
||||
>
|
||||
{savingVariant === variant.id ? '...' : 'OK'}
|
||||
</button>
|
||||
)}
|
||||
{r.editingPrice && (
|
||||
<button
|
||||
onClick={() => cancelEditPrice(variant.id)}
|
||||
className="text-gray-400 hover:text-gray-600 text-xs"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
{saveMsg[variant.id] && !r.editingStock && !r.editingPrice && (
|
||||
{saveMsg[variant.id] && !r.editingStock && !r.editingPrice && !r.editingSku && !r.editingEan && (
|
||||
<span className={`text-xs ${saveMsg[variant.id].startsWith('✓') ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{saveMsg[variant.id]}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user