inspiren-sem-tool/resources/js/forms/account/InvoiceForm.tsx

259 lines
9.1 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { InertiaFormProps } from "@inertiajs/react";
import { route } from "ziggy-js";
import axios from "axios";
import { Button, Group, Stack, TextInput, NumberInput, Loader, Select, Switch, Text } from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { IconDeviceFloppy } from "@tabler/icons-react";
import dayjs from "dayjs";
export interface InvoiceFormValues {
invoice_no: string;
linked_invoice_id: string;
is_credit_card: boolean;
is_paid: boolean;
payment_no: string;
start_date: string;
end_date: string;
amount: string;
management_fee: string;
management_fee_amount?: string;
management_fee_tax?: string;
media_fee: string;
media_fee_amount?: string;
media_fee_tax?: string;
tax_percent: string;
total_spending: string;
client_id?: string;
customer_id?: string;
nett_amount: string;
}
interface InvoiceOption {
value: string;
label: string;
}
interface Props {
form: InertiaFormProps<InvoiceFormValues>;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
submitLabel?: string;
invoiceOptions?: InvoiceOption[];
}
export default function InvoiceForm({
form,
onSubmit,
submitLabel = "Save invoice",
invoiceOptions = [],
}: Props) {
const formatDate = (value: string) => (value ? dayjs(value).toDate() : null);
const [fetchingSpend, setFetchingSpend] = useState(false);
const parseAmount = (value?: string) => {
const parsed = Number.parseFloat(value ?? "");
return Number.isFinite(parsed) ? parsed : 0;
};
const formatAmount = (value: number) => value.toFixed(2);
const calculateNettAmount = () => {
const mediaFeeAmount = form.data.media_fee;
const calculated = formatAmount(
parseAmount(mediaFeeAmount) / (1 + parseAmount(form.data.tax_percent) / 100)
);
form.setData("nett_amount", calculated);
};
useEffect(() => {
if (!form.data.is_credit_card) return;
// Credit-card invoices can be fee-free; normalize fields to 0 for convenience.
// if (form.data.management_fee !== "0") form.setData("management_fee", "0");
if (form.data.media_fee !== "0") form.setData("media_fee", "0");
// if (form.data.tax_percent !== "0") form.setData("tax_percent", "0");
}, [form.data.is_credit_card]);
useEffect(() => {
const startDate = form.data.start_date;
const endDate = form.data.end_date;
const customerId = form.data.customer_id;
if (!startDate || !endDate) {
form.setData("total_spending", "0");
setFetchingSpend(false);
return;
}
let canceled = false;
setFetchingSpend(true);
axios
.post(
route("google.getCampaignsDetails"),
{
clientCustomerId: customerId,
startDate,
endDate,
},
{
withCredentials: true,
}
)
.then((response) => {
if (canceled) return;
const value = parseFloat(response.data?.summary?.total_actual_spend ?? 0) || 0;
const formatted = value.toFixed(2);
form.setData("total_spending", formatted);
})
.catch(() => {
if (!canceled) {
form.setData("total_spending", "");
}
})
.finally(() => {
if (!canceled) {
setFetchingSpend(false);
}
});
return () => {
canceled = true;
};
}, [form.data.start_date, form.data.end_date, form.data.customer_id]);
return (
<form onSubmit={onSubmit}>
<Stack spacing="xl">
<TextInput
label="Invoice number"
value={form.data.invoice_no}
onChange={(event) => form.setData("invoice_no", event.target.value)}
error={form.errors.invoice_no}
required
/>
<Select
label="Linked invoice"
description="Leave blank if this invoice should stay independent."
data={invoiceOptions}
value={form.data.linked_invoice_id || null}
onChange={(value) => form.setData("linked_invoice_id", value ?? "")}
error={form.errors.linked_invoice_id}
clearable
searchable
nothingFound="No invoices available"
/>
<TextInput
label="Payment No"
value={form.data.payment_no}
onChange={(event) => form.setData("payment_no", event.target.value)}
error={form.errors.payment_no}
/>
<DateInput
label="Start date"
value={formatDate(form.data.start_date)}
onChange={(value) =>
form.setData("start_date", value ? dayjs(value).format("YYYY-MM-DD") : "")
}
error={form.errors.start_date}
clearable
/>
<DateInput
label="End date"
value={formatDate(form.data.end_date)}
onChange={(value) =>
form.setData("end_date", value ? dayjs(value).format("YYYY-MM-DD") : "")
}
error={form.errors.end_date}
clearable
/>
<NumberInput
precision={2}
step={0.01}
label="Management fee"
value={form.data.management_fee !== "" ? Number(form.data.management_fee) : undefined}
onChange={(value) => form.setData("management_fee", value?.toString() ?? "")}
error={form.errors.management_fee}
required
/>
<NumberInput
precision={2}
step={0.01}
label="Media fee"
value={form.data.media_fee !== "" ? Number(form.data.media_fee) : undefined}
onChange={(value) => form.setData("media_fee", value?.toString() ?? "")}
error={form.errors.media_fee}
required
/>
<NumberInput
precision={2}
step={0.01}
label="Withholding Tax (%)"
value={form.data.tax_percent !== "" ? Number(form.data.tax_percent) : undefined}
onChange={(value) => form.setData("tax_percent", value?.toString() ?? "")}
error={form.errors.tax_percent}
required
/>
<Group align="flex-end">
<NumberInput
precision={2}
label="Media Nett Amount (RM)"
value={form.data.nett_amount ? Number(form.data.nett_amount) : undefined}
onChange={(value) => form.setData("nett_amount", value?.toString() ?? "")}
error={form.errors.nett_amount}
required
style={{ flex: 1 }}
/>
<Button type="button" variant="outline" onClick={calculateNettAmount}>
Calculate
</Button>
</Group>
<NumberInput
precision={2}
label="Total Spending (RM)"
value={form.data.total_spending !== "" ? Number(form.data.total_spending) : undefined}
onChange={(value) => form.setData("total_spending", value?.toString() ?? "")}
rightSection={fetchingSpend ? <Loader size="xs" /> : null}
/>
<Text>Additional Info</Text>
<Group spacing="sm">
<Switch
aria-label="Credit card"
checked={form.data.is_credit_card}
onChange={(event) => form.setData("is_credit_card", event.currentTarget.checked)}
/>
<Text>Credit card</Text>
</Group>
{/* <Switch
label="Paid"
checked={form.data.is_paid}
onChange={(event) => form.setData("is_paid", event.currentTarget.checked)}
/> */}
<Button
type="submit"
loading={form.processing}
leftIcon={<IconDeviceFloppy size={16} />}
style={{ width: "max-content" }}
>
{submitLabel}
</Button>
</Stack>
</form>
);
}