This commit is contained in:
hagiye
2026-06-25 11:00:40 +03:00
parent 503878110f
commit f4a90755c2
42 changed files with 2100 additions and 98 deletions

View File

@@ -1,4 +1,5 @@
import { Badge, Button, Card, Divider, Group, Loader, Modal, Stack, Text } from '@mantine/core';
import { useState } from 'react';
import { Badge, Button, Card, Divider, Group, Loader, Modal, SegmentedControl, Stack, Text } from '@mantine/core';
import { CalendarClock, Coins, DoorOpen, FileText } from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
@@ -91,10 +92,11 @@ function Row({ label, value }: { label: string; value: string }) {
/** Batch 5 fee preview + Batch 6 invoice generation / gate clearance for an inventory item. */
export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModalProps) {
const { toast } = useToast();
const [billingCurrency, setBillingCurrency] = useState<'ETB' | 'USD'>('USD');
const enabledId = opened ? inventoryId ?? undefined : undefined;
const { data, isLoading } = useQuery(
api.warehouses.feePreview.queryOptions({
input: { inventoryId: enabledId ?? '' },
input: { inventoryId: enabledId ?? '', billingCurrency },
enabled: Boolean(enabledId),
}),
);
@@ -108,21 +110,22 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa
const gateClear = useMutation(api.warehouses.gateClearance.mutationOptions());
const activeInvoice = (invoices ?? []).find((i) => i.status !== 'CANCELLED');
const totalPreviewAmount = (data ?? []).reduce((sum, fee) => sum + Number(fee.amount || 0), 0);
const handleGenerate = async (confirmZero = false) => {
const handleGenerate = async () => {
if (!inventoryId) return;
if (totalPreviewAmount <= 0) {
toast({
title: 'No fee to invoice',
description: 'The item is still within the configured free days, or no active fee rule matched it.',
});
return;
}
try {
const inv = await generate.mutateAsync({ inventoryId, confirmZero });
const inv = await generate.mutateAsync({ inventoryId, billingCurrency });
toast({ title: 'Invoice generated', description: `${inv.invoiceNumber} - ${money(inv.totalAmount, inv.currency)}` });
} catch (error) {
const msg = extractErrorMessage(error);
if (/no payable warehouse fee/i.test(msg)) {
if (window.confirm('No payable warehouse fee found. Create a zero-amount invoice anyway?')) {
handleGenerate(true);
}
return;
}
toast({ variant: 'destructive', title: 'Generate failed', description: msg });
toast({ variant: 'destructive', title: 'Generate failed', description: extractErrorMessage(error) });
}
};
@@ -172,6 +175,22 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa
<Divider label="Invoice & Release" labelPosition="left" />
<Group justify="space-between" align="center">
<Text size="sm" fw={600}>
Billing currency
</Text>
<SegmentedControl
size="xs"
value={billingCurrency}
onChange={(value) => setBillingCurrency(value as 'ETB' | 'USD')}
data={[
{ value: 'USD', label: 'USD' },
{ value: 'ETB', label: 'Birr' },
]}
disabled={Boolean(activeInvoice)}
/>
</Group>
{activeInvoice ? (
<Group justify="space-between">
<Group gap="xs">
@@ -191,7 +210,7 @@ export function FeePreviewModal({ opened, onClose, inventoryId }: FeePreviewModa
color="orange"
leftSection={<FileText size={16} />}
loading={generate.isPending}
onClick={() => handleGenerate(false)}
onClick={handleGenerate}
>
Generate Fee Invoice
</Button>