Merge dev into feat-notification
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { ReceiptDollarIcon } from '@hugeicons/core-free-icons'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { FieldDescription } from '@/components/ui/field'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import {
|
||||
invoiceCreationMode as invoiceCreationModeApi,
|
||||
DEFAULT_INVOICE_CREATION_MODE,
|
||||
getApiError,
|
||||
} from '@/lib/api/index'
|
||||
import type { InvoiceCreationMode } from '@/lib/api/sys-config'
|
||||
|
||||
const MODE_LABELS: Record<InvoiceCreationMode, string> = {
|
||||
ModeA: 'Auto on confirm',
|
||||
ModeB: 'Separate step',
|
||||
}
|
||||
|
||||
const MODE_HINTS: Record<InvoiceCreationMode, string> = {
|
||||
ModeA: 'Confirming a Shop PO immediately creates a draft invoice.',
|
||||
ModeB: 'Confirmation and invoice creation are two distinct actions.',
|
||||
}
|
||||
|
||||
export function InvoiceCreationModeCard() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [mode, setMode] = useState<InvoiceCreationMode>(DEFAULT_INVOICE_CREATION_MODE)
|
||||
const [pending, setPending] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
invoiceCreationModeApi.get()
|
||||
.then(setMode)
|
||||
.catch(err => toast.add({ title: 'Failed to load invoice creation mode', description: getApiError(err), type: 'error' }))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
async function save(next: InvoiceCreationMode) {
|
||||
if (next === mode) return
|
||||
const previous = mode
|
||||
setMode(next)
|
||||
setPending(true)
|
||||
try {
|
||||
await invoiceCreationModeApi.set(next)
|
||||
} catch (err) {
|
||||
setMode(previous)
|
||||
toast.add({ title: 'Failed to update invoice creation mode', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading...</p>
|
||||
|
||||
return (
|
||||
<Card className="h-full gap-3 rounded-3xl border-border/60 py-4">
|
||||
<CardHeader className="border-b border-border/60 pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<HugeiconsIcon icon={ReceiptDollarIcon} size={16} />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">Invoice Creation</CardTitle>
|
||||
<CardDescription className="text-xs">When invoices are created for a Shop PO</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2.5">
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
value={[mode]}
|
||||
onValueChange={(v: string[]) => v[0] && save(v[0] as InvoiceCreationMode)}
|
||||
disabled={pending}
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="ModeA" className="flex-1">{MODE_LABELS.ModeA}</ToggleGroupItem>
|
||||
<ToggleGroupItem value="ModeB" className="flex-1">{MODE_LABELS.ModeB}</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldDescription className="text-xs leading-relaxed">{MODE_HINTS[mode]}</FieldDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ProductCapabilitiesCard } from './_components/product-capabilities-card'
|
||||
import { GrnTaxCard } from './_components/grn-tax-card'
|
||||
import { InvoiceCreationModeCard } from './_components/invoice-creation-mode-card'
|
||||
|
||||
export default function ConfigurationPage() {
|
||||
return (
|
||||
@@ -11,7 +12,6 @@ export default function ConfigurationPage() {
|
||||
<p className="text-sm text-muted-foreground mt-0.5">System-wide settings, grouped by area</p>
|
||||
</div>
|
||||
|
||||
{/* 12-column grid: each card takes 6 columns so the two sit side by side; they stack on narrow screens. */}
|
||||
<div className="grid grid-cols-12 items-stretch gap-6">
|
||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||
<ProductCapabilitiesCard />
|
||||
@@ -19,6 +19,9 @@ export default function ConfigurationPage() {
|
||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||
<GrnTaxCard />
|
||||
</div>
|
||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||
<InvoiceCreationModeCard />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,9 +24,12 @@ import {
|
||||
items as itemsApi,
|
||||
priceLists as priceListsApi,
|
||||
users as usersApi,
|
||||
invoiceCreationMode as invoiceCreationModeApi,
|
||||
DEFAULT_INVOICE_CREATION_MODE,
|
||||
getApiError,
|
||||
getPaginatedItems,
|
||||
} from '@/lib/api/index'
|
||||
import type { InvoiceCreationMode } from '@/lib/api/sys-config'
|
||||
import type { ItemSummaryResponse, ShopPOResponse, UserResponse } from '@/types/api'
|
||||
|
||||
const lineItemSchema = z.object({
|
||||
@@ -56,6 +59,11 @@ export default function EditShopPOPage() {
|
||||
const [usersList, setUsersList] = useState<UserResponse[]>([])
|
||||
const [confirmedByUserId, setConfirmedByUserId] = useState('')
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [mode, setMode] = useState<InvoiceCreationMode>(DEFAULT_INVOICE_CREATION_MODE)
|
||||
|
||||
useEffect(() => {
|
||||
invoiceCreationModeApi.get().then(setMode).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -98,9 +106,14 @@ export default function EditShopPOPage() {
|
||||
if (!po || !confirmedByUserId) return
|
||||
setConfirming(true)
|
||||
try {
|
||||
const updated = await shopPosApi.confirm(po.id, confirmedByUserId)
|
||||
const updated = mode === 'ModeA'
|
||||
? await shopPosApi.confirmAndCreateInvoice(po.id, confirmedByUserId)
|
||||
: await shopPosApi.confirm(po.id, confirmedByUserId)
|
||||
setPo(updated)
|
||||
toast.add({ title: 'Shop PO confirmed', type: 'success' })
|
||||
toast.add({
|
||||
title: mode === 'ModeA' ? 'Shop PO confirmed and invoice created' : 'Shop PO confirmed',
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
|
||||
@@ -21,7 +21,15 @@ import {
|
||||
} 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 {
|
||||
shopPos as shopPosApi,
|
||||
users as usersApi,
|
||||
invoiceCreationMode as invoiceCreationModeApi,
|
||||
DEFAULT_INVOICE_CREATION_MODE,
|
||||
getApiError,
|
||||
getPaginatedItems,
|
||||
} from '@/lib/api/index'
|
||||
import type { InvoiceCreationMode } from '@/lib/api/sys-config'
|
||||
import type { ShopPOResponse, UserResponse } from '@/types/api'
|
||||
|
||||
const schema = z.object({
|
||||
@@ -39,6 +47,7 @@ interface ConfirmShopPODialogProps {
|
||||
|
||||
export function ConfirmShopPODialog({ open, onClose, onSuccess, po }: ConfirmShopPODialogProps) {
|
||||
const [usersList, setUsersList] = useState<UserResponse[]>([])
|
||||
const [mode, setMode] = useState<InvoiceCreationMode>(DEFAULT_INVOICE_CREATION_MODE)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -51,14 +60,20 @@ export function ConfirmShopPODialog({ open, onClose, onSuccess, po }: ConfirmSho
|
||||
usersApi.list()
|
||||
.then((res) => setUsersList(res))
|
||||
.catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' }))
|
||||
invoiceCreationModeApi.get().then(setMode).catch(() => {})
|
||||
}, [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' })
|
||||
if (mode === 'ModeA') {
|
||||
await shopPosApi.confirmAndCreateInvoice(po!.id, values.confirmedByUserId)
|
||||
toast.add({ title: 'Shop PO confirmed and invoice created', type: 'success' })
|
||||
} else {
|
||||
await shopPosApi.confirm(po!.id, values.confirmedByUserId)
|
||||
toast.add({ title: 'Shop PO confirmed', type: 'success' })
|
||||
}
|
||||
onSuccess()
|
||||
onClose()
|
||||
} catch (err) {
|
||||
@@ -71,7 +86,10 @@ export function ConfirmShopPODialog({ open, onClose, onSuccess, po }: ConfirmSho
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Shop PO</DialogTitle>
|
||||
<DialogDescription>{po.storeName} — Rs. {po.totalAmount.toLocaleString()}</DialogDescription>
|
||||
<DialogDescription>
|
||||
{po.storeName} — Rs. {po.totalAmount.toLocaleString()}
|
||||
{mode === 'ModeA' && ' · a draft invoice will be created'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { FieldGroup } from '@/components/ui/field'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { payments as paymentsApi, bankAccounts as bankAccountsApi, getApiError } from '@/lib/api/index'
|
||||
import type { InvoiceResponse } from '@/types/api'
|
||||
import { payments as paymentsApi, bankAccounts as bankAccountsApi, stores as storesApi, getApiError } from '@/lib/api/index'
|
||||
import type { InvoiceResponse, OutstandingBalanceResponse } from '@/types/api'
|
||||
import type { BankAccountResponse } from '@/types/payments'
|
||||
|
||||
const schema = z
|
||||
@@ -67,6 +67,7 @@ const truncId = (id: string) => id.substring(0, 8)
|
||||
|
||||
export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoiceId }: PaymentSheetProps) {
|
||||
const [accounts, setAccounts] = useState<BankAccountResponse[]>([])
|
||||
const [credit, setCredit] = useState<OutstandingBalanceResponse | null>(null)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -84,6 +85,16 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoic
|
||||
})
|
||||
|
||||
const method = form.watch('method')
|
||||
const invoiceId = form.watch('invoiceId')
|
||||
const amount = form.watch('amount')
|
||||
const selectedInvoice = invoices.find((inv) => inv.id === invoiceId)
|
||||
const selectedStoreId = selectedInvoice?.storeId
|
||||
const remaining = selectedInvoice ? selectedInvoice.totalAmount - selectedInvoice.paidAmount : 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !selectedInvoice) return
|
||||
form.setValue('amount', Number((selectedInvoice.totalAmount - selectedInvoice.paidAmount).toFixed(2)))
|
||||
}, [open, selectedInvoice?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
@@ -101,10 +112,39 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoic
|
||||
bankAccountsApi.list()
|
||||
.then(list => setAccounts(list.filter(a => a.isActive !== false)))
|
||||
.catch(() => {})
|
||||
setCredit(null)
|
||||
}, [open, defaultInvoiceId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || method !== 'Credit' || !selectedStoreId) {
|
||||
setCredit(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
storesApi.outstandingBalance(selectedStoreId)
|
||||
.then((data) => { if (!cancelled) setCredit(data) })
|
||||
.catch(() => { if (!cancelled) setCredit(null) })
|
||||
return () => { cancelled = true }
|
||||
}, [open, method, selectedStoreId])
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
const { invoiceId, method, bankAccountId, chequeNumber, chequeDate, drawerBankName, accountHolderName, ...rest } = values
|
||||
if (selectedInvoice && values.amount > remaining) {
|
||||
toast.add({
|
||||
title: 'Amount exceeds outstanding balance',
|
||||
description: `Outstanding: Rs. ${remaining.toLocaleString()}`,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (method === 'Credit' && credit && values.amount > credit.availableCredit) {
|
||||
toast.add({
|
||||
title: 'Credit limit exceeded',
|
||||
description: `Available credit: Rs. ${credit.availableCredit.toLocaleString()}`,
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await paymentsApi.create(invoiceId, {
|
||||
...rest,
|
||||
@@ -161,6 +201,7 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoic
|
||||
name="amount"
|
||||
label="Amount"
|
||||
required
|
||||
description={selectedInvoice ? `Outstanding: ${fmtAmount(remaining)}` : undefined}
|
||||
render={({ field, invalid }) => (
|
||||
<Input
|
||||
id="amount"
|
||||
@@ -172,7 +213,7 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoic
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
onBlur={field.onBlur}
|
||||
placeholder="0.00"
|
||||
aria-invalid={invalid}
|
||||
aria-invalid={invalid || (selectedInvoice ? amount > remaining : false)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -196,6 +237,36 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices, defaultInvoic
|
||||
)}
|
||||
/>
|
||||
|
||||
{method === 'Credit' && selectedStoreId && (
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-sm">
|
||||
{credit ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Credit limit</span>
|
||||
<span>{fmtAmount(credit.creditLimit)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Currently used</span>
|
||||
<span>{fmtAmount(credit.outstandingBalance)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-medium">
|
||||
<span>Available</span>
|
||||
<span className={amount > credit.availableCredit ? 'text-destructive' : ''}>
|
||||
{fmtAmount(credit.availableCredit)}
|
||||
</span>
|
||||
</div>
|
||||
{amount > credit.availableCredit && (
|
||||
<p className="text-destructive text-xs mt-1">
|
||||
Amount exceeds available credit.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Loading credit info…</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{method === 'Cash' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -11,7 +11,7 @@ export { stock } from './stock'
|
||||
export { productReports, stockReports, dashboardReports } from './reports'
|
||||
export { shopPos, invoices, payments } from './sales'
|
||||
export { saleReturns, claims, supplierClaims } from './returns-claims'
|
||||
export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS } from './sys-config'
|
||||
export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS, invoiceCreationMode, INVOICE_CREATION_MODE_KEY, DEFAULT_INVOICE_CREATION_MODE } from './sys-config'
|
||||
export { bankAccounts } from './bank-accounts'
|
||||
export { chequeBooks, chequePages } from './cheques'
|
||||
export { receivedCheques } from './received-cheques'
|
||||
|
||||
@@ -48,6 +48,9 @@ export const shopPos = {
|
||||
get: (id: string) =>
|
||||
api.get<ShopPOResponse>(`/shop-pos/${id}`).then(r => r.data),
|
||||
|
||||
confirmedWithoutInvoice: (params?: { agentId?: string; routeId?: string; shopId?: string }) =>
|
||||
api.get<ShopPOResponse[]>('/shop-pos/confirmed-without-invoice', { params }).then(r => r.data),
|
||||
|
||||
create: (body: CreateShopPOBody) =>
|
||||
api.post<ShopPOResponse>('/shop-pos', body).then(r => r.data),
|
||||
|
||||
@@ -84,6 +87,9 @@ export const invoices = {
|
||||
get: (id: string) =>
|
||||
api.get<InvoiceResponse>(`/invoices/${id}`).then(r => r.data),
|
||||
|
||||
create: (shopPOId: string, createdByUserId?: string | null) =>
|
||||
api.post<InvoiceResponse>('/invoices', { shopPOId, createdByUserId }).then(r => r.data),
|
||||
|
||||
print: (id: string, printedByUserId?: string | null) =>
|
||||
api.post<InvoiceResponse>(`/invoices/${id}/print`, { printedByUserId }).then(r => r.data),
|
||||
|
||||
|
||||
@@ -72,3 +72,19 @@ export const grnTaxSettings = {
|
||||
.getAll()
|
||||
.then(all => parseGrnTaxSettings(Object.fromEntries(all.map(c => [c.key, c.value])))),
|
||||
}
|
||||
|
||||
// ─── Invoice creation mode (must match Distribution.Features.SysConfig.SysConfigService) ─────
|
||||
|
||||
export const INVOICE_CREATION_MODE_KEY = 'InvoiceCreationMode'
|
||||
|
||||
export type InvoiceCreationMode = 'ModeA' | 'ModeB'
|
||||
|
||||
export const DEFAULT_INVOICE_CREATION_MODE: InvoiceCreationMode = 'ModeB'
|
||||
|
||||
export const invoiceCreationMode = {
|
||||
get: () =>
|
||||
api.get<{ mode: InvoiceCreationMode }>('/sys-config/invoice-creation-mode').then(r => r.data.mode),
|
||||
|
||||
set: (mode: InvoiceCreationMode) =>
|
||||
api.put('/sys-config/invoice-creation-mode', { mode }).then(r => r.data),
|
||||
}
|
||||
|
||||
@@ -1004,6 +1004,7 @@ export interface ShopPOResponse {
|
||||
export interface InvoiceResponse {
|
||||
id: string
|
||||
shopPOId: string
|
||||
storeId: string
|
||||
storeName: string
|
||||
status: InvoiceStatus
|
||||
paymentStatus: PaymentStatus
|
||||
@@ -1013,6 +1014,7 @@ export interface InvoiceResponse {
|
||||
finalizedAt: string | null
|
||||
finalizedBy: string | null
|
||||
totalAmount: number
|
||||
paidAmount: number
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user