Created
March 14, 2024 03:25
-
-
Save joesoeph/e64b2cbe9d4ad8968e6ab519c27d3208 to your computer and use it in GitHub Desktop.
Handle Calculation Field Items Using Ant Design Component.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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