Merge branch 'hotfix/bug-reported-by-user' into 'production'

[HOTFIX/FE] Adjustment

See merge request mbugroup/lti-web-client!365
This commit is contained in:
Adnan Zahir
2026-04-02 14:37:14 +07:00
10 changed files with 267 additions and 26 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ const NumberInput = ({
| undefined; | undefined;
if (newChangeEvent) { if (newChangeEvent) {
newChangeEvent.target.value = numberFormatValues.value; newChangeEvent.target.value = parseFloat(
numberFormatValues.value
) as unknown as string;
onChange?.(newChangeEvent); onChange?.(newChangeEvent);
} }
@@ -287,8 +287,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
PT LUMBUNG TELUR INDONESIA PT LUMBUNG TELUR INDONESIA
</Text> </Text>
<Text style={ExpensePDFStyle.companyAddress}> <Text style={ExpensePDFStyle.companyAddress}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Cipedes, Kec. Sukajadi, Kota Bandung 40162 Bandung Barat, Jawa Barat 40514
</Text> </Text>
<View style={ExpensePDFStyle.doubleDivider} /> <View style={ExpensePDFStyle.doubleDivider} />
@@ -101,8 +101,8 @@ const PDFDocument = ({
<View style={pdfStyles.header}> <View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text> <Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}> <Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Cipedes, Kec. Sukajadi, Kota Bandung 40162 Bandung Barat, Jawa Barat 40514
</Text> </Text>
<View style={pdfStyles.divider} /> <View style={pdfStyles.divider} />
</View> </View>
@@ -87,8 +87,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => {
<View style={pdfStyles.header}> <View style={pdfStyles.header}>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text> <Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}> <Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Cipedes, Kec. Sukajadi, Kota Bandung 40162 Bandung Barat, Jawa Barat 40514
</Text> </Text>
<View style={pdfStyles.divider} /> <View style={pdfStyles.divider} />
</View> </View>
@@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: values.sku, sku: values.sku,
uom_id: values.uom_id, uom_id: values.uom_id,
product_category_id: values.product_category_id, product_category_id: values.product_category_id,
product_price: parseInt(values.product_price.toString()) || 0, product_price: parseFloat(values.product_price.toString()) || 0,
selling_price: values.selling_price selling_price: values.selling_price
? parseInt(values.selling_price.toString()) || 0 ? parseFloat(values.selling_price.toString()) || 0
: undefined, : undefined,
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined, tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined,
expiry_period: values.expiry_period expiry_period: values.expiry_period
? parseInt(values.expiry_period.toString()) || 0 ? parseFloat(values.expiry_period.toString()) || 0
: undefined, : undefined,
suppliers: values.suppliers.map((s) => ({ suppliers: values.suppliers.map((s) => ({
supplier_id: s.supplier?.value as number, supplier_id: s.supplier?.value as number,
price: parseInt(s.price.toString()) || 0, price: parseFloat(s.price.toString()) || 0,
})), })),
flag: values.flag, flag: values.flag,
sub_flags: values.sub_flags, sub_flags: values.sub_flags,
@@ -0,0 +1,201 @@
'use client';
import { RefObject, useState, useEffect } from 'react';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import Modal from '@/components/Modal';
import Button from '@/components/Button';
import DateInput from '@/components/input/DateInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { PurchaseFilter } from '@/types/api/purchase/purchase';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
interface PurchaseFilterModalProps {
ref: RefObject<HTMLDialogElement | null>;
onSubmit?: (values: PurchaseFilter) => void;
onReset?: () => void;
}
const PurchaseFilterModal = ({
ref,
onSubmit,
onReset,
}: PurchaseFilterModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
};
// ===== DATE ERROR STATE =====
const [dateErrorShown, setDateErrorShown] = useState(false);
const [hasDateError, setHasDateError] = useState(false);
// ===== CLEANUP TOAST ON UNMOUNT =====
useEffect(() => {
return () => {
if (dateErrorShown) {
toast.dismiss();
}
};
}, [dateErrorShown]);
// ===== CLEANUP TOAST WHEN MODAL CLOSES =====
useEffect(() => {
const dialogElement = ref.current;
const handleModalClose = () => {
if (dateErrorShown) {
toast.dismiss();
setDateErrorShown(false);
}
};
dialogElement?.addEventListener('close', handleModalClose);
return () => {
dialogElement?.removeEventListener('close', handleModalClose);
};
}, [ref, dateErrorShown]);
const {
setInputValue: setProductCategoryInputValue,
options: productCategoryOptions,
isLoadingOptions: isLoadingProductCategoryOptions,
loadMore: loadMoreProductCategory,
} = useSelect<ProductCategory>(
ProductCategoryApi.basePath,
'id',
'name',
'search'
);
const formik = useFormik<{
poDate: string;
category: { label: string; value: number }[];
status: { label: string; value: string }[];
}>({
initialValues: {
poDate: '',
category: [],
status: [],
},
onSubmit: async (values) => {
const formattedValues = {
...values,
category: values.category.map((item) => String(item.value)),
status: values.status.map((item) => String(item.value)),
};
onSubmit?.(formattedValues);
closeModalHandler();
},
onReset: () => {
onReset?.();
closeModalHandler();
},
});
const productCategoryChangeHandler = (
val: OptionType | OptionType[] | null
) => {
formik.setFieldValue('category', val);
};
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('status', val);
};
return (
<Modal
ref={ref}
className={{
modalBox: 'p-0 rounded-xl',
}}
>
<form
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
className='w-full flex flex-col'
>
{/* Modal Header */}
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
<div className='flex items-center gap-2 text-primary'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='text-sm font-medium'>Filter Data</h3>
</div>
<Button
type='button'
variant='ghost'
color='none'
onClick={closeModalHandler}
className='p-0 text-base-content/50 hover:text-base-content'
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Modal Body */}
<div className='p-4 flex flex-col gap-1.5'>
<div className='flex flex-col'>
<DateInput
label='PO Date'
name='poDate'
placeholder='Pilih Tanggal'
value={formik.values.poDate}
onChange={formik.handleChange}
isNestedModal
/>
<SelectInputCheckbox
label='Kategori'
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={productCategoryChangeHandler}
options={productCategoryOptions}
isLoading={isLoadingProductCategoryOptions}
onInputChange={setProductCategoryInputValue}
onMenuScrollToBottom={loadMoreProductCategory}
/>
<SelectInputCheckbox
label='Status'
placeholder='Status'
value={formik.values.status}
onChange={statusChangeHandler}
options={PURCHASE_ORDER_APPROVAL_LINE.map((item) => ({
label: item.step_name,
value: item.step_name,
}))}
/>
</div>
</div>
{/* Modal Footer */}
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
<Button
type='reset'
variant='ghost'
color='none'
className='p-3 rounded-lg text-base-content/65'
>
Reset Filter
</Button>
<Button
type='submit'
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
>
Apply Filter
</Button>
</div>
</form>
</Modal>
);
};
export default PurchaseFilterModal;
@@ -14,6 +14,7 @@ import useSWRInfinite from 'swr/infinite';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import Link from 'next/link';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Table from '@/components/Table'; import Table from '@/components/Table';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
@@ -25,18 +26,19 @@ import PopoverContent from '@/components/popover/PopoverContent';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge'; import StatusBadge from '@/components/helper/StatusBadge';
import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton';
import ButtonFilter from '@/components/helper/ButtonFilter';
import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Purchase } from '@/types/api/purchase/purchase'; import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase'; import { PurchaseApi } from '@/services/api/purchase';
import { ExpenseApi } from '@/services/api/expense'; import { ExpenseApi } from '@/services/api/expense';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
import Link from 'next/link';
// ===== STATUS BADGE UTILITIES ===== // ===== STATUS BADGE UTILITIES =====
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
@@ -165,14 +167,21 @@ const PurchaseTable = () => {
} = useTableFilter({ } = useTableFilter({
initial: { initial: {
search: '', search: '',
po_date: '',
approval_status: '',
product_category_id: '',
}, },
paramMap: { paramMap: {
page: 'page', page: 'page',
pageSize: 'limit', pageSize: 'limit',
po_date: 'po_date',
approval_status: 'approval_status',
product_category_id: 'product_category_id',
}, },
}); });
// ===== MODAL HOOKS ===== // ===== MODAL HOOKS =====
const filterModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
@@ -410,13 +419,17 @@ const PurchaseTable = () => {
[updateFilter, setSearchValue] [updateFilter, setSearchValue]
); );
// const pageSizeChangeHandler = useCallback( const filterSubmitHandler = (values: PurchaseFilter) => {
// (val: OptionType | OptionType[] | null) => { updateFilter('po_date', values.poDate);
// const newVal = val as OptionType; updateFilter('product_category_id', values.category.join(','));
// setPageSize(newVal.value as number); updateFilter('approval_status', values.status.join(','));
// }, };
// [setPageSize]
// ); const filterResetHandler = () => {
updateFilter('po_date', '');
updateFilter('product_category_id', '');
updateFilter('approval_status', '');
};
return ( return (
<> <>
@@ -455,6 +468,19 @@ const PurchaseTable = () => {
'placeholder:font-semibold placeholder:text-base-content/50', 'placeholder:font-semibold placeholder:text-base-content/50',
}} }}
/> />
<ButtonFilter
values={tableFilterState}
excludeFields={[
'page',
'pageSize',
'search',
'filter_by',
'sort_by',
]}
onClick={filterModal.openModal}
className='px-3 py-2.5'
/>
</div> </div>
</div> </div>
@@ -513,6 +539,12 @@ const PurchaseTable = () => {
</div> </div>
{/* ===== MODAL COMPONENTS ===== */} {/* ===== MODAL COMPONENTS ===== */}
<PurchaseFilterModal
ref={filterModal.ref}
onSubmit={filterSubmitHandler}
onReset={filterResetHandler}
/>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
@@ -34,7 +34,7 @@ const pdfStyles = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
}, },
logo: { logo: {
width: 120, width: 30,
height: 30, height: 30,
marginBottom: 8, marginBottom: 8,
}, },
@@ -265,7 +265,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
<View style={pdfStyles.header}> <View style={pdfStyles.header}>
{/* eslint-disable-next-line jsx-a11y/alt-text */} {/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image <Image
src={'https://placehold.co/120x30/png'} src='/assets/img/lti-logo.png'
style={pdfStyles.logo} style={pdfStyles.logo}
id={'mbu-logo'} id={'mbu-logo'}
/> />
@@ -273,8 +273,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
PT LUMBUNG TELUR INDONESIA PT LUMBUNG TELUR INDONESIA
</Text> </Text>
<Text style={pdfStyles.address}> <Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
Cipedes, Kec. Sukajadi, Kota Bandung 40162 Bandung Barat, Jawa Barat 40514
</Text> </Text>
<View style={pdfStyles.divider} /> <View style={pdfStyles.divider} />
</View> </View>
@@ -47,7 +47,7 @@ export const generateReportExpensePDF = async (
doc.setFontSize(7); doc.setFontSize(7);
doc.setTextColor(102, 102, 102); doc.setTextColor(102, 102, 102);
doc.text( doc.text(
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162', 'Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten Bandung Barat, Jawa Barat 40514',
marginX, marginX,
25 25
); );
+6
View File
@@ -144,3 +144,9 @@ export type DeletePurchaseRequestItemPayload = {
}; };
export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload;
export type PurchaseFilter = {
poDate: string;
category: string[];
status: string[];
};