Merge pull request 'Dev' (#42) from dev into production
Reviewed-on: #42
This commit was merged in pull request #42.
This commit is contained in:
@@ -29,12 +29,15 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/print")]
|
||||
public async Task<IActionResult> Print(Guid id, CancellationToken ct)
|
||||
public async Task<IActionResult> Print(Guid id, [FromBody] PrintInvoiceRequest? request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
// 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?.PrintedByUserId;
|
||||
if (userId is null) return BadRequest(new { error = "PrintedByUserId is required while auth is unavailable" });
|
||||
try
|
||||
{
|
||||
var result = await service.PrintAsync(id, userId, ct);
|
||||
var result = await service.PrintAsync(id, userId.Value, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
@@ -44,10 +47,13 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
|
||||
[HttpPost("{id:guid}/finalize")]
|
||||
public async Task<IActionResult> Finalize(Guid id, [FromBody] FinalizeInvoiceRequest? request, CancellationToken ct)
|
||||
{
|
||||
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
|
||||
// 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?.FinalizedByUserId;
|
||||
if (userId is null) return BadRequest(new { error = "FinalizedByUserId is required while auth is unavailable" });
|
||||
try
|
||||
{
|
||||
var result = await service.FinalizeAsync(id, request ?? new FinalizeInvoiceRequest(), userId, ct);
|
||||
var result = await service.FinalizeAsync(id, request ?? new FinalizeInvoiceRequest(), userId.Value, ct);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace Distribution.Features.Invoice;
|
||||
|
||||
public record CreateInvoiceRequest(Guid ShopPOId);
|
||||
public record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note = null);
|
||||
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null);
|
||||
public record FinalizeInvoiceRequest(List<InvoiceItemAdjustment>? Adjustments = null, Guid? FinalizedByUserId = null);
|
||||
public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
|
||||
|
||||
public record InvoiceResponse(
|
||||
Guid Id,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { InvoiceResponse, InvoiceStatus, PaymentResponse, PaymentMethod, Pa
|
||||
|
||||
import { PaymentSheet } from '../../payments/_components/payment-sheet'
|
||||
import { PaymentViewSheet } from '../../payments/_components/payment-view-sheet'
|
||||
import { InvoiceActionDialog } from '../../orders/_components/invoice-action-dialog'
|
||||
|
||||
const INVOICE_STATUS_VARIANT: Record<InvoiceStatus, 'default' | 'secondary' | 'outline'> = {
|
||||
Draft: 'secondary',
|
||||
@@ -58,6 +59,8 @@ export default function InvoiceViewPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [paymentSheetOpen, setPaymentSheetOpen] = useState(false)
|
||||
const [viewPayment, setViewPayment] = useState<PaymentResponse | null>(null)
|
||||
const [actionOpen, setActionOpen] = useState(false)
|
||||
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
|
||||
|
||||
const fetchInvoice = useCallback(async () => {
|
||||
const inv = await invoicesApi.get(params.id)
|
||||
@@ -86,26 +89,14 @@ export default function InvoiceViewPage() {
|
||||
}
|
||||
}, [fetchInvoice, fetchPayments, router])
|
||||
|
||||
async function handlePrint() {
|
||||
if (!invoice) return
|
||||
try {
|
||||
const updated = await invoicesApi.print(invoice.id)
|
||||
setInvoice(updated)
|
||||
toast.add({ title: 'Invoice printed', type: 'success' })
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to print invoice', description: getApiError(err), type: 'error' })
|
||||
}
|
||||
function handlePrint() {
|
||||
setActionMode('print')
|
||||
setActionOpen(true)
|
||||
}
|
||||
|
||||
async function handleFinalize() {
|
||||
if (!invoice) return
|
||||
try {
|
||||
const updated = await invoicesApi.finalize(invoice.id)
|
||||
setInvoice(updated)
|
||||
toast.add({ title: 'Invoice finalized', type: 'success' })
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to finalize invoice', description: getApiError(err), type: 'error' })
|
||||
}
|
||||
function handleFinalize() {
|
||||
setActionMode('finalize')
|
||||
setActionOpen(true)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -240,6 +231,14 @@ export default function InvoiceViewPage() {
|
||||
payment={viewPayment}
|
||||
invoiceName={`${invoice.storeName} — ${truncId(invoice.id)}`}
|
||||
/>
|
||||
|
||||
<InvoiceActionDialog
|
||||
open={actionOpen}
|
||||
onClose={() => setActionOpen(false)}
|
||||
onSuccess={fetchInvoice}
|
||||
invoice={invoice}
|
||||
mode={actionMode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
'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, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import type { InvoiceResponse, UserResponse } from '@/types/api'
|
||||
|
||||
const schema = z.object({
|
||||
userId: z.string().min(1, 'This is required'),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
|
||||
interface InvoiceActionDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
invoice: InvoiceResponse | null
|
||||
mode: 'print' | 'finalize'
|
||||
}
|
||||
|
||||
const COPY = {
|
||||
print: { title: 'Mark Invoice as Printed', label: 'Printed By', action: 'Mark as Printed', success: 'Invoice marked as printed' },
|
||||
finalize: { title: 'Finalize Invoice', label: 'Finalized By', action: 'Finalize Invoice', success: 'Invoice finalized' },
|
||||
}
|
||||
|
||||
export function InvoiceActionDialog({ open, onClose, onSuccess, invoice, mode }: InvoiceActionDialogProps) {
|
||||
const [usersList, setUsersList] = useState<UserResponse[]>([])
|
||||
const copy = COPY[mode]
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { userId: '' },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
form.reset({ userId: '' })
|
||||
usersApi.list({ pageSize: 200 })
|
||||
.then((res) => setUsersList(getPaginatedItems(res)))
|
||||
.catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' }))
|
||||
}, [open])
|
||||
|
||||
if (!invoice) return null
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
try {
|
||||
if (mode === 'print') await invoicesApi.print(invoice!.id, values.userId)
|
||||
else await invoicesApi.finalize(invoice!.id, values.userId)
|
||||
toast.add({ title: copy.success, type: 'success' })
|
||||
onSuccess()
|
||||
onClose()
|
||||
} catch (err) {
|
||||
toast.add({ title: `Failed to ${mode} invoice`, description: getApiError(err), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{copy.title}</DialogTitle>
|
||||
<DialogDescription>{invoice.storeName} — Rs. {invoice.totalAmount.toLocaleString()}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
<FieldGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="userId"
|
||||
label={copy.label}
|
||||
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>
|
||||
<SelectItem value="" disabled>Select user...</SelectItem>
|
||||
{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}>
|
||||
{form.formState.isSubmitting ? 'Saving…' : copy.action}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { invoices as invoicesApi, getApiError, getPaginatedItems } from '@/lib/a
|
||||
import type { InvoiceResponse, InvoiceStatus, PaymentStatus } from '@/types/api'
|
||||
|
||||
import { OrderViewSheet } from './order-view-sheet'
|
||||
import { InvoiceActionDialog } from './invoice-action-dialog'
|
||||
|
||||
const fmtDate = (v: string) =>
|
||||
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
@@ -38,6 +39,8 @@ export function OrdersClient() {
|
||||
|
||||
const [viewOpen, setViewOpen] = useState(false)
|
||||
const [selectedInvoice, setSelectedInvoice] = useState<InvoiceResponse | null>(null)
|
||||
const [actionOpen, setActionOpen] = useState(false)
|
||||
const [actionMode, setActionMode] = useState<'print' | 'finalize'>('print')
|
||||
|
||||
const fetchData = useCallback(async (p: number, ps: number) => {
|
||||
setLoading(true)
|
||||
@@ -61,24 +64,16 @@ export function OrdersClient() {
|
||||
setViewOpen(true)
|
||||
}
|
||||
|
||||
async function handlePrint(row: InvoiceResponse) {
|
||||
try {
|
||||
await invoicesApi.print(row.id)
|
||||
toast.add({ title: 'Invoice marked as printed', type: 'success' })
|
||||
fetchData(page, pageSize)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to print invoice', description: getApiError(err), type: 'error' })
|
||||
}
|
||||
function handlePrint(row: InvoiceResponse) {
|
||||
setSelectedInvoice(row)
|
||||
setActionMode('print')
|
||||
setActionOpen(true)
|
||||
}
|
||||
|
||||
async function handleFinalize(row: InvoiceResponse) {
|
||||
try {
|
||||
await invoicesApi.finalize(row.id)
|
||||
toast.add({ title: 'Invoice finalized', type: 'success' })
|
||||
fetchData(page, pageSize)
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to finalize invoice', description: getApiError(err), type: 'error' })
|
||||
}
|
||||
function handleFinalize(row: InvoiceResponse) {
|
||||
setSelectedInvoice(row)
|
||||
setActionMode('finalize')
|
||||
setActionOpen(true)
|
||||
}
|
||||
|
||||
const tableConfig = useMemo<TableConfig<InvoiceResponse>>(() => ({
|
||||
@@ -187,6 +182,14 @@ export function OrdersClient() {
|
||||
onClose={() => setViewOpen(false)}
|
||||
invoice={selectedInvoice}
|
||||
/>
|
||||
|
||||
<InvoiceActionDialog
|
||||
open={actionOpen}
|
||||
onClose={() => setActionOpen(false)}
|
||||
onSuccess={() => fetchData(page, pageSize)}
|
||||
invoice={selectedInvoice}
|
||||
mode={actionMode}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@ export const invoices = {
|
||||
get: (id: string) =>
|
||||
api.get<InvoiceResponse>(`/invoices/${id}`).then(r => r.data),
|
||||
|
||||
print: (id: string) =>
|
||||
api.post<InvoiceResponse>(`/invoices/${id}/print`).then(r => r.data),
|
||||
print: (id: string, printedByUserId?: string | null) =>
|
||||
api.post<InvoiceResponse>(`/invoices/${id}/print`, { printedByUserId }).then(r => r.data),
|
||||
|
||||
finalize: (id: string) =>
|
||||
api.post<InvoiceResponse>(`/invoices/${id}/finalize`).then(r => r.data),
|
||||
finalize: (id: string, finalizedByUserId?: string | null) =>
|
||||
api.post<InvoiceResponse>(`/invoices/${id}/finalize`, { finalizedByUserId }).then(r => r.data),
|
||||
}
|
||||
|
||||
// ─── Payments ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user