import { useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@astryxdesign/core/Button'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Text } from '@astryxdesign/core/Text'; import { VStack } from '@astryxdesign/core/VStack'; import { Minus, Plus, X } from 'lucide-react'; import { errorMessage } from '@/api/client'; import { stockApi } from '@/api/stock'; import type { Product } from '@/api/types'; import { Drawer } from '@/features/store-admin/Drawer'; import { queryKeys } from '@/queries/keys'; /** * Asking for several products at once. * * Restocking after a delivery is one errand. Doing it a product at a time means * a dialog per line, and the branch either gives up halfway or raises twenty * separate requests the admin then has to decide twenty times. * * ── Why the quantities are still per product ──────────────────────────────── * * The obvious shortcut is one quantity for the whole batch. It is wrong: a shop * asks for two crates of oil and twenty packets of biscuits, and a single box * would either be ignored or produce a request nobody meant. So every line * keeps its own number, defaulted to 1 and adjustable in place. * * A line can be dropped here too. Picking eight and realising one was a mistake * should not mean closing this and starting the selection again. */ export interface BulkRequestDialogProps { products: Product[]; tenantid: number; locationid: number; locationname: string; onClose: () => void; /** Called when at least one request was raised, so the caller can clear its selection. */ onDone: () => void; } export function BulkRequestDialog({ products, tenantid, locationid, locationname, onClose, onDone, }: BulkRequestDialogProps) { const client = useQueryClient(); const [qty, setQty] = useState>(() => Object.fromEntries(products.map((product) => [product.productid, 1])), ); const [dropped, setDropped] = useState>(() => new Set()); const [problem, setProblem] = useState(null); const lines = products.filter((product) => !dropped.has(product.productid)); const send = useMutation({ mutationFn: () => stockApi.createMany( lines.map((product) => ({ tenantid, locationid, productid: product.productid, qty: qty[product.productid] ?? 1, locationname, productname: product.productname ?? '', })), ), onSuccess: async (result) => { const failed = result?.failed?.length ?? 0; await client.invalidateQueries({ queryKey: queryKeys.stock.all }); if (failed > 0) { // Some landed. Saying so beats a bare success that hides the gap, and // beats an error that implies none of them were raised. setProblem( `${lines.length - failed} of ${lines.length} raised. The rest could not be — try them again.`, ); return; } onDone(); onClose(); }, onError: (cause) => setProblem(errorMessage(cause)), }); const setLine = (productid: number, next: number) => setQty((prev) => ({ ...prev, [productid]: Math.max(1, Math.min(9999, next)) })); return ( Set how much of each you need. Your admin decides each one, and nothing reaches the shelf until they approve it.
{lines.map((product) => (
{product.productname ?? `Product ${product.productid}`} {[product.unitvalue, product.productunit].filter(Boolean).join(' ') || '—'} setLine(product.productid, Number(event.target.value.replace(/\D/g, '')) || 1) } />
))}
{problem ? ( {problem} ) : null}