Merge pull request 'Add manual invoice creation mode with new API & UI flow' (#78) from dev into production
Reviewed-on: #78
This commit was merged in pull request #78.
This commit is contained in:
@@ -36,6 +36,22 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
|
|||||||
return result is null ? NotFound() : Ok(result);
|
return result is null ? NotFound() : Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Create([FromBody] CreateInvoiceRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware
|
||||||
|
// (testbug.md #28) — fall back to the explicitly supplied user until that's fixed.
|
||||||
|
var userId = currentUser.UserId ?? request.CreatedByUserId;
|
||||||
|
if (userId is null) return BadRequest(new { error = "CreatedByUserId is required while auth is unavailable" });
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await service.CreateAsync(request, userId.Value, ct);
|
||||||
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/print")]
|
[HttpPost("{id:guid}/print")]
|
||||||
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Distribution.Features.Invoice;
|
namespace Distribution.Features.Invoice;
|
||||||
|
|
||||||
public record CreateInvoiceRequest(Guid ShopPOId);
|
public record CreateInvoiceRequest(Guid ShopPOId, Guid? CreatedByUserId = null);
|
||||||
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
|
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
|
||||||
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
||||||
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Distribution.Features.Stock;
|
using Distribution.Features.ShopPO;
|
||||||
|
using Distribution.Features.Stock;
|
||||||
using Distribution.Infra.Authorization;
|
using Distribution.Infra.Authorization;
|
||||||
using Distribution.Infra.UoW;
|
using Distribution.Infra.UoW;
|
||||||
using Distribution.Models;
|
using Distribution.Models;
|
||||||
@@ -7,6 +8,7 @@ namespace Distribution.Features.Invoice;
|
|||||||
|
|
||||||
public class InvoiceService(
|
public class InvoiceService(
|
||||||
IInvoiceRepository repo,
|
IInvoiceRepository repo,
|
||||||
|
IShopPORepository shopPoRepo,
|
||||||
IUnitOfWork uow,
|
IUnitOfWork uow,
|
||||||
ICurrentUserService currentUser)
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
@@ -42,6 +44,41 @@ public class InvoiceService(
|
|||||||
return new OutstandingPaymentsResponse(count, outstanding);
|
return new OutstandingPaymentsResponse(count, outstanding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<InvoiceResponse> CreateAsync(CreateInvoiceRequest request, Guid createdBy, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var po = await shopPoRepo.GetByIdAsync(request.ShopPOId)
|
||||||
|
?? throw new KeyNotFoundException("Shop PO not found");
|
||||||
|
|
||||||
|
if (po.ConfirmedAt is null)
|
||||||
|
throw new InvalidOperationException("Shop PO must be confirmed before an invoice can be created");
|
||||||
|
if (po.Status == ShopPOStatus.Cancelled)
|
||||||
|
throw new InvalidOperationException("Cannot create an invoice for a cancelled Shop PO");
|
||||||
|
if (po.InvoiceId is not null)
|
||||||
|
throw new InvalidOperationException("An invoice already exists for this Shop PO");
|
||||||
|
|
||||||
|
var invoice = new Models.Invoice
|
||||||
|
{
|
||||||
|
FK_ShopPOId = po.Id,
|
||||||
|
Status = InvoiceStatus.Draft,
|
||||||
|
PaymentStatus = PaymentStatus.Pending,
|
||||||
|
CreatedBy = createdBy,
|
||||||
|
TotalAmount = po.TotalAmount
|
||||||
|
};
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repo.AddAsync(invoice);
|
||||||
|
po.InvoiceId = invoice.Id;
|
||||||
|
shopPoRepo.Update(po);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
var saved = await repo.GetByIdAsync(invoice.Id);
|
||||||
|
return ToResponse(saved!);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
|
public async Task<InvoiceResponse> PrintAsync(Guid id, Guid printedBy, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var invoice = await repo.GetByIdAsync(id)
|
var invoice = await repo.GetByIdAsync(id)
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ public class ShopPOController(ShopPOService service, ICurrentUserService current
|
|||||||
return result is null ? NotFound() : Ok(result);
|
return result is null ? NotFound() : Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("confirmed-without-invoice")]
|
||||||
|
public async Task<IActionResult> GetConfirmedWithoutInvoice(
|
||||||
|
[FromQuery] Guid? agentId,
|
||||||
|
[FromQuery] Guid? routeId,
|
||||||
|
[FromQuery] Guid? shopId)
|
||||||
|
{
|
||||||
|
var result = await service.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Create(CreateShopPORequest request, CancellationToken ct)
|
public async Task<IActionResult> Create(CreateShopPORequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ public class ShopPOService(
|
|||||||
return ToResponse(po, users);
|
return ToResponse(po, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
|
||||||
|
{
|
||||||
|
var items = await repo.GetConfirmedWithoutInvoiceAsync(agentId, routeId, shopId);
|
||||||
|
var users = await repo.GetUserInfoAsync(items.SelectMany(UserIds));
|
||||||
|
return items.Select(p => ToResponse(p, users)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ShopPOResponse> CreateAsync(CreateShopPORequest request, Guid raisedById, CancellationToken ct = default)
|
public async Task<ShopPOResponse> CreateAsync(CreateShopPORequest request, Guid raisedById, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (request.Items.Count == 0)
|
if (request.Items.Count == 0)
|
||||||
|
|||||||
+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 { ProductCapabilitiesCard } from './_components/product-capabilities-card'
|
||||||
import { GrnTaxCard } from './_components/grn-tax-card'
|
import { GrnTaxCard } from './_components/grn-tax-card'
|
||||||
|
import { InvoiceCreationModeCard } from './_components/invoice-creation-mode-card'
|
||||||
|
|
||||||
export default function ConfigurationPage() {
|
export default function ConfigurationPage() {
|
||||||
return (
|
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>
|
<p className="text-sm text-muted-foreground mt-0.5">System-wide settings, grouped by area</p>
|
||||||
</div>
|
</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="grid grid-cols-12 items-stretch gap-6">
|
||||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||||
<ProductCapabilitiesCard />
|
<ProductCapabilitiesCard />
|
||||||
@@ -19,6 +19,9 @@ export default function ConfigurationPage() {
|
|||||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||||
<GrnTaxCard />
|
<GrnTaxCard />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||||
|
<InvoiceCreationModeCard />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 { DataTable } from '@/components/shared/data-table'
|
||||||
import type { TableConfig } from '@/components/shared/data-table'
|
import type { TableConfig } from '@/components/shared/data-table'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from '@/components/ui/toast'
|
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 type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api'
|
||||||
|
|
||||||
import { OrderViewSheet } from './order-view-sheet'
|
import { OrderViewSheet } from './order-view-sheet'
|
||||||
import { InvoiceActionDialog } from './invoice-action-dialog'
|
import { InvoiceActionDialog } from './invoice-action-dialog'
|
||||||
|
import { CreateInvoiceDialog } from './create-invoice-dialog'
|
||||||
import { PaymentSheet } from '../../payments/_components/payment-sheet'
|
import { PaymentSheet } from '../../payments/_components/payment-sheet'
|
||||||
|
|
||||||
const fmtDate = (v: string) =>
|
const fmtDate = (v: string) =>
|
||||||
@@ -44,6 +47,12 @@ export function OrdersClient() {
|
|||||||
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
|
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
|
||||||
const [paymentOpen, setPaymentOpen] = useState(false)
|
const [paymentOpen, setPaymentOpen] = useState(false)
|
||||||
const [paymentInvoice, setPaymentInvoice] = useState<InvoiceResponse | null>(null)
|
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) => {
|
const fetchData = useCallback(async (p: number, ps: number) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -178,6 +187,9 @@ export function OrdersClient() {
|
|||||||
Billing documents generated from confirmed Shop POs
|
Billing documents generated from confirmed Shop POs
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{mode === 'ModeB' && (
|
||||||
|
<Button onClick={() => setCreateOpen(true)}>Create Invoice</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -212,6 +224,12 @@ export function OrdersClient() {
|
|||||||
invoices={paymentInvoice ? [paymentInvoice] : []}
|
invoices={paymentInvoice ? [paymentInvoice] : []}
|
||||||
defaultInvoiceId={paymentInvoice?.id}
|
defaultInvoiceId={paymentInvoice?.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CreateInvoiceDialog
|
||||||
|
open={createOpen}
|
||||||
|
onClose={() => setCreateOpen(false)}
|
||||||
|
onSuccess={() => fetchData(page, pageSize)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export { stock } from './stock'
|
|||||||
export { productReports, stockReports, dashboardReports } from './reports'
|
export { productReports, stockReports, dashboardReports } from './reports'
|
||||||
export { shopPos, invoices, payments } from './sales'
|
export { shopPos, invoices, payments } from './sales'
|
||||||
export { saleReturns, claims, supplierClaims } from './returns-claims'
|
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 { bankAccounts } from './bank-accounts'
|
||||||
export { chequeBooks, chequePages } from './cheques'
|
export { chequeBooks, chequePages } from './cheques'
|
||||||
export { receivedCheques } from './received-cheques'
|
export { receivedCheques } from './received-cheques'
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const shopPos = {
|
|||||||
get: (id: string) =>
|
get: (id: string) =>
|
||||||
api.get<ShopPOResponse>(`/shop-pos/${id}`).then(r => r.data),
|
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) =>
|
create: (body: CreateShopPOBody) =>
|
||||||
api.post<ShopPOResponse>('/shop-pos', body).then(r => r.data),
|
api.post<ShopPOResponse>('/shop-pos', body).then(r => r.data),
|
||||||
|
|
||||||
@@ -84,6 +87,9 @@ export const invoices = {
|
|||||||
get: (id: string) =>
|
get: (id: string) =>
|
||||||
api.get<InvoiceResponse>(`/invoices/${id}`).then(r => r.data),
|
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) =>
|
print: (id: string, printedByUserId?: string | null) =>
|
||||||
api.post<InvoiceResponse>(`/invoices/${id}/print`, { printedByUserId }).then(r => r.data),
|
api.post<InvoiceResponse>(`/invoices/${id}/print`, { printedByUserId }).then(r => r.data),
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,19 @@ export const grnTaxSettings = {
|
|||||||
.getAll()
|
.getAll()
|
||||||
.then(all => parseGrnTaxSettings(Object.fromEntries(all.map(c => [c.key, c.value])))),
|
.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),
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user