Add manual invoice creation mode with new API & UI flow
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:
@@ -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'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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user