Refactor/main flow #39
@@ -10,6 +10,8 @@ import { toast } from '@/components/ui/toast'
|
||||
import { shopPos as shopPosApi, getApiError } from '@/lib/api/index'
|
||||
import type { ShopPOResponse, ShopPOStatus } from '@/types/api'
|
||||
|
||||
import { ConfirmShopPODialog } from '../_components/confirm-shop-po-dialog'
|
||||
|
||||
const PO_STATUS_VARIANT: Record<ShopPOStatus, 'default' | 'secondary' | 'destructive'> = {
|
||||
Pending: 'secondary',
|
||||
PartiallyFulfilled: 'secondary',
|
||||
@@ -41,6 +43,7 @@ export default function ShopPOViewPage() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const [po, setPo] = useState<ShopPOResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -62,17 +65,6 @@ export default function ShopPOViewPage() {
|
||||
}
|
||||
}, [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() {
|
||||
if (!po) return
|
||||
try {
|
||||
@@ -123,7 +115,7 @@ export default function ShopPOViewPage() {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!po.confirmedAt && po.status !== 'Cancelled' && (
|
||||
<Button type="button" onClick={handleConfirm}>
|
||||
<Button type="button" onClick={() => setConfirmOpen(true)}>
|
||||
Confirm PO
|
||||
</Button>
|
||||
)}
|
||||
@@ -211,6 +203,15 @@ export default function ShopPOViewPage() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmShopPODialog
|
||||
open={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onSuccess={() => {
|
||||
shopPosApi.get(po.id).then(setPo)
|
||||
}}
|
||||
po={po}
|
||||
/>
|
||||
</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'
|
||||
import type { ShopPOResponse, ShopPOStatus, UserResponse, RouteResponse, StoreResponse } from '@/types/api'
|
||||
|
||||
import { ConfirmShopPODialog } from './confirm-shop-po-dialog'
|
||||
|
||||
const ALL = '__all__'
|
||||
type ConfirmedFilter = 'all' | 'confirmed' | 'unconfirmed'
|
||||
|
||||
@@ -60,6 +62,9 @@ export function SalesClient() {
|
||||
const [shopId, setShopId] = useState('')
|
||||
const [confirmedFilter, setConfirmedFilter] = useState<ConfirmedFilter>('all')
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [selectedPO, setSelectedPO] = useState<ShopPOResponse | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
usersApi.list({ pageSize: 200 }),
|
||||
@@ -111,14 +116,9 @@ export function SalesClient() {
|
||||
router.push(`/sales/${row.id}`)
|
||||
}
|
||||
|
||||
async function handleConfirm(row: ShopPOResponse) {
|
||||
try {
|
||||
await shopPosApi.confirm(row.id)
|
||||
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' })
|
||||
}
|
||||
function handleConfirm(row: ShopPOResponse) {
|
||||
setSelectedPO(row)
|
||||
setConfirmOpen(true)
|
||||
}
|
||||
|
||||
async function handleCancel(row: ShopPOResponse) {
|
||||
@@ -316,6 +316,13 @@ export function SalesClient() {
|
||||
setPageSize(p.pageSize)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmShopPODialog
|
||||
open={confirmOpen}
|
||||
onClose={() => setConfirmOpen(false)}
|
||||
onSuccess={() => fetchData(page, pageSize)}
|
||||
po={selectedPO}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,10 +24,11 @@ import {
|
||||
stores as storesApi,
|
||||
items as itemsApi,
|
||||
priceLists as priceListsApi,
|
||||
users as usersApi,
|
||||
getApiError,
|
||||
getPaginatedItems,
|
||||
} from '@/lib/api/index'
|
||||
import type { ItemSummaryResponse, StoreResponse } from '@/types/api'
|
||||
import type { ItemSummaryResponse, StoreResponse, UserResponse } from '@/types/api'
|
||||
|
||||
const lineItemSchema = z.object({
|
||||
itemId: z.string().min(1, 'Item is required'),
|
||||
@@ -37,6 +38,7 @@ const lineItemSchema = z.object({
|
||||
|
||||
const schema = z.object({
|
||||
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'),
|
||||
})
|
||||
|
||||
@@ -52,12 +54,13 @@ export default function NewShopPOPage() {
|
||||
const router = useRouter()
|
||||
const [storesList, setStoresList] = useState<StoreResponse[]>([])
|
||||
const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([])
|
||||
const [usersList, setUsersList] = useState<UserResponse[]>([])
|
||||
const [priceMinByIndex, setPriceMinByIndex] = useState<Record<number, number | null>>({})
|
||||
const [priceRetailByIndex, setPriceRetailByIndex] = useState<Record<number, number | null>>({})
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { storeId: '', items: [createDefaultItem()] },
|
||||
defaultValues: { storeId: '', raisedByUserId: '', items: [createDefaultItem()] },
|
||||
})
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'items' })
|
||||
@@ -107,10 +110,12 @@ export default function NewShopPOPage() {
|
||||
Promise.all([
|
||||
storesApi.list({ pageSize: 200 }),
|
||||
itemsApi.list({ pageSize: 200 }),
|
||||
usersApi.list({ pageSize: 200 }),
|
||||
])
|
||||
.then(([stores, items]) => {
|
||||
.then(([stores, items, users]) => {
|
||||
setStoresList(getPaginatedItems(stores))
|
||||
setItemsList(getPaginatedItems(items))
|
||||
setUsersList(getPaginatedItems(users))
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
|
||||
@@ -121,6 +126,7 @@ export default function NewShopPOPage() {
|
||||
try {
|
||||
await shopPosApi.create({
|
||||
storeId: values.storeId,
|
||||
raisedByUserId: values.raisedByUserId,
|
||||
items: values.items,
|
||||
})
|
||||
toast.add({ title: 'Shop PO created', type: 'success' })
|
||||
@@ -147,27 +153,52 @@ export default function NewShopPOPage() {
|
||||
<div className="rounded-2xl border p-6">
|
||||
<h3 className="mb-4 text-sm font-medium text-foreground">Order Details</h3>
|
||||
<FieldGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storeId"
|
||||
label="Store"
|
||||
required
|
||||
render={({ field }) => (
|
||||
<Select value={field.value as string} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select store..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="" disabled>Select store...</SelectItem>
|
||||
{storesList.map((store) => (
|
||||
<SelectItem key={store.id} value={store.id}>
|
||||
{store.storeName} — {store.ownerName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storeId"
|
||||
label="Store"
|
||||
required
|
||||
render={({ field }) => (
|
||||
<Select value={field.value as string} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select store..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="" disabled>Select store...</SelectItem>
|
||||
{storesList.map((store) => (
|
||||
<SelectItem key={store.id} value={store.id}>
|
||||
{store.storeName} — {store.ownerName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export const rides = {
|
||||
|
||||
export interface LoadingSheetCreateBody {
|
||||
fK_RideId: string
|
||||
populatedByUserId?: string | null
|
||||
}
|
||||
|
||||
export interface LoadingSheetItemInput {
|
||||
@@ -83,8 +84,8 @@ export const loadingSheets = {
|
||||
updateItems: (id: string, items: LoadingSheetItemInput[]) =>
|
||||
api.put<LoadingSheetResponse>(`/loading-sheets/${id}/items`, { items }).then(r => r.data),
|
||||
|
||||
confirm: (id: string) =>
|
||||
api.post<LoadingSheetResponse>(`/loading-sheets/${id}/confirm`).then(r => r.data),
|
||||
confirm: (id: string, confirmedByUserId?: string | null) =>
|
||||
api.post<LoadingSheetResponse>(`/loading-sheets/${id}/confirm`, { confirmedByUserId }).then(r => r.data),
|
||||
}
|
||||
|
||||
// ─── Delivery Tasks ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ShopPOItemBody {
|
||||
export interface CreateShopPOBody {
|
||||
storeId: string
|
||||
items: ShopPOItemBody[]
|
||||
raisedByUserId?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateShopPOItemsBody {
|
||||
@@ -51,11 +52,11 @@ export const shopPos = {
|
||||
updateItems: (id: string, body: UpdateShopPOItemsBody) =>
|
||||
api.put<ShopPOResponse>(`/shop-pos/${id}/items`, body).then(r => r.data),
|
||||
|
||||
confirm: (id: string) =>
|
||||
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm`).then(r => r.data),
|
||||
confirm: (id: string, confirmedByUserId?: string | null) =>
|
||||
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm`, { confirmedByUserId }).then(r => r.data),
|
||||
|
||||
confirmAndCreateInvoice: (id: string) =>
|
||||
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm-and-create-invoice`).then(r => r.data),
|
||||
confirmAndCreateInvoice: (id: string, confirmedByUserId?: string | null) =>
|
||||
api.post<ShopPOResponse>(`/shop-pos/${id}/confirm-and-create-invoice`, { confirmedByUserId }).then(r => r.data),
|
||||
|
||||
cancel: (id: string) =>
|
||||
api.post<ShopPOResponse>(`/shop-pos/${id}/cancel`).then(r => r.data),
|
||||
|
||||
Reference in New Issue
Block a user