Add user selection to Shop PO flow
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user