Skip to content

Instantly share code, notes, and snippets.

@joesoeph
Created March 14, 2024 03:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joesoeph/e64b2cbe9d4ad8968e6ab519c27d3208 to your computer and use it in GitHub Desktop.
Save joesoeph/e64b2cbe9d4ad8968e6ab519c27d3208 to your computer and use it in GitHub Desktop.
Handle Calculation Field Items Using Ant Design Component.
import { parseForm } from '@formdata-helper/remix';
import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useActionData, useLoaderData, useNavigation, useRouteError, useSubmit } from '@remix-run/react';
import { Button, Col, DatePicker, Form, Input, InputNumber, Row, Select, Switch, message } from 'antd';
import { useState } from 'react';
import { ChevronLeft, PlusCircle, SendFill, XLg } from 'react-bootstrap-icons';
import BreadcrumbPage from '~/components/BreadcrumbPage/BreadcrumbPage';
import ButtonNavigate from '~/components/ButtonNavigate';
import GlobalError from '~/components/GlobalError';
import ModalError, { useModalError } from '~/components/ModalError';
import { specialRoles } from '~/helpers/constant';
import { filterOption, getProductList } from '~/helpers/inputProduct';
import { toInputNumberFormat, toNumberValue } from '~/helpers/numberFormat';
import { fetchGet, fetchPost, hasAccess, redirectWithToast } from '~/services/utils.server';
const mainPage = '/purchase/invoices';
const title = 'Create Invoice';
const accessKeyword = 'canCreate';
export const meta: MetaFunction = () => {
return [{ title }];
};
export async function loader({ request }: LoaderFunctionArgs) {
const userInfo = await hasAccess(accessKeyword, request);
const apiURLs = [`dataManagement/suppliers`, `dataManagement/products`, `dataManagement/organizations`, `purchase/orders`];
const fetchAPI = async (url: string) => {
const response = await fetchGet(url, {}, request);
if (!response.ok) {
throw response;
}
return response.json();
};
try {
const [suppliers, products, organizations, purchaseOrders] = await Promise.all(apiURLs.map((url) => fetchAPI(url)));
return json({ suppliers, products, organizations, purchaseOrders, userInfo });
} catch (e) {
throw new Response(e.statusText, {
status: e.status,
statusText: e.statusText,
});
}
}
export async function action({ request }: ActionFunctionArgs) {
try {
let body = (await parseForm(request)).data;
const response = await fetchPost(`purchase/invoices`, { body }, request);
const payload = await response.json();
// This error should be handled by modal error
if (!response.ok) {
return json(payload, { status: response.status });
}
return redirectWithToast(`${mainPage}/${encodeURIComponent(payload.id)}/edit`, {
message: 'Created successfully',
type: 'success',
});
} catch (e) {
throw new Response(e.statusText, {
status: e.status,
statusText: e.statusText,
});
}
}
export default function Component() {
const { suppliers, products, organizations, purchaseOrders, userInfo } = useLoaderData<typeof loader>();
const payload = useActionData<typeof action>();
const navigation = useNavigation();
const { isModalOpen, handleCancel } = useModalError(payload);
const submit = useSubmit();
const [form] = Form.useForm();
const [lastPurchaseOrderId, setLastPurchaseOrderId] = useState('');
const [switchIsPaid, setSwitchIsPaid] = useState(false);
const onFinish = async (values: any) => {
const formData = new FormData();
formData.append('data', JSON.stringify(values));
await submit(formData, { method: 'post' });
};
const onFinishFailed = (errorInfo: any) => {
message.error('Fail to submit form. Please check again.');
};
const handlePurchaseOrderChange = () => {
const purchaseOrderId = form.getFieldValue('purchaseOrderId');
if (purchaseOrderId !== lastPurchaseOrderId) {
const purchaseOrder = purchaseOrders.find((item) => item.id === purchaseOrderId);
if (purchaseOrder) {
const purchaseInvoiceItems = purchaseOrder.purchaseOrderItems.map((item) => {
return {
productId: item.productId,
quantity: item.quantity,
unitPriceIDR: item.unitPriceIDR,
unitPriceUSD: item.unitPriceUSD,
amountUSD: item.amountUSD,
amountIDR: item.amountIDR,
};
});
const organizationId = purchaseOrder.organizationId;
const supplierId = purchaseOrder.supplierId;
const rateUSDToIDR = purchaseOrder.rateUSDToIDR;
form.setFieldsValue({
purchaseInvoiceItems,
organizationId,
supplierId,
rateUSDToIDR,
});
message.info('Autofill');
}
setLastPurchaseOrderId(purchaseOrderId);
}
};
const handleIsPaidChange = () => {
const isPaid = form.getFieldValue('isPaid');
if (isPaid) {
setSwitchIsPaid(true);
} else {
setSwitchIsPaid(false);
}
};
const handleCalculation = (changedValue) => {
const rateUSDToIDR = form.getFieldValue('rateUSDToIDR') ?? 0;
const purchaseInvoiceItems = form.getFieldValue('purchaseInvoiceItems') ?? [];
const findIndicesWithValue = (arr) => {
return arr.findIndex((value) => value !== undefined);
};
const isAmountIDRChanged = changedValue?.purchaseInvoiceItems?.find((item) => item?.amountIDR) !== undefined;
const isPurchaseInvoiceItemsChanged = changedValue?.purchaseInvoiceItems !== undefined;
const isRateUSDToIDRChanged = changedValue?.rateUSDToIDR !== undefined;
// Don't calculate the amountIDR if the amountIDR is changed by special user
if (!isAmountIDRChanged) {
// We need calculate based on the changed purchaseInvoiceItems row.
// This is prevent to calculate all of the purchaseInvoiceItems
if (isPurchaseInvoiceItemsChanged) {
const changedPurchaseInvoiceItems = changedValue?.purchaseInvoiceItems;
const rowChanged = findIndicesWithValue(changedPurchaseInvoiceItems);
if (rowChanged !== -1) {
const quantity = purchaseInvoiceItems[rowChanged]?.quantity ?? 0;
const unitPriceUSD = purchaseInvoiceItems[rowChanged]?.unitPriceUSD ?? 0;
const amountUSD = quantity * unitPriceUSD;
const amountIDR = amountUSD * rateUSDToIDR;
const unitPriceIDR = unitPriceUSD * rateUSDToIDR;
changedPurchaseInvoiceItems[rowChanged] = {
...purchaseInvoiceItems[rowChanged],
amountUSD,
amountIDR,
unitPriceIDR,
};
purchaseInvoiceItems[rowChanged] = changedPurchaseInvoiceItems[rowChanged];
form.setFieldsValue({ purchaseInvoiceItems });
}
}
// We just need to calculate the amountIDR if the rateUSDToIDR is changed
if (isRateUSDToIDRChanged) {
purchaseInvoiceItems.forEach((item: any, index: number) => {
const quantity = item?.quantity ?? 0;
const unitPriceUSD = item?.unitPriceUSD ?? 0;
const amountUSD = quantity * unitPriceUSD;
const amountIDR = amountUSD * rateUSDToIDR;
const unitPriceIDR = unitPriceUSD * rateUSDToIDR;
purchaseInvoiceItems[index] = {
...item,
amountUSD,
amountIDR,
unitPriceIDR,
};
});
form.setFieldsValue({ purchaseInvoiceItems });
}
}
};
const handleFormChange = (changedValue) => {
handlePurchaseOrderChange();
handleIsPaidChange();
handleCalculation(changedValue);
};
return (
<>
<BreadcrumbPage />
<div className="rounded-md bg-white p-4 shadow-md">
<div className="mb-8 flex gap-4">
<ButtonNavigate to={mainPage} icon={<ChevronLeft />}>
Back
</ButtonNavigate>
</div>
<Form layout="vertical" onFinish={onFinish} onFinishFailed={onFinishFailed} form={form} autoComplete="off" onValuesChange={handleFormChange}>
<Form.Item label="No. Doc" name="id">
<Input />
</Form.Item>
<Form.Item label="Doc. Date" name="docDate" rules={[{ required: true, type: 'date' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="Due. Date" name="dueDate" rules={[{ required: true, type: 'date' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="Purchase Order" name="purchaseOrderId">
<Select options={purchaseOrders.map((item) => ({ value: item.id, label: item.name }))} />
</Form.Item>
<Form.Item label="Organization" name="organizationId" rules={[{ required: true }]}>
<Select options={organizations.map((item) => ({ value: item.id, label: item.name }))} />
</Form.Item>
<Form.Item label="Supplier" name="supplierId" rules={[{ required: true }]}>
<Select options={suppliers.map((item) => ({ value: item.id, label: item.name }))} />
</Form.Item>
<Form.Item label="USD to IDR Rate" name="rateUSDToIDR" rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
min={1}
prefix={'IDR'}
/>
</Form.Item>
<div className="my-8 rounded-md border-[1px] border-slate-500/50 p-4 shadow-md">
<p className="mb-6 text-lg">Purchase Invoice Items:</p>
<Form.List name="purchaseInvoiceItems" initialValue={[]}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className="relative mb-3 rounded-md border-[1px] border-slate-500/50 p-4">
<Row gutter={[10, 10]} className="mb-4 flex w-full ">
<Col xs={10} lg={5}>
<Form.Item {...restField} label="Product" name={[name, 'productId']} rules={[{ required: true }]}>
<Select options={getProductList(products)} showSearch filterOption={filterOption} />
</Form.Item>
</Col>
<Col xs={6} lg={5}>
<Form.Item {...restField} label="Quantity" name={[name, 'quantity']} rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
<Col xs={8} lg={7}>
<Form.Item {...restField} label="Unit Price USD" name={[name, 'unitPriceUSD']} rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'USD'}
/>
</Form.Item>
<Form.Item {...restField} label="Unit Price IDR" name={[name, 'unitPriceIDR']} rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'IDR'}
disabled
/>
</Form.Item>
</Col>
<Col xs={8} lg={7}>
<Form.Item {...restField} label="Amount USD" name={[name, 'amountUSD']} rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'USD'}
disabled
/>
</Form.Item>
<Form.Item {...restField} label="Amount IDR" name={[name, 'amountIDR']} rules={[{ required: true }]}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'IDR'}
disabled={!specialRoles.includes(userInfo.user.role.id)}
/>
</Form.Item>
</Col>
</Row>
<Form.Item className="absolute right-0 top-0">
<Button type="text" icon={<XLg />} onClick={() => remove(name)} className="cursor-pointer" shape="circle" />
</Form.Item>
</div>
))}
<Form.Item style={{ marginBottom: 0, marginTop: 20 }}>
<Button type="dashed" onClick={() => add()} block icon={<PlusCircle />}>
Add purchase invoice items
</Button>
</Form.Item>
</>
)}
</Form.List>
</div>
<Form.Item label="Is Paid?" name="isPaid" initialValue={false}>
<Switch checkedChildren="YES" unCheckedChildren="NO" />
</Form.Item>
<Row gutter={[10, 10]}>
<Col xs={12} lg={12}>
<Form.Item label="Other Fee USD" name="otherFeeUSD" className="w-full" hidden={!switchIsPaid}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'USD'}
/>
</Form.Item>
</Col>
<Col xs={12} lg={12}>
<Form.Item label="Other Fee IDR" name="otherFeeIDR" className="w-full" hidden={!switchIsPaid}>
<InputNumber
formatter={(value) => toInputNumberFormat(value)}
parser={(value) => toNumberValue(value)}
style={{ width: '100%' }}
prefix={'IDR'}
/>
</Form.Item>
</Col>
</Row>
<Form.Item label="Note for Other Fee" name="noteForOtherFee" hidden={!switchIsPaid}>
<Input.TextArea />
</Form.Item>
<Form.Item>
<div className="mt-6 flex items-center justify-center gap-4">
<Button type="primary" htmlType="submit" loading={navigation.state !== 'idle' && navigation.formMethod === 'POST'} icon={<SendFill />}>
Submit
</Button>
</div>
</Form.Item>
</Form>
</div>
<ModalError payload={payload} handleCancel={handleCancel} isModalOpen={isModalOpen} />
</>
);
}
export function ErrorBoundary() {
const error = useRouteError();
return <GlobalError error={error} />;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment