Add manual invoice creation mode with new API & UI flow
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 13m13s
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 14m28s

Introduced "ModeB" for manual invoice creation in system config. Added backend endpoints and service logic for creating invoices from confirmed Shop POs, with validation and error handling. Updated DTOs to support optional CreatedByUserId. On the frontend, added configuration card to toggle modes, a dialog for manual invoice creation, and UI updates to support the new workflow. Enables admins to choose between automatic and manual invoice creation.
This commit is contained in:
Kalana B. Thilakarathna
2026-10-20 13:38:43 +05:30
parent 5707bd4c31
commit 2de9e5e4ad
12 changed files with 344 additions and 5 deletions
@@ -0,0 +1,142 @@
'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 { invoices as invoicesApi, shopPos as shopPosApi, users as usersApi, getApiError } from '@/lib/api/index'
import type { ShopPOResponse, UserResponse } from '@/types/api'
const schema = z.object({
shopPOId: z.string().min(1, 'Shop PO is required'),
userId: z.string().min(1, 'User is required'),
})
type FormValues = z.infer<typeof schema>
interface CreateInvoiceDialogProps {
open: boolean
onClose: () => void
onSuccess: () => void
}
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
const truncId = (id: string) => id.substring(0, 8)
export function CreateInvoiceDialog({ open, onClose, onSuccess }: CreateInvoiceDialogProps) {
const [pos, setPos] = useState<ShopPOResponse[]>([])
const [usersList, setUsersList] = useState<UserResponse[]>([])
const [loading, setLoading] = useState(false)
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { shopPOId: '', userId: '' },
})
useEffect(() => {
if (!open) return
form.reset({ shopPOId: '', userId: '' })
setLoading(true)
Promise.all([
shopPosApi.confirmedWithoutInvoice(),
usersApi.list(),
])
.then(([posRes, usersRes]) => {
setPos(posRes)
setUsersList(usersRes)
})
.catch((err) => toast.add({ title: 'Failed to load data', description: getApiError(err), type: 'error' }))
.finally(() => setLoading(false))
}, [open])
async function onSubmit(values: FormValues) {
try {
await invoicesApi.create(values.shopPOId, values.userId)
toast.add({ title: 'Invoice created', type: 'success' })
onSuccess()
onClose()
} catch (err) {
toast.add({ title: 'Failed to create invoice', description: getApiError(err), type: 'error' })
}
}
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Create Invoice</DialogTitle>
<DialogDescription>Generate an invoice from a confirmed Shop PO that doesn&apos;t have one yet.</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<FieldGroup>
<FormField
control={form.control}
name="shopPOId"
label="Shop PO"
required
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange} disabled={loading || pos.length === 0}>
<SelectTrigger>
<SelectValue placeholder={loading ? 'Loading…' : pos.length === 0 ? 'No eligible Shop POs' : 'Select Shop PO...'} />
</SelectTrigger>
<SelectContent>
{pos.map((po) => (
<SelectItem key={po.id} value={po.id}>
{po.storeName} — {fmtAmount(po.totalAmount)} [{truncId(po.id)}]
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<FormField
control={form.control}
name="userId"
label="Created 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>
{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 || pos.length === 0}>
{form.formState.isSubmitting ? 'Saving…' : 'Create Invoice'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
@@ -5,12 +5,15 @@ import { useState, useEffect, useCallback, useMemo } from 'react'
import { DataTable } from '@/components/shared/data-table'
import type { TableConfig } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { invoices as invoicesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import { invoices as invoicesApi, invoiceCreationMode as invoiceCreationModeApi, DEFAULT_INVOICE_CREATION_MODE, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { InvoiceCreationMode } from '@/lib/api/sys-config'
import type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api'
import { OrderViewSheet } from './order-view-sheet'
import { InvoiceActionDialog } from './invoice-action-dialog'
import { CreateInvoiceDialog } from './create-invoice-dialog'
import { PaymentSheet } from '../../payments/_components/payment-sheet'
const fmtDate = (v: string) =>
@@ -44,6 +47,12 @@ export function OrdersClient() {
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
const [paymentOpen, setPaymentOpen] = useState(false)
const [paymentInvoice, setPaymentInvoice] = useState<InvoiceResponse | null>(null)
const [createOpen, setCreateOpen] = useState(false)
const [mode, setMode] = useState<InvoiceCreationMode>(DEFAULT_INVOICE_CREATION_MODE)
useEffect(() => {
invoiceCreationModeApi.get().then(setMode).catch(() => {})
}, [])
const fetchData = useCallback(async (p: number, ps: number) => {
setLoading(true)
@@ -178,6 +187,9 @@ export function OrdersClient() {
Billing documents generated from confirmed Shop POs
</p>
</div>
{mode === 'ModeB' && (
<Button onClick={() => setCreateOpen(true)}>Create Invoice</Button>
)}
</div>
<DataTable
@@ -212,6 +224,12 @@ export function OrdersClient() {
invoices={paymentInvoice ? [paymentInvoice] : []}
defaultInvoiceId={paymentInvoice?.id}
/>
<CreateInvoiceDialog
open={createOpen}
onClose={() => setCreateOpen(false)}
onSuccess={() => fetchData(page, pageSize)}
/>
</>
)
}