inspiren-sem-tool/resources/js/components/invoice-pdf-button.tsx

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>
</>
);
}