diff --git a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx index fd9fb9c..e941765 100644 --- a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx @@ -16,16 +16,18 @@ import { SelectValue, } from '@/components/ui/select' import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { toast } from '@/components/ui/toast' import { shopPos as shopPosApi, items as itemsApi, priceLists as priceListsApi, + users as usersApi, getApiError, getPaginatedItems, } from '@/lib/api/index' -import type { ItemSummaryResponse, ShopPOResponse } from '@/types/api' +import type { ItemSummaryResponse, ShopPOResponse, UserResponse } from '@/types/api' const lineItemSchema = z.object({ itemId: z.string().min(1, 'Item is required'), @@ -51,6 +53,9 @@ export default function EditShopPOPage() { const [po, setPo] = useState(null) const [itemsList, setItemsList] = useState([]) const [loading, setLoading] = useState(true) + const [usersList, setUsersList] = useState([]) + const [confirmedByUserId, setConfirmedByUserId] = useState('') + const [confirming, setConfirming] = useState(false) const form = useForm({ resolver: zodResolver(schema), @@ -71,10 +76,11 @@ export default function EditShopPOPage() { ) useEffect(() => { - Promise.all([shopPosApi.get(id), itemsApi.list({ pageSize: 200 })]) - .then(([poRes, items]) => { + Promise.all([shopPosApi.get(id), itemsApi.list({ pageSize: 200 }), usersApi.list({ pageSize: 200 })]) + .then(([poRes, items, users]) => { setPo(poRes) setItemsList(getPaginatedItems(items)) + setUsersList(getPaginatedItems(users)) form.reset({ items: poRes.items .filter((i) => i.isActive) @@ -88,6 +94,20 @@ export default function EditShopPOPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]) + async function handleConfirmPO() { + if (!po || !confirmedByUserId) return + setConfirming(true) + try { + const updated = await shopPosApi.confirm(po.id, confirmedByUserId) + setPo(updated) + toast.add({ title: 'Shop PO confirmed', type: 'success' }) + } catch (err) { + toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' }) + } finally { + setConfirming(false) + } + } + async function resolveAndSetPrice(index: number, itemId: string) { const today = new Date().toISOString().slice(0, 10) if (po) { @@ -126,7 +146,14 @@ export default function EditShopPOPage() { Back
-

Edit Shop PO

+
+

Edit Shop PO

+ {po.confirmedAt ? ( + Confirmed + ) : ( + Unconfirmed + )} +

{po.storeName} — items can only be changed before the PO is confirmed.

@@ -136,6 +163,7 @@ export default function EditShopPOPage() { {locked && (

This Shop PO is {po.confirmedAt ? 'confirmed' : 'cancelled'} and can no longer be edited. + {po.confirmedAt && po.confirmedByName && ` Confirmed by ${po.confirmedByName}.`}

)} @@ -271,6 +299,35 @@ export default function EditShopPOPage() { Cancel
+ + {!locked && ( + <> + +

Confirm PO

+

+ Select who is confirming this PO, then confirm it. +

+
+ + +
+ + )} diff --git a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx index 6443765..d2704bb 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx @@ -27,7 +27,6 @@ import { } from '@/lib/api/index' import type { ShopPOResponse, ShopPOStatus, UserResponse, RouteResponse, StoreResponse } from '@/types/api' -import { ConfirmShopPODialog } from './confirm-shop-po-dialog' import { CancelShopPODialog } from './cancel-shop-po-dialog' const ALL = '__all__' @@ -63,8 +62,6 @@ export function SalesClient() { const [shopId, setShopId] = useState('') const [confirmedFilter, setConfirmedFilter] = useState('all') - const [confirmOpen, setConfirmOpen] = useState(false) - const [selectedPO, setSelectedPO] = useState(null) const [cancelOpen, setCancelOpen] = useState(false) const [cancelPO, setCancelPO] = useState(null) @@ -124,8 +121,7 @@ export function SalesClient() { } function handleConfirm(row: ShopPOResponse) { - setSelectedPO(row) - setConfirmOpen(true) + router.push(`/sales/${row.id}/edit`) } function handleCancel(row: ShopPOResponse) { @@ -194,14 +190,15 @@ export function SalesClient() { minWidth: 160, cellRender: (row) => row.confirmedByName ? ( -
+
+ Confirmed {row.confirmedByName} {row.confirmedByRole && ( {row.confirmedByRole} )}
) : ( - — + Unconfirmed ), }, { @@ -365,12 +362,6 @@ export function SalesClient() { }} /> - setConfirmOpen(false)} - onSuccess={() => fetchData(page, pageSize)} - po={selectedPO} - /> setCancelOpen(false)} diff --git a/distribution_fe/app/(dashboard)/sales/new/page.tsx b/distribution_fe/app/(dashboard)/sales/new/page.tsx index b0f15d3..aecada7 100644 --- a/distribution_fe/app/(dashboard)/sales/new/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/new/page.tsx @@ -211,7 +211,7 @@ export default function NewShopPOPage() {

- diff --git a/distribution_fe/components/ui/select.tsx b/distribution_fe/components/ui/select.tsx index fbb1cd0..8b25b53 100644 --- a/distribution_fe/components/ui/select.tsx +++ b/distribution_fe/components/ui/select.tsx @@ -5,7 +5,9 @@ import { Select as SelectPrimitive } from "@base-ui/react/select" import { cn } from "@/lib/utils" import { HugeiconsIcon } from "@hugeicons/react" -import { UnfoldMoreIcon, Tick02Icon, ArrowUp01Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons" +import { UnfoldMoreIcon, Tick02Icon, ArrowUp01Icon, ArrowDown01Icon, Search01Icon } from "@hugeicons/core-free-icons" + +const SEARCH_THRESHOLD = 7 function collectSelectItems(children: React.ReactNode): Array<{ value: unknown; label: string }> { const items: Array<{ value: unknown; label: string }> = [] @@ -66,6 +68,57 @@ function getItemLabel(children: React.ReactNode): string | undefined { return undefined } +function filterSelectChildren(children: React.ReactNode, query: string): React.ReactNode { + const normalizedQuery = query.trim().toLowerCase() + if (!normalizedQuery) return children + + const filtered = React.Children.map(children, (child) => { + if (!React.isValidElement<{ value?: unknown; label?: string; children?: React.ReactNode }>(child)) { + return child + } + + if ("value" in child.props) { + const label = child.props.label ?? getItemLabel(child.props.children) ?? "" + return label.toLowerCase().includes(normalizedQuery) ? child : null + } + + if (child.props.children) { + const filteredChildren = filterSelectChildren(child.props.children, query) + if (React.Children.count(filteredChildren) === 0) return null + return React.cloneElement(child, undefined, filteredChildren) + } + + return child + }) + + return filtered +} + +function SelectSearchInput({ + query, + onQueryChange, +}: { + query: string + onQueryChange: (value: string) => void +}) { + return ( +
+ + onQueryChange(event.target.value)} + onKeyDown={(event) => event.stopPropagation()} + placeholder="Search..." + className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ ) +} + function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { return ( ) { + const [query, setQuery] = React.useState("") + const itemCount = React.useMemo(() => collectSelectItems(children).length, [children]) + const showSearch = itemCount > SEARCH_THRESHOLD + const filteredChildren = showSearch ? filterSelectChildren(children, query) : children + return ( + {showSearch && } - {children} + + {React.Children.count(filteredChildren) === 0 ? ( +
No results found.
+ ) : ( + filteredChildren + )} +