refactor(FE-238-106): change dateinput and create chickin page

This commit is contained in:
randy-ar
2025-11-03 10:09:12 +07:00
parent 495e11c6fe
commit 3eb2930640
8 changed files with 580 additions and 206 deletions
+44
View File
@@ -17,6 +17,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
@@ -195,6 +196,12 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
@@ -2872,6 +2879,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5732,6 +5755,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-day-picker": {
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+1
View File
@@ -19,6 +19,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,15 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export default function AddChickinKandang() {
const router = useRouter();
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
return (
<div>
<h1>Tambah Chickin untuk Kandang ID: {kandangId}</h1>
</div>
);
}
+215 -80
View File
@@ -2,10 +2,12 @@
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card';
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';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm'; import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
@@ -13,10 +15,12 @@ import { ProjectFlockApi } from '@/services/api/production';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { is } from 'react-day-picker/locale';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -113,21 +117,27 @@ const AddChickin = () => {
<> <>
<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 <div className='flex flex-row justify-between items-center'>
href='/production/project-flock' <Button
variant='link' href='/production/project-flock'
className='w-fit p-0 text-primary' variant='link'
> className='w-fit p-0 text-primary'
<Icon icon='uil:arrow-left' width={24} height={24} /> >
Kembali <Icon icon='uil:arrow-left' width={24} height={24} />
</Button> Kembali
</Button>
<h1 className='text-2xl font-semibold text-center'>
Daftar Kandang Project Flock
</h1>
<div></div>
</div>
<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
label='Project Flock' label='Ganti Project Flock'
placeholder='Pilih project flock' placeholder='Pilih Project Flock'
options={options} options={options}
onInputChange={(val) => { onInputChange={(val) => {
setSearchProjectFlock(val); setSearchProjectFlock(val);
@@ -164,78 +174,203 @@ const AddChickin = () => {
</div> </div>
</div> </div>
</header> </header>
<Table<Kandang> <Card
emptyContent={ title='Informasi Flock'
<div className='w-full p-5 text-center'>
{projectFlockId && isResponseError(projectFlock) ? (
<span className='text-lg opacity-50'>
{projectFlock.message}
</span>
) : (
<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={{ className={{
containerClassName: cn({ wrapper: 'w-full bg-white mb-3',
'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',
}} }}
/> >
<Table<ProjectFlock>
emptyContent={
<div className='w-full p-5 text-center'>
{projectFlockId && isResponseError(projectFlock) ? (
<span className='text-lg opacity-50'>
{projectFlock.message}
</span>
) : (
<span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
)}
</div>
}
data={isResponseSuccess(projectFlock) ? [projectFlock.data] : []}
columns={[
{
header: 'Area',
accessorKey: 'area.name',
},
{
header: 'Lokasi',
accessorKey: 'location.name',
},
{
header: 'Nama Flock',
accessorKey: 'flock.name',
},
{
header: 'Kategori',
accessorKey: 'category',
},
{
header: 'Status',
accessorKey: 'status',
cell: (props) => {
return props.row.original.approval.step_name ? (
<PillBadge
color={(() => {
switch (
props.row.original.approval.step_name.toUpperCase()
) {
case 'AKTIF':
return 'red';
case 'PENGAJUAN':
return 'green';
default:
return 'gray';
}
})()}
content={props.row.original.approval.step_name
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())}
/>
) : (
'-'
);
},
},
{
header: 'Periode',
accessorKey: 'period',
},
{
header: 'FCR Layer',
accessorKey: 'fcr.name',
},
]}
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',
}}
/>
</Card>
<Card
title='Daftar Chickin'
className={{
wrapper: 'w-full bg-white',
}}
>
<Table<Kandang>
emptyContent={
<div className='w-full p-5 text-center'>
{projectFlockId && isResponseError(projectFlock) ? (
<span className='text-lg opacity-50'>
{projectFlock.message}
</span>
) : (
<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,
},
{
accessorFn: () =>
isResponseSuccess(projectFlock)
? projectFlock.data.area.name
: '',
header: 'Area',
},
{
accessorFn: () =>
isResponseSuccess(projectFlock)
? projectFlock.data.location.name
: '',
header: 'Lokasi',
},
{
accessorKey: 'name',
header: 'Kandang',
},
{
accessorKey: 'capacity',
header: 'Kapasitas',
},
{
accessorKey: 'pic.name',
header: 'Penanggung Jawab',
},
{
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',
}}
/>
</Card>
</section> </section>
<Modal ref={chickinModal.ref}> <Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
+38 -23
View File
@@ -1,6 +1,13 @@
'use client'; 'use client';
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react'; import {
ReactNode,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
export const useModal = () => { export const useModal = () => {
@@ -8,31 +15,35 @@ export const useModal = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const openModal = useCallback(() => { const openModal = useCallback(() => {
if (!ref.current) return;
ref.current.showModal();
setOpen(true); setOpen(true);
ref.current?.showModal();
}, []); }, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
if (!ref.current) return;
ref.current.close();
setOpen(false); setOpen(false);
ref.current?.close();
}, []); }, []);
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (open) { open ? closeModal() : openModal();
closeModal();
} else {
openModal();
}
}, [open, closeModal, openModal]); }, [open, closeModal, openModal]);
if (ref.current) { // Gunakan useEffect agar event listener tidak didaftarkan berulang kali
ref.current.addEventListener('close', () => { useEffect(() => {
closeModal(); const dialog = ref.current;
}); if (!dialog) return;
}
return { ref, open, setOpen, openModal, closeModal, toggle } as const; const handleClose = () => setOpen(false);
dialog.addEventListener('close', handleClose);
return () => {
dialog.removeEventListener('close', handleClose);
};
}, []);
return { ref, open, openModal, closeModal, toggle } as const;
}; };
interface ModalProps { interface ModalProps {
@@ -46,15 +57,19 @@ interface ModalProps {
} }
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
return ( const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
<dialog ref={ref} className={cn('modal', className?.modal)}> if (closeOnBackdrop && e.target === ref.current) {
<div className={cn('modal-box', className?.modalBox)}>{children}</div> ref.current?.close();
}
};
{closeOnBackdrop && ( return (
<form method='dialog' className='modal-backdrop'> <dialog
<button>close</button> ref={ref}
</form> className={cn('modal', className?.modal)}
)} onClick={handleBackdropClick}
>
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
</dialog> </dialog>
); );
}; };
+217 -66
View File
@@ -4,29 +4,21 @@ import {
ChangeEventHandler, ChangeEventHandler,
FocusEventHandler, FocusEventHandler,
ReactNode, ReactNode,
useEffect,
useState, useState,
} from 'react'; } from 'react';
import { cn } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import Modal, { useModal } from '../Modal';
const formatToISO = (dateStr: string): string | null => { import { DateRange, DayPicker, Matcher } from 'react-day-picker';
const parts = dateStr.split('/'); import 'react-day-picker/dist/style.css';
if (parts.length !== 3) return null; import Button from '../Button';
const [day, month, year] = parts; import { Icon } from '@iconify/react';
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;
name: string; name: string;
value?: string; value?: string | { from?: string; to?: string };
placeholder?: string; placeholder?: string;
min?: string; min?: string;
max?: string; max?: string;
@@ -42,9 +34,8 @@ export interface DateInputProps {
readOnly?: boolean; readOnly?: boolean;
required?: boolean; required?: boolean;
isLoading?: boolean; isLoading?: boolean;
isRange?: boolean;
errorMessage?: string; errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>; onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>; onBlur?: FocusEventHandler<HTMLInputElement>;
} }
@@ -54,51 +45,139 @@ const DateInput = ({
bottomLabel, bottomLabel,
name, name,
value, value,
placeholder, placeholder = 'dd/mm/yyyy',
min, min,
max, max,
className, className,
isError: externalError, isError: externalError,
isValid: externalValid, isValid: externalValid,
errorMessage: externalErrorMessage, errorMessage: externalErrorMessage,
startAdornment,
endAdornment,
disabled = false, disabled = false,
required = false, required = false,
onChange, onChange,
onBlur, onBlur,
readOnly = false, readOnly = false,
isLoading = false, isLoading = false,
isRange = false,
}: DateInputProps) => { }: DateInputProps) => {
const [internalError, setInternalError] = useState<string | null>(null); const [internalError, setInternalError] = useState<string | null>(null);
const [selected, setSelected] = useState<Date | undefined>();
const [selectedRange, setSelectedRange] = useState<{
from?: Date;
to?: Date;
}>({});
const [displayValue, setDisplayValue] = useState<string>('');
const minISO = min ? formatToISO(min) ?? undefined : undefined; const minDate = min
const maxISO = max ? formatToISO(max) ?? undefined : undefined; ? new Date(min.split('/').reverse().join('-'))
: undefined;
const maxDate = max
? new Date(max.split('/').reverse().join('-'))
: undefined;
const valueISO = const calendarModal = useModal();
value && value.includes('/') ? formatToISO(value) ?? '' : value ?? '';
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => { // --- Sync value props ---
const selectedDate = e.target.value; useEffect(() => {
const isoMin = minISO; if (!value) return;
const isoMax = maxISO; if (isRange && typeof value === 'object') {
const from = value.from ? new Date(value.from) : undefined;
if (isoMin && selectedDate < isoMin) { const to = value.to ? new Date(value.to) : undefined;
setInternalError(`Tanggal tidak boleh sebelum ${min}`); setSelectedRange({ from, to });
} else if (isoMax && selectedDate > isoMax) { setDisplayValue(
setInternalError(`Tanggal tidak boleh setelah ${max}`); `${from ? formatDate(from, 'DD/MM/YYYY') : ''} ${
} else { to ? '- ' + formatDate(to, 'DD/MM/YYYY') : ''
setInternalError(null); }`
);
} else if (typeof value === 'string') {
const iso = value.includes('/')
? value.split('/').reverse().join('-')
: value;
const date = new Date(iso);
setSelected(date);
setDisplayValue(formatDate(iso, 'DD/MM/YYYY'));
} }
}, [value, isRange]);
const event = { const handleClick = (e: React.MouseEvent<HTMLInputElement>) => {
...e, e.preventDefault();
target: { if (!disabled && !readOnly) calendarModal.openModal();
...e.target, };
value: formatToLocal(selectedDate),
}, const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
}; onBlur?.(e);
onChange?.(event as React.ChangeEvent<HTMLInputElement>); };
const handleSelectSingle = (selectedDate?: Date) => {
if (!selectedDate) return;
if (minDate && selectedDate < minDate) {
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
return;
}
if (maxDate && selectedDate > maxDate) {
setInternalError(`Tanggal tidak boleh setelah ${max}`);
return;
}
setInternalError(null);
setSelected(selectedDate);
const formattedDisplay = formatDate(selectedDate, 'DD/MM/YYYY');
const formattedISO = formatDate(selectedDate, 'YYYY-MM-DD');
setDisplayValue(formattedDisplay);
const syntheticEvent = {
target: { name, value: formattedISO },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
calendarModal.closeModal();
};
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
if (!range) return;
setSelectedRange(range);
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
const toStr = range.to ? formatDate(range.to, 'DD/MM/YYYY') : '';
setDisplayValue(`${fromStr}${toStr ? ' - ' + toStr : ''}`);
// Jika kedua tanggal sudah terpilih
if (range.from && range.to) {
if (minDate && range.from < minDate) {
setInternalError(`Tanggal mulai tidak boleh sebelum ${min}`);
return;
}
if (maxDate && range.to > maxDate) {
setInternalError(`Tanggal akhir tidak boleh setelah ${max}`);
return;
}
setInternalError(null);
const syntheticEvent = {
target: {
name,
value: {
from: formatDate(range.from, 'YYYY-MM-DD'),
to: formatDate(range.to, 'YYYY-MM-DD'),
},
},
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
}
};
const handleResetDate = () => {
setSelected(undefined);
setSelectedRange({});
setDisplayValue('');
const syntheticEvent = {
target: { name, value: isRange ? { from: '', to: '' } : '' },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
calendarModal.closeModal();
};
const handleSaveDate = () => {
if (internalError) return;
calendarModal.closeModal();
}; };
const finalIsError = externalError || !!internalError; const finalIsError = externalError || !!internalError;
@@ -122,49 +201,53 @@ const DateInput = ({
> >
{label} {label}
{required && ( {required && (
<> <span className='text-error' title='required'>
{' '} *
<span className='tooltip tooltip-error' data-tip='required'> </span>
<span className='text-error'>*</span>
</span>
</>
)} )}
</label> </label>
)} )}
<div <div
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 transition-all duration-200 flex items-center border',
{ {
'border-error': finalIsError, 'border-error': finalIsError,
'border-success!': externalValid && !finalIsError, 'border-success': externalValid && !finalIsError,
}, },
className?.inputWrapper className?.inputWrapper
)} )}
> >
{startAdornment && startAdornment}
<input <input
type='date' type='text'
id={name} id={name}
name={name} name={name}
placeholder={placeholder} placeholder={isRange ? 'dd/mm/yyyy - dd/mm/yyyy' : placeholder}
value={valueISO} value={displayValue}
onChange={handleChange} onBlur={handleBlur}
onBlur={onBlur} onClick={handleClick}
min={minISO}
max={maxISO}
disabled={disabled} disabled={disabled}
className={cn('grow bg-transparent cursor-pointer', className?.input)} readOnly // ✅ tidak bisa diketik manual
readOnly={readOnly} className={cn(
'grow bg-transparent cursor-pointer focus:outline-none',
className?.input
)}
/> />
{(isLoading || endAdornment) && ( {isLoading && (
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />} <span className='loading loading-spinner' />
{endAdornment && endAdornment}
</div> </div>
)} )}
<Icon
icon='uil:calendar'
width={24}
height={24}
className='cursor-pointer text-dark'
onClick={(e) =>
handleClick(e as unknown as React.MouseEvent<HTMLInputElement>)
}
/>
</div> </div>
{!finalIsError && bottomLabel && ( {!finalIsError && bottomLabel && (
@@ -173,6 +256,74 @@ const DateInput = ({
{finalIsError && finalErrorMessage && ( {finalIsError && finalErrorMessage && (
<p className='w-full text-sm text-error'>{finalErrorMessage}</p> <p className='w-full text-sm text-error'>{finalErrorMessage}</p>
)} )}
<Modal
ref={calendarModal.ref}
className={{
modal: 'rounded',
modalBox: `w-fit min-h-${isRange ? '124' : '110'} flex flex-col`,
}}
closeOnBackdrop
>
{isRange ? (
<DayPicker
required={required}
mode='range'
captionLayout='dropdown-years'
navLayout='around'
reverseYears
defaultMonth={selectedRange.from ?? new Date()}
startMonth={minDate ?? new Date(1999, 1)}
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
selected={selectedRange as DateRange}
onSelect={handleSelectRange}
footer={<div className='text-center mt-3'>{displayValue}</div>}
disabled={
[
minDate ? { before: minDate } : undefined,
maxDate ? { after: maxDate } : undefined,
].filter(Boolean) as Matcher[]
}
/>
) : (
<DayPicker
required={required}
mode='single'
captionLayout='dropdown-years'
navLayout='around'
reverseYears
defaultMonth={selected ?? new Date()}
startMonth={minDate ?? new Date(1999, 1)}
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
selected={selected}
onSelect={handleSelectSingle}
disabled={
[
minDate ? { before: minDate } : undefined,
maxDate ? { after: maxDate } : undefined,
].filter(Boolean) as Matcher[]
}
/>
)}
<div className='mt-auto flex flex-col gap-2'>
{isRange && (
<small className='text-secondary'>
Tekan dua kali untuk memilih tanggal awal
</small>
)}
<div className='flex h-full justify-end items-end gap-2'>
<Button type='button' color='warning' onClick={handleResetDate}>
Reset
</Button>
{isRange && (
<Button type='button' onClick={handleSaveDate}>
Simpan
</Button>
)}
</div>
</div>
</Modal>
</div> </div>
); );
}; };
@@ -44,54 +44,56 @@ const RowOptionsMenu = ({
'dropdown-content': type === 'dropdown', 'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse', 'mt-2': type === 'collapse',
}, },
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow' 'p-2.5 mr-2 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)} )}
> >
<Button <div className='flex flex-col gap-1'>
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{props.row.original.approval.step_name === 'Aktif' && (
<Button <Button
href={`/production/chickin/add?projectFlockId=${props.row.original.id}`} href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost' variant='ghost'
color='success' color='primary'
className='justify-start text-sm' className='justify-start text-sm'
> >
<Icon icon='mdi:home-import-outline' width={16} height={16} /> <Icon icon='mdi:eye-outline' width={16} height={16} />
Chickin Detail
</Button> </Button>
)} {props.row.original.approval.step_name === 'Aktif' && (
{props.row.original.approval.step_name === 'Pengajuan' && ( <Button
href={`/production/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
</Button>
)}
{props.row.original.approval.step_name === 'Pengajuan' && (
<Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
)}
<Button <Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
color='warning' color='error'
className='justify-start text-sm' className='text-error hover:text-inherit'
> >
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon
Edit icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button> </Button>
)} </div>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</div> </div>
); );
}; };