mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 07:15:44 +00:00
refactor(FE-92-87): mengganti select input dengan reuseable component
This commit is contained in:
@@ -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();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,10 +371,10 @@ const ProjectFlockTable = () => {
|
|||||||
const selectableRows = allRows.filter(
|
const selectableRows = allRows.filter(
|
||||||
(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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user