feat(F-176): completed feature

This commit is contained in:
chattie
2026-08-22 18:49:33 +02:00
parent 8e23602fbb
commit 4081462cc4
12 changed files with 170 additions and 25 deletions

View File

@@ -50,6 +50,7 @@ export default function RegisterPage() {
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [searching, setSearching] = useState(false);
const [searchError, setSearchError] = useState('');
const [selectedItem, setSelectedItem] = useState<CartItem | null>(null);
const [showDiscountPanel, setShowDiscountPanel] = useState(false);
const [customer, setCustomer] = useState<Customer | null>(null);
@@ -112,13 +113,16 @@ export default function RegisterPage() {
};
const doSearch = useCallback(async (q: string) => {
if (q.trim().length < 2) { setSearchResults([]); return; }
if (q.trim().length < 2) { setSearchResults([]); setSearchError(''); return; }
setSearching(true);
setSearchError('');
try {
const res = await posApi.searchProducts(q, config?.session?.storeId) as { items: SearchResult[] };
setSearchResults(res.items ?? []);
} catch { setSearchResults([]); }
finally { setSearching(false); }
} catch (err) {
setSearchResults([]);
setSearchError(err instanceof Error ? err.message : 'No se pudo buscar productos');
} finally { setSearching(false); }
}, [config]);
useEffect(() => {
@@ -324,7 +328,8 @@ export default function RegisterPage() {
)}
{searching && <p className="text-center text-gray-400 py-4">Buscando</p>}
{search && searchResults.length === 0 && !searching && (
{searchError && !searching && <p className="text-center text-red-600 py-4">{searchError}</p>}
{search && searchResults.length === 0 && !searching && !searchError && (
<p className="text-center text-gray-400 py-4">Sin resultados para &quot;{search}&quot;</p>
)}
</div>

View File

@@ -454,17 +454,29 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
if (!q || q.trim().length < 2)
throw new AppError(400, 'INVALID_QUERY', 'Query must be at least 2 characters');
const result = await pool.query(
`SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean,
COALESCE(s.quantity, 0) AS stock, pp.price_cents, c.name AS category, b.name AS brand
`SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean,
COALESCE(s.quantity, 0) AS stock,
COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents,
c.name AS category, b.name AS brand
FROM catalog_product_variants v
JOIN catalog_products p ON p.id = v.product_id
LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id AND store_id = $3 LIMIT 1) s ON true
LEFT JOIN LATERAL (
SELECT COALESCE(SUM(available), 0)::int AS quantity
FROM inventory_stock
WHERE variant_id = v.id AND ($3::uuid IS NULL OR store_id = $3)
) s ON true
LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR'
LEFT JOIN catalog_product_categories pc ON pc.variant_id = v.id AND pc.is_primary = true
LEFT JOIN categories_categories c ON c.id = pc.category_id
LEFT JOIN LATERAL (
SELECT category.name
FROM catalog_product_categories pc
JOIN categories_categories category ON category.id = pc.category_id
WHERE pc.product_id = p.id
ORDER BY category.name
LIMIT 1
) c ON true
LEFT JOIN brands_brands b ON b.id = p.brand_id
WHERE (v.name ILIKE $1 OR v.sku ILIKE $1 OR v.ean = $1) AND v.active = true AND p.active = true
ORDER BY v.name LIMIT $2`,
WHERE (p.name ILIKE $1 OR v.sku ILIKE $1 OR v.ean ILIKE $1) AND p.state = 'active'
ORDER BY p.name LIMIT $2`,
[`%${q.trim()}%`, limit, storeId ?? null],
);
return reply.send({
@@ -498,12 +510,17 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray<Role>);
const { ean } = request.params;
const result = await pool.query(
`SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean, COALESCE(s.quantity, 0) AS stock, pp.price_cents
`SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean,
COALESCE(s.quantity, 0) AS stock,
COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents
FROM catalog_product_variants v
JOIN catalog_products p ON p.id = v.product_id
LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id LIMIT 1) s ON true
LEFT JOIN LATERAL (
SELECT COALESCE(SUM(available), 0)::int AS quantity
FROM inventory_stock WHERE variant_id = v.id
) s ON true
LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR'
WHERE v.ean = $1 AND v.active = true AND p.active = true LIMIT 1`,
WHERE v.ean = $1 AND p.state = 'active' LIMIT 1`,
[ean],
);
if (!result.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Product not found');
@@ -535,12 +552,17 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray<Role>);
const { sku } = request.params;
const result = await pool.query(
`SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean, COALESCE(s.quantity, 0) AS stock, pp.price_cents
`SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean,
COALESCE(s.quantity, 0) AS stock,
COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents
FROM catalog_product_variants v
JOIN catalog_products p ON p.id = v.product_id
LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id LIMIT 1) s ON true
LEFT JOIN LATERAL (
SELECT COALESCE(SUM(available), 0)::int AS quantity
FROM inventory_stock WHERE variant_id = v.id
) s ON true
LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR'
WHERE v.sku = $1 AND v.active = true AND p.active = true LIMIT 1`,
WHERE v.sku = $1 AND p.state = 'active' LIMIT 1`,
[sku],
);
if (!result.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Product not found');