refactor(FE-92-87): mengganti select input dengan reuseable component

This commit is contained in:
randy-ar
2025-10-30 21:23:37 +07:00
parent d0d201bf3a
commit 99194eaf80
5 changed files with 320 additions and 205 deletions
+180 -159
View File
@@ -1,5 +1,6 @@
'use client'; 'use client';
import Badge from '@/components/Badge';
import Button from '@/components/Button'; import Button from '@/components/Button';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
@@ -39,10 +40,12 @@ const AddChickin = () => {
const [isLoadingProjectFlockKandang, setIsLoadingProjectFlockKandang] = const [isLoadingProjectFlockKandang, setIsLoadingProjectFlockKandang] =
useState(false); useState(false);
const [searchProjectFlock, setSearchProjectFlock] = useState(''); const [searchProjectFlock, setSearchProjectFlock] = useState('');
const [selectedProjectFlock, setSelectedProjectFlock] =
useState<OptionType | null>(null);
// Fetch Data // Fetch Data
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
projectFlockId, projectFlockId ?? selectedProjectFlock?.value.toString(),
(id: number) => ProjectFlockApi.getSingle(id) (id: number) => ProjectFlockApi.getSingle(id)
); );
const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } = const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } =
@@ -59,7 +62,7 @@ const AddChickin = () => {
? listProjectFlock?.data.map((projectFlock) => { ? listProjectFlock?.data.map((projectFlock) => {
return { return {
value: projectFlock.id, value: projectFlock.id,
label: `${projectFlock?.flock?.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`, label: `${projectFlock?.flock?.name} - Periode ${projectFlock?.period}`,
}; };
}) })
: []; : [];
@@ -67,24 +70,6 @@ const AddChickin = () => {
const chickinModal = useModal(); const chickinModal = useModal();
const alertModal = useModal(); const alertModal = useModal();
if (!projectFlockId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (
!isLoadingProjectFlock &&
(!projectFlock || isResponseError(projectFlock))
) {
router.replace('/404');
return;
}
// Handle Function // Handle Function
const handleChickinClick = async (kandang: Kandang) => { const handleChickinClick = async (kandang: Kandang) => {
setIsLoadingProjectFlockKandang(true); setIsLoadingProjectFlockKandang(true);
@@ -95,7 +80,7 @@ const AddChickin = () => {
>(getProjectFlockKandangUrl, { >(getProjectFlockKandangUrl, {
method: 'GET', method: 'GET',
params: { params: {
project_flock_id: projectFlockId ?? 0, project_flock_id: projectFlockId ?? selectedProjectFlock?.value ?? 0,
kandang_id: kandang.id, kandang_id: kandang.id,
}, },
}); });
@@ -116,153 +101,189 @@ const AddChickin = () => {
chickinModal.closeModal(); chickinModal.closeModal();
router.push('/production/chickin'); router.push('/production/chickin');
}; };
const handleChangeProjectFlock = (val: OptionType | null) => {
setSelectedProjectFlock(val);
if (projectFlockId) {
router.push('/production/chickin/add');
}
};
return ( return (
<> <>
{isResponseSuccess(projectFlock) && ( <>
<> <section className='w-full p-4'>
<section className='w-full p-4'> <header className='flex flex-col gap-4'>
<header className='flex flex-col gap-4'> <Button
<Button href='/production/project-flock'
href='/production/project-flock' variant='link'
variant='link' className='w-fit p-0 text-primary'
className='w-fit p-0 text-primary' >
> <Icon icon='uil:arrow-left' width={24} height={24} />
<Icon icon='uil:arrow-left' width={24} height={24} /> Kembali
Kembali </Button>
</Button>
<div className='flex flex-col gap-4 w-full my-4'> <div className='flex flex-col gap-4 w-full my-4'>
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'> <div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
<SelectInput <SelectInput
required required
isSearchable label='Project Flock'
label='Project Flock' placeholder='Pilih project flock'
options={options} options={options}
isLoading={isLoadingListProjectFlock} onInputChange={(val) => {
value={{ setSearchProjectFlock(val);
label: `${projectFlock.data?.flock?.name} - ${projectFlock.data?.category} - Periode ${projectFlock.data?.period}`,
value: projectFlock.data?.id,
}}
onChange={(val) =>
router.push(
`/production/chickin/add?projectFlockId=${
(val as OptionType | null)?.value
}`
)
}
onInputChange={(val) => {
setSearchProjectFlock(val);
}}
/>
</div>
</div>
</header>
<Table<Kandang>
data={projectFlock.data?.kandangs}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama Kandang',
},
{
header: 'Aksi',
cell: (props) => {
return (
<>
<Button
color='success'
variant='outline'
onClick={() => {
handleChickinClick(props.row.original);
}}
disabled={isLoadingProjectFlockKandang}
>
<Icon
icon='mdi:home-import-outline'
width={24}
height={24}
/>
Chickin
</Button>
</>
);
},
},
]}
page={undefined}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(projectFlock) &&
projectFlock.data?.kandangs?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</section>
<Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'>
<h1 className='text-xl font-semibold text-center mb-6'>
Chickin Kandang - {selectedKandang?.name}
</h1>
<Button
color='error'
variant='link'
onClick={chickinModal.closeModal}
>
<Icon
className='text-black'
icon='uil:times'
width={24}
height={24}
/>
</Button>
</div>
{isResponseSuccess(projectFlockKandang) &&
!isLoadingProjectFlockKandang && (
<ChickinForm
initialValues={{
project_flock_kandang: projectFlockKandang.data,
created_user: projectFlock.data?.created_user,
created_at: projectFlock.data?.created_at,
updated_at: projectFlock.data?.updated_at,
approval: projectFlock.data?.approval,
}} }}
afterSubmit={handleAfterSubmit} isLoading={isLoadingListProjectFlock}
value={
isResponseSuccess(projectFlock)
? {
label: `${projectFlock.data?.flock?.name}`,
value: projectFlock.data?.id,
}
: undefined
}
onChange={(val) => {
handleChangeProjectFlock(val as OptionType);
}}
isSearchable
isClearable
startAdornment={
isResponseSuccess(projectFlock) && (
<Badge
variant='soft'
color='success'
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold',
}}
>
Periode {projectFlock.data?.period}
</Badge>
)
}
/> />
)} </div>
</Modal> </div>
<ConfirmationModal </header>
ref={alertModal.ref} <Table<Kandang>
type='info' emptyContent={
text={`Persediaan Day Old Chick pada kandang (${selectedKandang?.name}) belum ada, mohon isi terlebih dahulu di bagian Persediaan!`} <div className='w-full p-5 text-center'>
secondaryButton={undefined} {projectFlockId && isResponseError(projectFlock) ? (
primaryButton={{ <span className='text-lg opacity-50'>
text: 'Ya', {projectFlock.message}
color: 'info', </span>
onClick: () => { ) : (
alertModal.closeModal(); <span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
)}
</div>
}
data={
isResponseSuccess(projectFlock) ? projectFlock.data?.kandangs : []
}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
}, },
{
accessorKey: 'name',
header: 'Nama Kandang',
},
{
header: 'Aksi',
cell: (props) => {
return (
<>
<Button
color='success'
variant='outline'
onClick={() => {
handleChickinClick(props.row.original);
}}
disabled={isLoadingProjectFlockKandang}
>
<Icon
icon='mdi:home-import-outline'
width={24}
height={24}
/>
Chickin
</Button>
</>
);
},
},
]}
page={undefined}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(projectFlock) &&
projectFlock.data?.kandangs?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}} }}
/> />
</> </section>
)} <Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'>
<h1 className='text-xl font-semibold text-center mb-6'>
Chickin Kandang - {selectedKandang?.name}
</h1>
<Button
color='error'
variant='link'
onClick={chickinModal.closeModal}
>
<Icon
className='text-black'
icon='uil:times'
width={24}
height={24}
/>
</Button>
</div>
{isResponseSuccess(projectFlockKandang) &&
isResponseSuccess(projectFlock) &&
!isLoadingProjectFlockKandang && (
<ChickinForm
initialValues={{
project_flock_kandang: projectFlockKandang.data,
created_user: projectFlock.data?.created_user,
created_at: projectFlock.data?.created_at,
updated_at: projectFlock.data?.updated_at,
approval: projectFlock.data?.approval,
}}
afterSubmit={handleAfterSubmit}
/>
)}
</Modal>
<ConfirmationModal
ref={alertModal.ref}
type='info'
text={`Persediaan Day Old Chick pada kandang (${selectedKandang?.name}) belum ada, mohon isi terlebih dahulu di bagian Persediaan!`}
secondaryButton={undefined}
primaryButton={{
text: 'Ya',
color: 'info',
onClick: () => {
alertModal.closeModal();
},
}}
/>
</>
</> </>
); );
}; };
+63 -20
View File
@@ -4,10 +4,24 @@ import {
ChangeEventHandler, ChangeEventHandler,
FocusEventHandler, FocusEventHandler,
ReactNode, ReactNode,
useState,
} from 'react'; } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
const formatToISO = (dateStr: string): string | null => {
const parts = dateStr.split('/');
if (parts.length !== 3) return null;
const [day, month, year] = parts;
if (!day || !month || !year) return null;
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
};
const formatToLocal = (isoDate: string): string => {
if (!isoDate) return '';
const [year, month, day] = isoDate.split('-');
return `${day}/${month}/${year}`;
};
export interface DateInputProps { export interface DateInputProps {
label?: string; label?: string;
bottomLabel?: string; bottomLabel?: string;
@@ -44,9 +58,9 @@ const DateInput = ({
min, min,
max, max,
className, className,
isError, isError: externalError,
isValid, isValid: externalValid,
errorMessage, errorMessage: externalErrorMessage,
startAdornment, startAdornment,
endAdornment, endAdornment,
disabled = false, disabled = false,
@@ -56,6 +70,40 @@ const DateInput = ({
readOnly = false, readOnly = false,
isLoading = false, isLoading = false,
}: DateInputProps) => { }: DateInputProps) => {
const [internalError, setInternalError] = useState<string | null>(null);
const minISO = min ? formatToISO(min) ?? undefined : undefined;
const maxISO = max ? formatToISO(max) ?? undefined : undefined;
const valueISO =
value && value.includes('/') ? formatToISO(value) ?? '' : value ?? '';
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
const selectedDate = e.target.value;
const isoMin = minISO;
const isoMax = maxISO;
if (isoMin && selectedDate < isoMin) {
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
} else if (isoMax && selectedDate > isoMax) {
setInternalError(`Tanggal tidak boleh setelah ${max}`);
} else {
setInternalError(null);
}
const event = {
...e,
target: {
...e.target,
value: formatToLocal(selectedDate),
},
};
onChange?.(event as React.ChangeEvent<HTMLInputElement>);
};
const finalIsError = externalError || !!internalError;
const finalErrorMessage = internalError || externalErrorMessage;
return ( return (
<div <div
className={cn( className={cn(
@@ -68,9 +116,7 @@ const DateInput = ({
htmlFor={name} htmlFor={name}
className={cn( className={cn(
'w-full text-sm font-normal leading-5', 'w-full text-sm font-normal leading-5',
{ { 'text-error': finalIsError },
'text-error': isError,
},
className?.label className?.label
)} )}
> >
@@ -90,8 +136,8 @@ const DateInput = ({
className={cn( className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200 flex items-center', 'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200 flex items-center',
{ {
'border-error': isError, 'border-error': finalIsError,
'border-success!': isValid, 'border-success!': externalValid && !finalIsError,
}, },
className?.inputWrapper className?.inputWrapper
)} )}
@@ -103,16 +149,13 @@ const DateInput = ({
id={name} id={name}
name={name} name={name}
placeholder={placeholder} placeholder={placeholder}
value={value} value={valueISO}
onChange={onChange} onChange={handleChange}
onBlur={onBlur} onBlur={onBlur}
min={min} min={minISO}
max={max} max={maxISO}
disabled={disabled} disabled={disabled}
className={cn( className={cn('grow bg-transparent cursor-pointer', className?.input)}
'grow bg-transparent cursor-pointer',
className?.input
)}
readOnly={readOnly} readOnly={readOnly}
/> />
@@ -124,11 +167,11 @@ const DateInput = ({
)} )}
</div> </div>
{!isError && bottomLabel && ( {!finalIsError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p> <p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)} )}
{isError && errorMessage && ( {finalIsError && finalErrorMessage && (
<p className='w-full text-sm text-error'>{errorMessage}</p> <p className='w-full text-sm text-error'>{finalErrorMessage}</p>
)} )}
</div> </div>
); );
+64 -19
View File
@@ -1,22 +1,23 @@
'use client'; 'use client';
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react'; import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import Select, { import Select, {
OptionProps, OptionProps,
GroupBase, GroupBase,
InputActionMeta, InputActionMeta,
MultiValue, MultiValue,
SingleValue, SingleValue,
components as ReactSelectComponents,
ControlProps,
} from 'react-select'; } from 'react-select';
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper'; import { cn, getByPath } from '@/lib/helper';
import useSWR from 'swr';
import { httpClientFetcher } from '@/services/http/client'; import { httpClientFetcher } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { isResponseSuccess } from '@/lib/api-helper';
export interface OptionType { export interface OptionType {
value: string | number; value: string | number;
@@ -53,6 +54,7 @@ interface SelectInputBaseProps<T = OptionType> {
openMenu?: boolean; openMenu?: boolean;
delay?: number; delay?: number;
onInputChange?: (search: string) => void; onInputChange?: (search: string) => void;
startAdornment?: ReactNode;
} }
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> { interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
@@ -63,6 +65,33 @@ interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
const animatedComponents = makeAnimated(); const animatedComponents = makeAnimated();
const CustomControl = <
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
>(
props: ControlProps<Option, IsMulti, Group>
) => {
const { children } = props;
const customProps = props.selectProps as unknown as {
shouldShowAdornment?: boolean;
startAdornment?: ReactNode;
};
const shouldShowAdornment = customProps.shouldShowAdornment ?? false;
const startAdornment = customProps.startAdornment;
return (
<ReactSelectComponents.Control {...props}>
<div className='flex-1 px-4! py-1.5 gap-1 flex items-center'>
{shouldShowAdornment && startAdornment}
{children}
</div>
</ReactSelectComponents.Control>
);
};
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => { const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const { const {
label, label,
@@ -87,15 +116,24 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
delay = 300, delay = 300,
createables = false, createables = false,
onInputChange, onInputChange,
startAdornment,
} = props; } = props;
const [internalInputValue, setInternalInputValue] = useState(''); const [internalInputValue, setInternalInputValue] = useState('');
const [debouncedInputValue] = useDebounce(internalInputValue, delay); const [debouncedInputValue] = useDebounce(internalInputValue, delay);
const shouldShowAdornment = startAdornment && !internalInputValue;
const components = useMemo(() => { const components = useMemo(() => {
const base = isAnimated ? animatedComponents : {}; const base = isAnimated ? animatedComponents : {};
return { ...base, IndicatorSeparator: () => null }; const customComponents = { ...base, IndicatorSeparator: () => null };
}, [isAnimated]);
if (startAdornment) {
customComponents.Control = CustomControl;
}
return customComponents;
}, [isAnimated, startAdornment]);
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => { const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') setInternalInputValue(val); if (meta.action === 'input-change') setInternalInputValue(val);
@@ -148,12 +186,13 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
<SelectComponent<T, boolean, GroupBase<T>> <SelectComponent<T, boolean, GroupBase<T>>
instanceId='select' instanceId='select'
value={value ?? (isMulti ? [] : null)} value={value ?? (isMulti ? [] : undefined)}
onChange={handleChange} onChange={onChange ? handleChange : undefined}
options={options} options={options}
menuIsOpen={openMenu} menuIsOpen={openMenu}
inputValue={internalInputValue} inputValue={internalInputValue}
onInputChange={internalInputChangeHandler} onInputChange={internalInputChangeHandler}
onMenuClose={() => setInternalInputValue('')}
isMulti={isMulti} isMulti={isMulti}
isDisabled={isDisabled} isDisabled={isDisabled}
isLoading={isLoading} isLoading={isLoading}
@@ -163,17 +202,19 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
placeholder={placeholder} placeholder={placeholder}
className={cn('w-full', className?.select)} className={cn('w-full', className?.select)}
classNames={{ classNames={{
control: ({ isFocused, isDisabled }) => ...(!startAdornment && {
cn( control: ({ isFocused, isDisabled }) =>
'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!', cn(
{ 'w-full min-h-12! rounded border bg-white transition-shadow cursor-pointer!',
'border-red-500! ring-2 ring-red-200': isError, {
'border-indigo-500 ring-2 ring-indigo-200': isFocused, 'border-red-500! ring-2 ring-red-200': isError,
'border-gray-300': !isError && !isFocused, 'border-indigo-500 ring-2 ring-indigo-200': isFocused,
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled, 'border-gray-300': !isError && !isFocused,
} 'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
), }
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'), ),
valueContainer: () => cn('flex-1 px-4! py-2! gap-1'),
}),
placeholder: () => placeholder: () =>
cn({ 'text-gray-400': !isError, 'text-red-300!': isError }), cn({ 'text-gray-400': !isError, 'text-red-300!': isError }),
singleValue: () => singleValue: () =>
@@ -190,7 +231,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'), cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
menuList: () => cn('p-2! max-h-60 overflow-auto'), menuList: () => cn('p-2! max-h-60 overflow-auto'),
option: ({ isFocused, isSelected }) => option: ({ isFocused, isSelected }) =>
cn('mt-1 px-3 py-2 rounded cursor-pointer!', { cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', {
'bg-indigo-600 text-white': isFocused, 'bg-indigo-600 text-white': isFocused,
'bg-blue-500!': isSelected, 'bg-blue-500!': isSelected,
'text-gray-700': !isFocused && !isSelected, 'text-gray-700': !isFocused && !isSelected,
@@ -211,6 +252,10 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
...components, ...components,
...(optionComponent ? { Option: optionComponent } : {}), ...(optionComponent ? { Option: optionComponent } : {}),
}} }}
{...(startAdornment && {
shouldShowAdornment,
startAdornment,
})}
menuPortalTarget={ menuPortalTarget={
typeof document !== 'undefined' ? document.body : undefined typeof document !== 'undefined' ? document.body : undefined
} }
@@ -372,9 +372,9 @@ const ProjectFlockTable = () => {
(row) => row.original?.approval?.step_number == 1 (row) => row.original?.approval?.step_number == 1
); );
const allSelected = selectableRows.every((row) => const allSelected =
row.getIsSelected() selectableRows.every((row) => row.getIsSelected()) &&
) && selectableRows.length != 0; selectableRows.length != 0;
const someSelected = const someSelected =
selectableRows.some((row) => row.getIsSelected()) && selectableRows.some((row) => row.getIsSelected()) &&
@@ -495,7 +495,7 @@ const ProjectFlockTable = () => {
return ( return (
<> <>
{currentPageSize > 2 && ( {currentPageSize > 1 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}> <RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu <RowOptionsMenu
type='dropdown' type='dropdown'
@@ -505,10 +505,10 @@ const ProjectFlockTable = () => {
</RowDropdownOptions> </RowDropdownOptions>
)} )}
{currentPageSize <= 2 && ( {currentPageSize <= 1 && (
<RowCollapseOptions> <RowCollapseOptions>
<RowOptionsMenu <RowOptionsMenu
type='dropdown' type='collapse'
props={props} props={props}
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
/> />
+6
View File
@@ -40,6 +40,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
], ],
}, },
{
title: 'Penjualan',
link: '/sale',
icon: 'mdi:attach-money',
},
{ {
title: 'Persediaan', title: 'Persediaan',
link: '/inventory', link: '/inventory',