Add user selection to Shop PO flow

This commit is contained in:
2026-09-23 12:47:53 +05:30
parent 4b95e03e05
commit ee672da9f7
6 changed files with 202 additions and 50 deletions
@@ -10,6 +10,8 @@ import { toast } from '@/components/ui/toast'
import { shopPos as shopPosApi, getApiError } from '@/lib/api/index' import { shopPos as shopPosApi, getApiError } from '@/lib/api/index'
import type { ShopPOResponse, ShopPOStatus } from '@/types/api' import type { ShopPOResponse, ShopPOStatus } from '@/types/api'
import { ConfirmShopPODialog } from '../_components/confirm-shop-po-dialog'
const PO_STATUS_VARIANT: Record<ShopPOStatus, 'default' | 'secondary' | 'destructive'> = { const PO_STATUS_VARIANT: Record<ShopPOStatus, 'default' | 'secondary' | 'destructive'> = {
Pending: 'secondary', Pending: 'secondary',
PartiallyFulfilled: 'secondary', PartiallyFulfilled: 'secondary',
@@ -41,6 +43,7 @@ export default function ShopPOViewPage() {
const params = useParams<{ id: string }>() const params = useParams<{ id: string }>()
const [po, setPo] = useState<ShopPOResponse | null>(null) const [po, setPo] = useState<ShopPOResponse | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [confirmOpen, setConfirmOpen] = useState(false)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -62,17 +65,6 @@ export default function ShopPOViewPage() {
} }
}, [params.id, router]) }, [params.id, router])
async function handleConfirm() {
if (!po) return
try {
const updated = await shopPosApi.confirm(po.id)
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' })
}
}
async function handleCancel() { async function handleCancel() {
if (!po) return if (!po) return
try { try {
@@ -123,7 +115,7 @@ export default function ShopPOViewPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!po.confirmedAt && po.status !== 'Cancelled' && ( {!po.confirmedAt && po.status !== 'Cancelled' && (
<Button type="button" onClick={handleConfirm}> <Button type="button" onClick={() => setConfirmOpen(true)}>
Confirm PO Confirm PO
</Button> </Button>
)} )}
@@ -211,6 +203,15 @@ export default function ShopPOViewPage() {
</table> </table>
</div> </div>
</div> </div>
<ConfirmShopPODialog
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onSuccess={() => {
shopPosApi.get(po.id).then(setPo)
}}
po={po}
/>
</div> </div>
) )
} }
@@ -0,0 +1,111 @@
'use client'
import { useEffect, useState } from 'react'
import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { FieldGroup } from '@/components/ui/field'
import { toast } from '@/components/ui/toast'
import { shopPos as shopPosApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { ShopPOResponse, UserResponse } from '@/types/api'
const schema = z.object({
confirmedByUserId: z.string().min(1, 'Confirmed by is required'),
})
type FormValues = z.infer<typeof schema>
interface ConfirmShopPODialogProps {
open: boolean
onClose: () => void
onSuccess: () => void
po: ShopPOResponse | null
}
export function ConfirmShopPODialog({ open, onClose, onSuccess, po }: ConfirmShopPODialogProps) {
const [usersList, setUsersList] = useState<UserResponse[]>([])
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { confirmedByUserId: '' },
})
useEffect(() => {
if (!open) return
form.reset({ confirmedByUserId: '' })
usersApi.list({ pageSize: 200 })
.then((res) => setUsersList(getPaginatedItems(res)))
.catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' }))
}, [open])
if (!po) return null
async function onSubmit(values: FormValues) {
try {
await shopPosApi.confirm(po!.id, values.confirmedByUserId)
toast.add({ title: 'Shop PO confirmed', type: 'success' })
onSuccess()
onClose()
} catch (err) {
toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' })
}
}
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Confirm Shop PO</DialogTitle>
<DialogDescription>{po.storeName} — Rs. {po.totalAmount.toLocaleString()}</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<FieldGroup>
<FormField
control={form.control}
name="confirmedByUserId"
label="Confirmed By"
description="Temporary — required until auth resolves the logged-in user automatically."
required
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Select user..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="" disabled>Select user...</SelectItem>
{usersList.map((user) => (
<SelectItem key={user.id} value={user.id}>{user.fullName}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</FieldGroup>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>Cancel</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Confirming…' : 'Confirm PO'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
@@ -27,6 +27,8 @@ import {
} from '@/lib/api/index' } from '@/lib/api/index'
import type { ShopPOResponse, ShopPOStatus, UserResponse, RouteResponse, StoreResponse } from '@/types/api' import type { ShopPOResponse, ShopPOStatus, UserResponse, RouteResponse, StoreResponse } from '@/types/api'
import { ConfirmShopPODialog } from './confirm-shop-po-dialog'
const ALL = '__all__' const ALL = '__all__'
type ConfirmedFilter = 'all' | 'confirmed' | 'unconfirmed' type ConfirmedFilter = 'all' | 'confirmed' | 'unconfirmed'
@@ -60,6 +62,9 @@ export function SalesClient() {
const [shopId, setShopId] = useState('') const [shopId, setShopId] = useState('')
const [confirmedFilter, setConfirmedFilter] = useState<ConfirmedFilter>('all') const [confirmedFilter, setConfirmedFilter] = useState<ConfirmedFilter>('all')
const [confirmOpen, setConfirmOpen] = useState(false)
const [selectedPO, setSelectedPO] = useState<ShopPOResponse | null>(null)
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
usersApi.list({ pageSize: 200 }), usersApi.list({ pageSize: 200 }),
@@ -111,14 +116,9 @@ export function SalesClient() {
router.push(`/sales/${row.id}`) router.push(`/sales/${row.id}`)
} }
async function handleConfirm(row: ShopPOResponse) { function handleConfirm(row: ShopPOResponse) {
try { setSelectedPO(row)
await shopPosApi.confirm(row.id) setConfirmOpen(true)
toast.add({ title: 'Shop PO confirmed', type: 'success' })
fetchData(page, pageSize)
} catch (err) {
toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' })
}
} }
async function handleCancel(row: ShopPOResponse) { async function handleCancel(row: ShopPOResponse) {
@@ -316,6 +316,13 @@ export function SalesClient() {
setPageSize(p.pageSize) setPageSize(p.pageSize)
}} }}
/> />
<ConfirmShopPODialog
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onSuccess={() => fetchData(page, pageSize)}
po={selectedPO}
/>
</> </>
) )
} }
@@ -24,10 +24,11 @@ import {
stores as storesApi, stores as storesApi,
items as itemsApi, items as itemsApi,
priceLists as priceListsApi, priceLists as priceListsApi,
users as usersApi,
getApiError, getApiError,
getPaginatedItems, getPaginatedItems,
} from '@/lib/api/index' } from '@/lib/api/index'
import type { ItemSummaryResponse, StoreResponse } from '@/types/api' import type { ItemSummaryResponse, StoreResponse, UserResponse } from '@/types/api'
const lineItemSchema = z.object({ const lineItemSchema = z.object({
itemId: z.string().min(1, 'Item is required'), itemId: z.string().min(1, 'Item is required'),
@@ -37,6 +38,7 @@ const lineItemSchema = z.object({
const schema = z.object({ const schema = z.object({
storeId: z.string().min(1, 'Store is required'), storeId: z.string().min(1, 'Store is required'),
raisedByUserId: z.string().min(1, 'Raised by is required'),
items: z.array(lineItemSchema).min(1, 'At least one item is required'), items: z.array(lineItemSchema).min(1, 'At least one item is required'),
}) })
@@ -52,12 +54,13 @@ export default function NewShopPOPage() {
const router = useRouter() const router = useRouter()
const [storesList, setStoresList] = useState<StoreResponse[]>([]) const [storesList, setStoresList] = useState<StoreResponse[]>([])
const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([]) const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([])
const [usersList, setUsersList] = useState<UserResponse[]>([])
const [priceMinByIndex, setPriceMinByIndex] = useState<Record<number, number | null>>({}) const [priceMinByIndex, setPriceMinByIndex] = useState<Record<number, number | null>>({})
const [priceRetailByIndex, setPriceRetailByIndex] = useState<Record<number, number | null>>({}) const [priceRetailByIndex, setPriceRetailByIndex] = useState<Record<number, number | null>>({})
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { storeId: '', items: [createDefaultItem()] }, defaultValues: { storeId: '', raisedByUserId: '', items: [createDefaultItem()] },
}) })
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' }) const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' })
@@ -107,10 +110,12 @@ export default function NewShopPOPage() {
Promise.all([ Promise.all([
storesApi.list({ pageSize: 200 }), storesApi.list({ pageSize: 200 }),
itemsApi.list({ pageSize: 200 }), itemsApi.list({ pageSize: 200 }),
usersApi.list({ pageSize: 200 }),
]) ])
.then(([stores, items]) => { .then(([stores, items, users]) => {
setStoresList(getPaginatedItems(stores)) setStoresList(getPaginatedItems(stores))
setItemsList(getPaginatedItems(items)) setItemsList(getPaginatedItems(items))
setUsersList(getPaginatedItems(users))
}) })
.catch((err) => { .catch((err) => {
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
@@ -121,6 +126,7 @@ export default function NewShopPOPage() {
try { try {
await shopPosApi.create({ await shopPosApi.create({
storeId: values.storeId, storeId: values.storeId,
raisedByUserId: values.raisedByUserId,
items: values.items, items: values.items,
}) })
toast.add({ title: 'Shop PO created', type: 'success' }) toast.add({ title: 'Shop PO created', type: 'success' })
@@ -147,6 +153,7 @@ export default function NewShopPOPage() {
<div className="rounded-2xl border p-6"> <div className="rounded-2xl border p-6">
<h3 className="mb-4 text-sm font-medium text-foreground">Order Details</h3> <h3 className="mb-4 text-sm font-medium text-foreground">Order Details</h3>
<FieldGroup> <FieldGroup>
<div className="grid gap-6 sm:grid-cols-2">
<FormField <FormField
control={form.control} control={form.control}
name="storeId" name="storeId"
@@ -168,6 +175,30 @@ export default function NewShopPOPage() {
</Select> </Select>
)} )}
/> />
<FormField
control={form.control}
name="raisedByUserId"
label="Raised By"
description="Temporary — required until auth resolves the logged-in user automatically."
required
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Select user..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="" disabled>Select user...</SelectItem>
{usersList.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.fullName}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>
</FieldGroup> </FieldGroup>
</div> </div>
+3 -2
View File
@@ -54,6 +54,7 @@ export const rides = {
export interface LoadingSheetCreateBody { export interface LoadingSheetCreateBody {
fK_RideId: string fK_RideId: string
populatedByUserId?: string | null
} }
export interface LoadingSheetItemInput { export interface LoadingSheetItemInput {
@@ -83,8 +84,8 @@ export const loadingSheets = {
updateItems: (id: string, items: LoadingSheetItemInput[]) => updateItems: (id: string, items: LoadingSheetItemInput[]) =>
api.put<LoadingSheetResponse>(`/loading-sheets/${id}/items`, { items }).then(r => r.data), api.put<LoadingSheetResponse>(`/loading-sheets/${id}/items`, { items }).then(r => r.data),
confirm: (id: string) => confirm: (id: string, confirmedByUserId?: string | null) =>
api.post<LoadingSheetResponse>(`/loading-sheets/${id}/confirm`).then(r => r.data), api.post<LoadingSheetResponse>(`/loading-sheets/${id}/confirm`, { confirmedByUserId }).then(r => r.data),
} }
// ─── Delivery Tasks ─────────────────────────────────────────────────────────── // ─── Delivery Tasks ───────────────────────────────────────────────────────────
+5 -4
View File
@@ -23,6 +23,7 @@ export interface ShopPOItemBody {
export interface CreateShopPOBody { export interface CreateShopPOBody {
storeId: string storeId: string
items: ShopPOItemBody[] items: ShopPOItemBody[]
raisedByUserId?: string | null
} }
export interface UpdateShopPOItemsBody { export interface UpdateShopPOItemsBody {
@@ -51,11 +52,11 @@ export const shopPos = {
updateItems: (id: string, body: UpdateShopPOItemsBody) => updateItems: (id: string, body: UpdateShopPOItemsBody) =>
api.put<ShopPOResponse>(`/shop-pos/${id}/items`, body).then(r => r.data), api.put<ShopPOResponse>(`/shop-pos/${id}/items`, body).then(r => r.data),
confirm: (id: string) => confirm: (id: string, confirmedByUserId?: string | null) =>
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm`).then(r => r.data), api.post<ShopPOResponse>(`/shop-pos/${id}/confirm`, { confirmedByUserId }).then(r => r.data),
confirmAndCreateInvoice: (id: string) => confirmAndCreateInvoice: (id: string, confirmedByUserId?: string | null) =>
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm-and-create-invoice`).then(r => r.data), api.post<ShopPOResponse>(`/shop-pos/${id}/confirm-and-create-invoice`, { confirmedByUserId }).then(r => r.data),
cancel: (id: string) => cancel: (id: string) =>
api.post<ShopPOResponse>(`/shop-pos/${id}/cancel`).then(r => r.data), api.post<ShopPOResponse>(`/shop-pos/${id}/cancel`).then(r => r.data),