199 lines
5.7 KiB
TypeScript
199 lines
5.7 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Button,
|
|
FileInput,
|
|
Group,
|
|
Modal,
|
|
Stack,
|
|
Text,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
import { notifications } from '@mantine/notifications';
|
|
import { IconFileDollar, IconUpload } from '@tabler/icons-react';
|
|
import axios from 'axios';
|
|
import { useMemo, useState } from 'react';
|
|
|
|
type InvoicePdfButtonProps = {
|
|
invoiceId: number;
|
|
invoiceNo?: string | null;
|
|
display?: 'icon' | 'button';
|
|
label?: string;
|
|
};
|
|
|
|
type InvoicePdfUploadResponse = {
|
|
message?: string;
|
|
pdf_url?: string;
|
|
};
|
|
|
|
export default function InvoicePdfButton({
|
|
invoiceId,
|
|
invoiceNo,
|
|
display = 'icon',
|
|
label = 'View Invoice',
|
|
}: InvoicePdfButtonProps) {
|
|
const [opened, setOpened] = useState(false);
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [checking, setChecking] = useState(false);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const urls = useMemo(
|
|
() => ({
|
|
view: route('client-invoices.getPdfInvoice', { id: invoiceId }),
|
|
upload: route('client-invoices.pdf.upload', {
|
|
invoice: invoiceId,
|
|
}),
|
|
}),
|
|
[invoiceId],
|
|
);
|
|
|
|
const openInvoice = (url = urls.view) => {
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
};
|
|
|
|
const handleView = async () => {
|
|
setChecking(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await axios.get<Blob>(urls.view, {
|
|
responseType: 'blob',
|
|
validateStatus: (status) =>
|
|
(status >= 200 && status < 300) || status === 404,
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
setOpened(true);
|
|
return;
|
|
}
|
|
|
|
const blobUrl = URL.createObjectURL(response.data);
|
|
openInvoice(blobUrl);
|
|
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
|
} catch {
|
|
notifications.show({
|
|
title: 'Unable to open invoice',
|
|
message: 'Please try again later.',
|
|
color: 'red',
|
|
});
|
|
} finally {
|
|
setChecking(false);
|
|
}
|
|
};
|
|
|
|
const handleUpload = async () => {
|
|
if (!file) {
|
|
setError('Select a PDF invoice to upload.');
|
|
return;
|
|
}
|
|
|
|
setUploading(true);
|
|
setError(null);
|
|
|
|
const formData = new FormData();
|
|
formData.append('invoice_pdf', file);
|
|
|
|
try {
|
|
const response = await axios.post<InvoicePdfUploadResponse>(
|
|
urls.upload,
|
|
formData,
|
|
{
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
},
|
|
);
|
|
|
|
notifications.show({
|
|
title: 'Invoice uploaded',
|
|
message:
|
|
response.data.message ?? 'Invoice uploaded successfully.',
|
|
color: 'green',
|
|
});
|
|
|
|
setOpened(false);
|
|
setFile(null);
|
|
} catch (caught) {
|
|
if (axios.isAxiosError(caught) && caught.response?.status === 422) {
|
|
setError('Upload a valid PDF file up to 20 MB.');
|
|
return;
|
|
}
|
|
|
|
setError('The invoice could not be uploaded. Please try again.');
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
};
|
|
|
|
const button =
|
|
display === 'button' ? (
|
|
<Button
|
|
leftIcon={<IconFileDollar size={16} />}
|
|
variant="light"
|
|
color="green"
|
|
loading={checking}
|
|
onClick={handleView}
|
|
>
|
|
{label}
|
|
</Button>
|
|
) : (
|
|
<Tooltip label={label} withArrow withinPortal>
|
|
<ActionIcon
|
|
aria-label={label}
|
|
disabled={checking}
|
|
onClick={handleView}
|
|
>
|
|
<IconFileDollar color="green" />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{button}
|
|
|
|
<Modal
|
|
opened={opened}
|
|
onClose={() => setOpened(false)}
|
|
title="Upload invoice"
|
|
centered
|
|
>
|
|
<Stack spacing="md">
|
|
<Text size="sm" color="dimmed">
|
|
{invoiceNo
|
|
? `Invoice ${invoiceNo} is not available yet. Upload the PDF to view it.`
|
|
: 'This invoice PDF is not available yet. Upload the PDF to view it.'}
|
|
</Text>
|
|
|
|
<FileInput
|
|
label="Invoice PDF"
|
|
accept="application/pdf"
|
|
icon={<IconUpload size={16} />}
|
|
value={file}
|
|
onChange={(value) => {
|
|
setFile(value);
|
|
setError(null);
|
|
}}
|
|
error={error}
|
|
/>
|
|
|
|
<Group position="right" spacing="sm">
|
|
<Button
|
|
variant="subtle"
|
|
onClick={() => setOpened(false)}
|
|
disabled={uploading}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
leftIcon={<IconUpload size={16} />}
|
|
onClick={handleUpload}
|
|
loading={uploading}
|
|
>
|
|
Upload
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|