feat: Implement Free Issue functionality in Shop PO and Invoice management

This commit is contained in:
2026-10-22 16:37:03 +05:30
parent 58247a1a0b
commit b1bda1448e
7 changed files with 509 additions and 21 deletions
@@ -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">