feat(F-091): completed feature
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { productsApi, inventoryApi, pricingApi } from '@/lib/api-client';
|
||||
import type { ProductVariant, VariantPrice, StockAvailability } from '@/types';
|
||||
import { ApiError, type ProductVariant, type VariantPrice, type StockAvailability } from '@/types';
|
||||
|
||||
interface VariantRow {
|
||||
variant: ProductVariant;
|
||||
@@ -75,6 +75,7 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
const [rows, setRows] = useState<Record<string, VariantRow>>({});
|
||||
const [savingVariant, setSavingVariant] = useState<string | null>(null);
|
||||
const [saveMsg, setSaveMsg] = useState<Record<string, string>>({});
|
||||
const savingCodes = useRef(new Set<string>());
|
||||
|
||||
// Load variants
|
||||
useEffect(() => {
|
||||
@@ -307,6 +308,9 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const saveKey = `sku:${variantId}`;
|
||||
if (savingCodes.current.has(saveKey)) return;
|
||||
savingCodes.current.add(saveKey);
|
||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingSku: true } }));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
@@ -323,12 +327,17 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
} catch (error) {
|
||||
const message = error instanceof ApiError && error.statusCode === 409
|
||||
? 'El SKU ya existe en otra variante'
|
||||
: 'No se pudo guardar el SKU';
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: message }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...prev[variantId], editingSku: false, savingSku: false, skuValue: prev[variantId].variant.sku },
|
||||
}));
|
||||
} finally {
|
||||
savingCodes.current.delete(saveKey);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -342,6 +351,9 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const saveKey = `ean:${variantId}`;
|
||||
if (savingCodes.current.has(saveKey)) return;
|
||||
savingCodes.current.add(saveKey);
|
||||
setRows((prev) => ({ ...prev, [variantId]: { ...r, savingEan: true } }));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '' }));
|
||||
try {
|
||||
@@ -358,12 +370,17 @@ export function InventorySection({ productId }: InventorySectionProps) {
|
||||
}));
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: '✓ Guardado' }));
|
||||
setTimeout(() => setSaveMsg((prev) => ({ ...prev, [variantId]: '' })), 3000);
|
||||
} catch {
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: 'Error' }));
|
||||
} catch (error) {
|
||||
const message = error instanceof ApiError && error.statusCode === 409
|
||||
? 'El EAN ya existe en otra variante'
|
||||
: 'No se pudo guardar el EAN';
|
||||
setSaveMsg((prev) => ({ ...prev, [variantId]: message }));
|
||||
setRows((prev) => ({
|
||||
...prev,
|
||||
[variantId]: { ...prev[variantId], editingEan: false, savingEan: false, eanValue: prev[variantId].variant.ean ?? '' },
|
||||
}));
|
||||
} finally {
|
||||
savingCodes.current.delete(saveKey);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -19,9 +19,11 @@ export class ProductBrandNotFoundError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export type ProductVariantCode = 'sku' | 'ean';
|
||||
|
||||
export class ProductVariantCodeAlreadyExistsError extends Error {
|
||||
constructor() {
|
||||
super('Product variant SKU or EAN already exists');
|
||||
constructor(public readonly field?: ProductVariantCode) {
|
||||
super(field ? `Product variant ${field.toUpperCase()} already exists` : 'Product variant SKU or EAN already exists');
|
||||
this.name = 'ProductVariantCodeAlreadyExistsError';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type pg from 'pg';
|
||||
import { ProductVariantCodeAlreadyExistsError } from '../domain/errors.js';
|
||||
import {
|
||||
ProductVariantCodeAlreadyExistsError,
|
||||
type ProductVariantCode,
|
||||
} from '../domain/errors.js';
|
||||
import type { ProductVariantRepository } from '../domain/ports.js';
|
||||
import type { NewProductVariant, ProductVariant, ProductVariantPatch } from '../domain/variant.js';
|
||||
|
||||
@@ -47,7 +50,7 @@ export class PgProductVariantRepository implements ProductVariantRepository {
|
||||
return toVariant(row);
|
||||
} catch (error) {
|
||||
if (isPgError(error, UNIQUE_VIOLATION)) {
|
||||
throw new ProductVariantCodeAlreadyExistsError();
|
||||
throw new ProductVariantCodeAlreadyExistsError(uniqueConstraintField(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -82,7 +85,7 @@ export class PgProductVariantRepository implements ProductVariantRepository {
|
||||
return row ? toVariant(row) : undefined;
|
||||
} catch (error) {
|
||||
if (isPgError(error, UNIQUE_VIOLATION)) {
|
||||
throw new ProductVariantCodeAlreadyExistsError();
|
||||
throw new ProductVariantCodeAlreadyExistsError(uniqueConstraintField(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -116,3 +119,14 @@ function toVariant(row: VariantRow): ProductVariant {
|
||||
function isPgError(error: unknown, code: string): boolean {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
||||
}
|
||||
|
||||
function uniqueConstraintField(error: unknown): ProductVariantCode | undefined {
|
||||
if (typeof error !== 'object' || error === null || !('constraint' in error)) {
|
||||
return undefined;
|
||||
}
|
||||
const constraint = error.constraint;
|
||||
if (typeof constraint !== 'string') return undefined;
|
||||
if (constraint.endsWith('_sku_key')) return 'sku';
|
||||
if (constraint.endsWith('_ean_key')) return 'ean';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user