feat: Implement Free Issue functionality in Shop PO and Invoice management
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
} from '@/lib/api/index'
|
||||
import type { InvoiceCreationMode } from '@/lib/api/sys-config'
|
||||
import type { ItemSummaryResponse, ShopPOResponse, UserResponse } from '@/types/api'
|
||||
import { FreeIssuePanel, type FreeLine } from '../../_components/free-issue-panel'
|
||||
|
||||
const lineItemSchema = z.object({
|
||||
itemId: z.string().min(1, 'Item is required'),
|
||||
@@ -60,6 +61,8 @@ export default function EditShopPOPage() {
|
||||
const [confirmedByUserId, setConfirmedByUserId] = useState('')
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [mode, setMode] = useState<InvoiceCreationMode>(DEFAULT_INVOICE_CREATION_MODE)
|
||||
const [freeLines, setFreeLines] = useState<FreeLine[]>([])
|
||||
const [discountIds, setDiscountIds] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
invoiceCreationModeApi.get().then(setMode).catch(() => {})
|
||||
@@ -91,9 +94,21 @@ export default function EditShopPOPage() {
|
||||
setUsersList(users)
|
||||
form.reset({
|
||||
items: poRes.items
|
||||
.filter((i) => i.isActive)
|
||||
.filter((i) => i.isActive && !i.isFree)
|
||||
.map((i) => ({ itemId: i.itemId, quantity: i.quantity, unitPrice: i.unitPrice })),
|
||||
})
|
||||
setDiscountIds(poRes.discounts.map((d) => d.schemeId))
|
||||
setFreeLines(
|
||||
poRes.items
|
||||
.filter((i) => i.isActive && i.isFree && i.schemeId)
|
||||
.map((i) => ({
|
||||
itemId: i.itemId,
|
||||
itemName: i.itemName,
|
||||
quantity: i.quantity,
|
||||
schemeId: i.schemeId!,
|
||||
schemeName: i.schemeName ?? 'Scheme',
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({ title: 'Failed to load Shop PO', description: getApiError(err), type: 'error' })
|
||||
@@ -138,7 +153,13 @@ export default function EditShopPOPage() {
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
try {
|
||||
await shopPosApi.updateItems(id, { items: values.items })
|
||||
await shopPosApi.updateItems(id, {
|
||||
discountSchemeIds: discountIds,
|
||||
items: [
|
||||
...values.items,
|
||||
...freeLines.map((l) => ({ itemId: l.itemId, quantity: l.quantity, isFree: true, schemeId: l.schemeId })),
|
||||
],
|
||||
})
|
||||
toast.add({ title: 'Shop PO updated', type: 'success' })
|
||||
router.push('/sales')
|
||||
} catch (err) {
|
||||
@@ -296,6 +317,20 @@ export default function EditShopPOPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FreeIssuePanel
|
||||
storeId={po.storeId}
|
||||
paidLines={watchedItems.map((l) => ({
|
||||
itemId: l.itemId,
|
||||
quantity: Number(l.quantity) || 0,
|
||||
unitPrice: Number(l.unitPrice) || 0,
|
||||
}))}
|
||||
freeLines={freeLines}
|
||||
onChange={setFreeLines}
|
||||
discountIds={discountIds}
|
||||
onDiscountChange={setDiscountIds}
|
||||
disabled={locked}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="w-full rounded-2xl border p-6 sm:w-96">
|
||||
<h3 className="text-sm font-medium text-foreground">Order Summary</h3>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function ShopPOViewPage() {
|
||||
|
||||
if (!po) return null
|
||||
|
||||
const activeItems = po.items.filter((i) => i.isActive)
|
||||
const activeItems = po.items.filter((i) => i.isActive).sort((a, b) => Number(a.isFree) - Number(b.isFree))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -155,6 +155,7 @@ export default function ShopPOViewPage() {
|
||||
<tr>
|
||||
<th className="min-w-[220px] px-3 py-2 text-left font-medium">Item</th>
|
||||
<th className="w-28 px-3 py-2 text-right font-medium">Quantity</th>
|
||||
<th className="w-28 px-3 py-2 text-right font-medium">Free Issue</th>
|
||||
<th className="w-28 px-3 py-2 text-right font-medium">Fulfilled</th>
|
||||
<th className="w-32 px-3 py-2 text-right font-medium">Unit Price</th>
|
||||
<th className="w-32 px-3 py-2 text-right font-medium">Line Total</th>
|
||||
@@ -163,15 +164,21 @@ export default function ShopPOViewPage() {
|
||||
<tbody>
|
||||
{activeItems.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-3 py-4 text-center text-muted-foreground">
|
||||
<td colSpan={6} className="px-3 py-4 text-center text-muted-foreground">
|
||||
No items on this PO.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
activeItems.map((item) => (
|
||||
<tr key={item.id} className="border-t">
|
||||
<td className="px-3 py-2">{item.itemName}</td>
|
||||
<td className="px-3 py-2 text-right">{item.quantity}</td>
|
||||
<td className="px-3 py-2">
|
||||
{item.itemName}
|
||||
{item.isFree && item.schemeName && (
|
||||
<Badge variant="outline" className="ml-2">{item.schemeName}</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">{item.isFree ? '-' : item.quantity}</td>
|
||||
<td className="px-3 py-2 text-right">{item.isFree ? item.quantity : '-'}</td>
|
||||
<td className="px-3 py-2 text-right">{item.quantityFulfilled}</td>
|
||||
<td className="px-3 py-2 text-right">{fmtAmount(item.unitPrice)}</td>
|
||||
<td className="px-3 py-2 text-right font-medium">{fmtAmount(item.quantity * item.unitPrice)}</td>
|
||||
@@ -181,8 +188,14 @@ export default function ShopPOViewPage() {
|
||||
</tbody>
|
||||
{activeItems.length > 0 && (
|
||||
<tfoot>
|
||||
{po.discounts.map((d) => (
|
||||
<tr key={d.schemeId} className="border-t bg-muted/30">
|
||||
<td colSpan={5} className="px-3 py-2 text-right text-sm text-muted-foreground">Scheme discount ({d.schemeName})</td>
|
||||
<td className="px-3 py-2 text-right text-sm font-medium text-foreground">-{fmtAmount(d.amount)}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="border-t bg-muted/30">
|
||||
<td colSpan={4} className="px-3 py-3 text-right text-sm font-medium text-foreground">
|
||||
<td colSpan={5} className="px-3 py-3 text-right text-sm font-medium text-foreground">
|
||||
Total
|
||||
</td>
|
||||
<td className="px-3 py-3 text-right text-base font-semibold text-foreground">
|
||||
|
||||
Reference in New Issue
Block a user