mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu
This commit is contained in:
@@ -2,7 +2,7 @@ import InventoryAdjustmentTable from '@/components/pages/inventory/adjustment/In
|
|||||||
|
|
||||||
const InventoryAdjustment = () => {
|
const InventoryAdjustment = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full'>
|
||||||
<InventoryAdjustmentTable />
|
<InventoryAdjustmentTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab
|
|||||||
|
|
||||||
const Recording = () => {
|
const Recording = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full overflow-x-hidden'>
|
<section className='w-full'>
|
||||||
<RecordingTable />
|
<RecordingTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,15 +3,51 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { FormikValues } from 'formik';
|
import { FormikValues } from 'formik';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export type ButtonFilterProps = ButtonProps & {
|
export type ButtonFilterProps = ButtonProps & {
|
||||||
values: FormikValues;
|
values: FormikValues;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
excludeFields?: string[];
|
||||||
|
fieldGroups?: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
||||||
|
|
||||||
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
const ButtonFilter = ({
|
||||||
|
values,
|
||||||
|
onClick,
|
||||||
|
excludeFields = [],
|
||||||
|
fieldGroups = [],
|
||||||
|
...props
|
||||||
|
}: ButtonFilterProps) => {
|
||||||
|
const activeCount = useMemo(() => {
|
||||||
|
const filteredValues: FormikValues = {};
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
if (!excludeFields.includes(key)) {
|
||||||
|
filteredValues[key] = values[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let count = getFilledFormikValuesCount(filteredValues);
|
||||||
|
|
||||||
|
fieldGroups.forEach((group) => {
|
||||||
|
const groupFields = group.filter(
|
||||||
|
(field) => !excludeFields.includes(field)
|
||||||
|
);
|
||||||
|
const filledGroupFields = groupFields.filter(
|
||||||
|
(field) => filteredValues[field]
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
filledGroupFields.length === groupFields.length &&
|
||||||
|
groupFields.length > 1
|
||||||
|
) {
|
||||||
|
count -= groupFields.length - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}, [values, excludeFields, fieldGroups]);
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
@@ -21,7 +57,7 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
||||||
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
||||||
getFilledFormikValuesCount(values) > 0
|
activeCount > 0
|
||||||
? 'border-primary-gradient text-primary rounded-lg!'
|
? 'border-primary-gradient text-primary rounded-lg!'
|
||||||
: 'rounded-lg',
|
: 'rounded-lg',
|
||||||
props.className
|
props.className
|
||||||
@@ -31,14 +67,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
icon='heroicons:funnel'
|
icon='heroicons:funnel'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className={
|
className={activeCount > 0 ? 'text-blue-600' : ''}
|
||||||
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
Filter
|
Filter
|
||||||
{getFilledFormikValuesCount(values) > 0 && (
|
{activeCount > 0 && (
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||||
{getFilledFormikValuesCount(values)}
|
{activeCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -134,14 +134,20 @@ const DropFileInput: React.FC<DropFileInputProps> = ({
|
|||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p
|
<p
|
||||||
className={cn('w-full text-sm opacity-60', className?.bottomLabel)}
|
className={cn(
|
||||||
|
'w-full mt-1.5 text-xs opacity-60',
|
||||||
|
className?.bottomLabel
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{bottomLabel}
|
{bottomLabel}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isError && (
|
{isError && (
|
||||||
<p
|
<p
|
||||||
className={cn('w-full text-sm text-error', className?.errorMessage)}
|
className={cn(
|
||||||
|
'w-full mt-1.5 text-xs text-error',
|
||||||
|
className?.errorMessage
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ export const RadioGroup = ({
|
|||||||
|
|
||||||
{/* Label bawah */}
|
{/* Label bawah */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='text-sm opacity-60'>{bottomLabel}</p>
|
<p className='mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pesan error */}
|
{/* Pesan error */}
|
||||||
{isError && errorMessage && (
|
{isError && errorMessage && (
|
||||||
<p className='text-sm text-error'>{errorMessage}</p>
|
<p className='mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</RadioGroupContext.Provider>
|
</RadioGroupContext.Provider>
|
||||||
|
|||||||
@@ -488,9 +488,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
{isError && (
|
||||||
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,9 +159,11 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
|
|
||||||
{/* Bottom label or error message */}
|
{/* Bottom label or error message */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
ClosingFilterType,
|
ClosingFilterType,
|
||||||
} from '@/components/pages/closing/filter/ClosingFilter';
|
} from '@/components/pages/closing/filter/ClosingFilter';
|
||||||
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
|
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -158,6 +159,7 @@ const ClosingsTable = () => {
|
|||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('location_id', '');
|
updateFilter('location_id', '');
|
||||||
updateFilter('project_status', '');
|
updateFilter('project_status', '');
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -287,23 +289,6 @@ const ClosingsTable = () => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.project_status, projectStatusOptions]);
|
}, [formik.values.project_status, projectStatusOptions]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (tableFilterState.location_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.project_status) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [tableFilterState.location_id, tableFilterState.project_status]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== SEARCH CHANGE HANDLER =====
|
// ===== SEARCH CHANGE HANDLER =====
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
@@ -352,25 +337,12 @@ const ClosingsTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,41 +16,39 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import SelectInput, {
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
OptionType,
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import DateInput from '@/components/input/DateInput';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
}: {
|
}: {
|
||||||
type: 'dropdown' | 'collapse';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Expense, unknown>;
|
props: CellContext<Expense, unknown>;
|
||||||
approveClickHandler: () => void;
|
|
||||||
rejectClickHandler: () => void;
|
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const popoverId = `expense#${props.row.original.id}`;
|
||||||
|
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
const showEditButton = props.row.original.latest_approval
|
const showEditButton = props.row.original.latest_approval
|
||||||
? props.row.original.latest_approval.step_number !== 6 &&
|
? props.row.original.latest_approval.step_number !== 6 &&
|
||||||
(props.row.original.latest_approval.step_number === 1 ||
|
(props.row.original.latest_approval.step_number === 1 ||
|
||||||
@@ -59,81 +57,95 @@ const RowOptionsMenu = ({
|
|||||||
props.row.original.latest_approval.step_number === 4)
|
props.row.original.latest_approval.step_number === 4)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
// TODO: apply RBAC
|
|
||||||
const showRealizationButton = props.row.original.latest_approval
|
const showRealizationButton = props.row.original.latest_approval
|
||||||
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||||
props.row.original.latest_approval.step_number === 4
|
props.row.original.latest_approval.step_number === 4
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<div className='relative'>
|
||||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
<PopoverButton
|
||||||
<RequirePermission permissions='lti.expense.detail'>
|
tabIndex={0}
|
||||||
<Button
|
variant='ghost'
|
||||||
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
color='none'
|
||||||
variant='ghost'
|
popoverTarget={popoverId}
|
||||||
color='primary'
|
anchorName={popoverAnchorName}
|
||||||
className='justify-start text-sm'
|
>
|
||||||
>
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
</PopoverButton>
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
|
|
||||||
{showEditButton && (
|
<PopoverContent
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
id={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.expense.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='warning'
|
color='none'
|
||||||
className='justify-start text-sm'
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
icon='material-symbols:edit-outline'
|
Detail
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
|
||||||
|
|
||||||
{showRealizationButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.create.realization'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
|
<Button
|
||||||
|
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:pencil-outline' width={20} height={20} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRealizationButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
href={`/expense/realization/?expenseId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:money-bag-rounded'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-info'
|
||||||
|
/>
|
||||||
|
Realisasi
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
href={`/expense/realization/?expenseId=${props.row.original.id}`}
|
onClick={() => {
|
||||||
|
deleteClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='info'
|
color='error'
|
||||||
className='justify-start text-sm text-info focus-visible:text-info-content hover:text-info-content'
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
icon='material-symbols:money-bag-rounded'
|
Delete
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Realisasi
|
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
<RequirePermission permissions='lti.expense.delete'>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={deleteClickHandler}
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:delete-outline-rounded'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
/>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
</RowOptionsMenuWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,6 +191,9 @@ const ExpensesTable = () => {
|
|||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
const [selectedExpense, setSelectedExpense] = useState<Expense | undefined>(
|
const [selectedExpense, setSelectedExpense] = useState<Expense | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -340,31 +355,7 @@ const ExpensesTable = () => {
|
|||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
|
||||||
setSelectedExpense(props.row.original);
|
|
||||||
|
|
||||||
// Set row selection
|
|
||||||
setRowSelection({
|
|
||||||
[String(props.row.original.id)]: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
setApprovalNotes('');
|
|
||||||
approveModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const rejectClickHandler = () => {
|
|
||||||
setSelectedExpense(props.row.original);
|
|
||||||
|
|
||||||
// Set row selection
|
|
||||||
setRowSelection({
|
|
||||||
[String(props.row.original.id)]: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
setApprovalNotes('');
|
|
||||||
rejectModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteClickHandler = () => {
|
const deleteClickHandler = () => {
|
||||||
setSelectedExpense(props.row.original);
|
setSelectedExpense(props.row.original);
|
||||||
@@ -372,31 +363,11 @@ const ExpensesTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RowOptionsMenu
|
||||||
{currentPageSize > 3 && (
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
props={props}
|
||||||
<RowOptionsMenu
|
deleteClickHandler={deleteClickHandler}
|
||||||
type='dropdown'
|
/>
|
||||||
props={props}
|
|
||||||
approveClickHandler={approveClickHandler}
|
|
||||||
rejectClickHandler={rejectClickHandler}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
/>
|
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 3 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu
|
|
||||||
type='collapse'
|
|
||||||
props={props}
|
|
||||||
approveClickHandler={approveClickHandler}
|
|
||||||
rejectClickHandler={rejectClickHandler}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
/>
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -535,51 +506,32 @@ const ExpensesTable = () => {
|
|||||||
setIsRejectLoading(false);
|
setIsRejectLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setLocationInputValue,
|
|
||||||
options: locationOptions,
|
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedLocation(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'locationId',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
setInputValue: setVendorInputValue,
|
|
||||||
options: vendorOptions,
|
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
|
||||||
|
|
||||||
const [selectedVendor, setSelectedVendor] = useState<OptionType | null>(null);
|
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedVendor(val as OptionType);
|
|
||||||
updateFilter('vendorId', val ? ((val as OptionType).value as string) : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const transactionDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
// ===== FILTER MODAL HANDLERS =====
|
||||||
e
|
const handleFilterModalOpen = () => {
|
||||||
) => {
|
filterModal.openModal();
|
||||||
updateFilter('transactionDate', e.target.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const realizationDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (
|
const handleFilterSubmit = (values: {
|
||||||
e
|
transaction_date?: string | null;
|
||||||
) => {
|
realization_date?: string | null;
|
||||||
updateFilter('realizationDate', e.target.value);
|
location_id?: string | null;
|
||||||
|
vendor_id?: string | null;
|
||||||
|
}) => {
|
||||||
|
updateFilter('transactionDate', values.transaction_date || '');
|
||||||
|
updateFilter('realizationDate', values.realization_date || '');
|
||||||
|
updateFilter('locationId', values.location_id || '');
|
||||||
|
updateFilter('vendorId', values.vendor_id || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterReset = () => {
|
||||||
|
updateFilter('transactionDate', '');
|
||||||
|
updateFilter('realizationDate', '');
|
||||||
|
updateFilter('locationId', '');
|
||||||
|
updateFilter('vendorId', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
@@ -595,188 +547,176 @@ const ExpensesTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
{/* Action Buttons */}
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<RequirePermission permissions='lti.expense.create'>
|
||||||
<RequirePermission permissions='lti.expense.create'>
|
<Button
|
||||||
|
href='/expense/add'
|
||||||
|
color='primary'
|
||||||
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
|
Add Expense
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
{selectedRowIds.length > 0 && (
|
||||||
|
<>
|
||||||
|
<hr className='w-px h-full border-none bg-base-content/10 sm:block hidden' />
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense/add'
|
variant='outline'
|
||||||
color='primary'
|
color='none'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
onClick={bulkApproveClickHandler}
|
||||||
|
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
||||||
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
<Icon
|
||||||
Add Expense
|
icon='lucide-lab:farm'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Approve Head Area
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
<RequirePermission permissions='lti.expense.approve.unit_vice_president'>
|
||||||
<>
|
<Button
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
variant='outline'
|
||||||
<Button
|
color='none'
|
||||||
variant='outline'
|
onClick={bulkApproveClickHandler}
|
||||||
color='info'
|
disabled={
|
||||||
onClick={bulkApproveClickHandler}
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnHeadArea}
|
}
|
||||||
className='w-full sm:w-fit'
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
>
|
>
|
||||||
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
<Icon
|
||||||
Approve Head Area
|
icon='tdesign:money'
|
||||||
</Button>
|
width={20}
|
||||||
</RequirePermission>
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Approve Unit Vice President
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.approve.unit_vice_president'>
|
<RequirePermission permissions='lti.expense.approve.finance'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='none'
|
||||||
onClick={bulkApproveClickHandler}
|
onClick={bulkApproveClickHandler}
|
||||||
disabled={
|
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
}
|
>
|
||||||
className='w-full sm:w-fit'
|
<Icon
|
||||||
>
|
icon='tdesign:money'
|
||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
width={20}
|
||||||
Approve Unit Vice President
|
height={20}
|
||||||
</Button>
|
className='text-success'
|
||||||
</RequirePermission>
|
/>
|
||||||
|
Approve Finance
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.approve.finance'>
|
<RequirePermission
|
||||||
<Button
|
permissions={[
|
||||||
variant='outline'
|
'lti.expense.approve.head_area',
|
||||||
color='success'
|
'lti.expense.approve.unit_vice_president',
|
||||||
onClick={bulkApproveClickHandler}
|
'lti.expense.approve.finance',
|
||||||
disabled={!isAllSelectedRowLatestApprovalOnFinance}
|
]}
|
||||||
className='w-full sm:w-fit'
|
>
|
||||||
>
|
<Button
|
||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
variant='outline'
|
||||||
Approve Finance
|
color='none'
|
||||||
</Button>
|
onClick={bulkRejectClickHandler}
|
||||||
</RequirePermission>
|
disabled={
|
||||||
|
!isAllSelectedRowLatestApprovalOnHeadArea &&
|
||||||
|
!isAllSelectedRowLatestApprovalOnUnitVicePresident &&
|
||||||
|
!isAllSelectedRowLatestApprovalOnFinance
|
||||||
|
}
|
||||||
|
className='px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:close'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-error'
|
||||||
|
/>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<RequirePermission
|
{/* Search and Filter */}
|
||||||
permissions={[
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
'lti.expense.approve.head_area',
|
<DebouncedTextInput
|
||||||
'lti.expense.approve.unit_vice_president',
|
name='search'
|
||||||
'lti.expense.approve.finance',
|
placeholder='Search'
|
||||||
]}
|
value={tableFilterState.search ?? ''}
|
||||||
>
|
onChange={searchChangeHandler}
|
||||||
<Button
|
startAdornment={
|
||||||
variant='outline'
|
<Icon
|
||||||
color='error'
|
icon='heroicons:magnifying-glass'
|
||||||
onClick={bulkRejectClickHandler}
|
width={20}
|
||||||
disabled={
|
height={20}
|
||||||
!isAllSelectedRowLatestApprovalOnHeadArea &&
|
/>
|
||||||
!isAllSelectedRowLatestApprovalOnUnitVicePresident &&
|
}
|
||||||
!isAllSelectedRowLatestApprovalOnFinance
|
className={{
|
||||||
}
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
className='w-full sm:w-fit'
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
>
|
input:
|
||||||
<Icon
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
icon='material-symbols:close'
|
}}
|
||||||
width={24}
|
/>
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
Reject
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
<ButtonFilter
|
||||||
<DateInput
|
values={tableFilterState}
|
||||||
required
|
excludeFields={[
|
||||||
label='Tanggal Transaksi'
|
'page',
|
||||||
name='transaction_date'
|
'pageSize',
|
||||||
placeholder='Masukkan tanggal transaksi'
|
'search',
|
||||||
value={tableFilterState.transactionDate}
|
'nameSort',
|
||||||
onChange={transactionDateChangeHandler}
|
'userId',
|
||||||
className={{
|
]}
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
onClick={handleFilterModalOpen}
|
||||||
}}
|
className='px-3 py-2.5'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateInput
|
|
||||||
required
|
|
||||||
label='Tanggal Realisasi'
|
|
||||||
name='realization_date'
|
|
||||||
placeholder='Masukkan tanggal realisasi'
|
|
||||||
value={tableFilterState.realizationDate}
|
|
||||||
onChange={realizationDateChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Lokasi'
|
|
||||||
options={locationOptions}
|
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={locationChangeHandler}
|
|
||||||
onInputChange={setLocationInputValue}
|
|
||||||
isClearable
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
label='Vendor'
|
|
||||||
options={vendorOptions}
|
|
||||||
isLoading={isLoadingVendorOptions}
|
|
||||||
value={selectedVendor}
|
|
||||||
onChange={vendorChangeHandler}
|
|
||||||
onInputChange={setVendorInputValue}
|
|
||||||
isClearable
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Biaya Operasional'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table<Expense>
|
{/* Table Section */}
|
||||||
data={isResponseSuccess(expenses) ? expenses?.data : []}
|
<div className='flex flex-col mb-4'>
|
||||||
columns={expensesColumns}
|
<Table<Expense>
|
||||||
pageSize={tableFilterState.pageSize}
|
data={isResponseSuccess(expenses) ? expenses?.data : []}
|
||||||
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
|
columns={expensesColumns}
|
||||||
totalItems={
|
pageSize={tableFilterState.pageSize}
|
||||||
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0}
|
||||||
}
|
totalItems={
|
||||||
onPageChange={setPage}
|
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
|
||||||
isLoading={isLoading}
|
}
|
||||||
sorting={sorting}
|
onPageChange={setPage}
|
||||||
setSorting={setSorting}
|
onPageSizeChange={setPageSize}
|
||||||
rowSelection={rowSelection}
|
isLoading={isLoading}
|
||||||
setRowSelection={setRowSelection}
|
sorting={sorting}
|
||||||
enableRowSelection={tableEnableRowSelectionHandler}
|
setSorting={setSorting}
|
||||||
className={{
|
rowSelection={rowSelection}
|
||||||
containerClassName: cn({
|
setRowSelection={setRowSelection}
|
||||||
'mb-20':
|
enableRowSelection={tableEnableRowSelectionHandler}
|
||||||
isResponseSuccess(expenses) && expenses?.data?.length === 0,
|
className={{
|
||||||
}),
|
containerClassName: cn('p-3 mb-0', {
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
'w-full':
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
isResponseSuccess(expenses) && expenses?.data?.length === 0,
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
}),
|
||||||
headerColumnClassName:
|
headerColumnClassName: 'text-nowrap',
|
||||||
'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:
|
</div>
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
@@ -831,6 +771,12 @@ const ExpensesTable = () => {
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExpensesFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
onSubmit={handleFilterSubmit}
|
||||||
|
onReset={handleFilterReset}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export type ExpensesFilterType = {
|
||||||
|
transaction_date: string | null;
|
||||||
|
realization_date: string | null;
|
||||||
|
location_id: string | null;
|
||||||
|
vendor_id: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpensesFilterSchema = yup.object({
|
||||||
|
transaction_date: yup.string().nullable(),
|
||||||
|
realization_date: yup
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-greater-or-equal-transaction',
|
||||||
|
'Tanggal realisasi tidak boleh sebelum tanggal transaksi',
|
||||||
|
function (value) {
|
||||||
|
const { transaction_date } = this.parent;
|
||||||
|
if (!transaction_date || !value) return true;
|
||||||
|
return new Date(value) >= new Date(transaction_date);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
location_id: yup.string().nullable(),
|
||||||
|
vendor_id: yup.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import {
|
||||||
|
ExpensesFilterSchema,
|
||||||
|
ExpensesFilterValues,
|
||||||
|
} from '@/components/pages/expense/filter/ExpensesFilter';
|
||||||
|
|
||||||
|
interface ExpensesFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
initialValues?: ExpensesFilterValues;
|
||||||
|
onSubmit?: (values: Partial<ExpensesFilterValues>) => void;
|
||||||
|
onReset?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpensesFilterModal = ({
|
||||||
|
ref,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: ExpensesFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setVendorInputValue,
|
||||||
|
options: vendorOptions,
|
||||||
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
|
initialValues: initialValues || {
|
||||||
|
transaction_date: null,
|
||||||
|
realization_date: null,
|
||||||
|
location_id: null,
|
||||||
|
vendor_id: null,
|
||||||
|
},
|
||||||
|
validationSchema: ExpensesFilterSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
onSubmit?.(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationValue = formik.values.location_id
|
||||||
|
? locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const vendorValue = formik.values.vendor_id
|
||||||
|
? vendorOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.vendor_id
|
||||||
|
) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const locationId =
|
||||||
|
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const vendorId =
|
||||||
|
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
||||||
|
formik.setFieldValue('vendor_id', vendorId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={ref}
|
||||||
|
className={{
|
||||||
|
modalBox: 'p-0 rounded-xl xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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-base-content/10'>
|
||||||
|
<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'>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Transaksi'
|
||||||
|
name='transaction_date'
|
||||||
|
placeholder='Masukkan tanggal transaksi'
|
||||||
|
value={formik.values.transaction_date || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transaction_date &&
|
||||||
|
!!formik.errors.transaction_date
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Realisasi'
|
||||||
|
name='realization_date'
|
||||||
|
placeholder='Masukkan tanggal realisasi'
|
||||||
|
value={formik.values.realization_date || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.realization_date &&
|
||||||
|
!!formik.errors.realization_date
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{formik.touched.realization_date &&
|
||||||
|
formik.errors.realization_date && (
|
||||||
|
<span className='text-xs text-error'>
|
||||||
|
{formik.errors.realization_date}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationValue}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
options={vendorOptions}
|
||||||
|
value={vendorValue}
|
||||||
|
onChange={vendorChangeHandler}
|
||||||
|
onInputChange={setVendorInputValue}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesFilterModal;
|
||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
FinanceTableFilterSchema,
|
FinanceTableFilterSchema,
|
||||||
FinanceTableFilterValues,
|
FinanceTableFilterValues,
|
||||||
} from './FinanceTableFilter.schema';
|
} from './FinanceTableFilter.schema';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
@@ -234,6 +233,7 @@ const FinanceTable = () => {
|
|||||||
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [dateErrorShown, setDateErrorShown] = useState(false);
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
// ===== Formik for Filter =====
|
// ===== Formik for Filter =====
|
||||||
const filterFormik = useFormik<FinanceTableFilterValues>({
|
const filterFormik = useFormik<FinanceTableFilterValues>({
|
||||||
@@ -423,10 +423,7 @@ const FinanceTable = () => {
|
|||||||
const endDateObj = new Date(endDate);
|
const endDateObj = new Date(endDate);
|
||||||
|
|
||||||
if (endDateObj < startDate) {
|
if (endDateObj < startDate) {
|
||||||
filterFormik.setFieldError(
|
setHasDateError(true);
|
||||||
'end_date',
|
|
||||||
'Tanggal akhir tidak boleh masa lampau'
|
|
||||||
);
|
|
||||||
if (!dateErrorShown) {
|
if (!dateErrorShown) {
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
duration: Infinity,
|
duration: Infinity,
|
||||||
@@ -434,12 +431,14 @@ const FinanceTable = () => {
|
|||||||
setDateErrorShown(true);
|
setDateErrorShown(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
filterFormik.setFieldError('end_date', undefined);
|
setHasDateError(false);
|
||||||
if (dateErrorShown) {
|
if (dateErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDateErrorShown(false);
|
setDateErrorShown(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -454,10 +453,7 @@ const FinanceTable = () => {
|
|||||||
const endDate = new Date(value);
|
const endDate = new Date(value);
|
||||||
|
|
||||||
if (endDate < startDateObj) {
|
if (endDate < startDateObj) {
|
||||||
filterFormik.setFieldError(
|
setHasDateError(true);
|
||||||
'end_date',
|
|
||||||
'Tanggal akhir tidak boleh masa lampau'
|
|
||||||
);
|
|
||||||
if (!dateErrorShown) {
|
if (!dateErrorShown) {
|
||||||
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
duration: Infinity,
|
duration: Infinity,
|
||||||
@@ -468,7 +464,7 @@ const FinanceTable = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filterFormik.setFieldError('end_date', undefined);
|
setHasDateError(false);
|
||||||
if (dateErrorShown) {
|
if (dateErrorShown) {
|
||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDateErrorShown(false);
|
setDateErrorShown(false);
|
||||||
@@ -647,7 +643,7 @@ const FinanceTable = () => {
|
|||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:bank-transfer-in' width={20} height={20} />
|
<Icon icon='mdi:bank-transfer-in' width={20} height={20} />
|
||||||
Add Injection (Saldo Bank)
|
Injection Saldo Bank
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.finance.initial_balances.create'>
|
<RequirePermission permissions='lti.finance.initial_balances.create'>
|
||||||
@@ -657,7 +653,7 @@ const FinanceTable = () => {
|
|||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:cash-register' width={20} height={20} />
|
<Icon icon='mdi:cash-register' width={20} height={20} />
|
||||||
Add Initial Balance
|
Saldo Awal
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.finance.payments.create'>
|
<RequirePermission permissions='lti.finance.payments.create'>
|
||||||
@@ -667,7 +663,7 @@ const FinanceTable = () => {
|
|||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
Add Finance
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
@@ -765,10 +761,7 @@ const FinanceTable = () => {
|
|||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form onSubmit={filterFormik.handleSubmit} onReset={filterFormik.handleReset}>
|
||||||
onSubmit={filterFormik.handleSubmit}
|
|
||||||
onReset={filterFormik.handleReset}
|
|
||||||
>
|
|
||||||
<div className='p-4 flex flex-col gap-1.5'>
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
|
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
|
||||||
@@ -823,7 +816,7 @@ const FinanceTable = () => {
|
|||||||
isMulti
|
isMulti
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
<SelectInputRadio
|
<SelectInput
|
||||||
options={sortByOptions}
|
options={sortByOptions}
|
||||||
label='Urutkan Berdasarkan'
|
label='Urutkan Berdasarkan'
|
||||||
placeholder='Pilih Urutan'
|
placeholder='Pilih Urutan'
|
||||||
@@ -836,22 +829,23 @@ const FinanceTable = () => {
|
|||||||
name='start_date'
|
name='start_date'
|
||||||
label='Periode Tanggal (Mulai)'
|
label='Periode Tanggal (Mulai)'
|
||||||
value={filterFormik.values.start_date}
|
value={filterFormik.values.start_date}
|
||||||
|
errorMessage={filterFormik.errors.start_date}
|
||||||
onChange={startDateChangeHandler}
|
onChange={startDateChangeHandler}
|
||||||
errorMessage={
|
isError={
|
||||||
filterFormik.errors.end_date
|
filterFormik.touched.start_date &&
|
||||||
? filterFormik.errors.end_date
|
Boolean(filterFormik.errors.start_date)
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
label='Periode Tanggal (Akhir)'
|
label='Periode Tanggal (Akhir)'
|
||||||
value={filterFormik.values.end_date}
|
value={filterFormik.values.end_date}
|
||||||
|
errorMessage={filterFormik.errors.end_date}
|
||||||
onChange={endDateChangeHandler}
|
onChange={endDateChangeHandler}
|
||||||
errorMessage={
|
isError={
|
||||||
filterFormik.errors.end_date
|
(filterFormik.touched.end_date &&
|
||||||
? filterFormik.errors.end_date
|
Boolean(filterFormik.errors.end_date)) ||
|
||||||
: undefined
|
hasDateError
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Badge from '@/components/Badge';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper';
|
||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||||
import { Icon } from '@iconify/react';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const InventoryAdjustmentTable = () => {
|
const InventoryAdjustmentTable = () => {
|
||||||
const {
|
const {
|
||||||
@@ -41,80 +39,106 @@ const InventoryAdjustmentTable = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Data
|
|
||||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
const { data: inventoryAdjustments, isLoading } = useSWR(
|
||||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||||
InventoryAdjustmentApi.getAllFetcher
|
InventoryAdjustmentApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// State
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
// Columns
|
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = [
|
() => [
|
||||||
{
|
{
|
||||||
header: '#',
|
id: 'adj_number',
|
||||||
cell: (props) =>
|
header: 'No. Referensi',
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
accessorFn: (row) => row.adj_number ?? '-',
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'product_name',
|
|
||||||
header: 'Nama Produk',
|
|
||||||
accessorFn: (row) => row.product_warehouse?.product?.name ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'warehouse_name',
|
|
||||||
header: 'Gudang',
|
|
||||||
accessorFn: (row) => row.product_warehouse?.warehouse?.name ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'created_at',
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorFn: (row) =>
|
|
||||||
new Date(row.created_at).toLocaleDateString('id-ID', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'quantity',
|
|
||||||
header: 'Kuantitas',
|
|
||||||
accessorFn: (row) => formatNumber(String(row.increase + row.decrease)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'transaction_type',
|
|
||||||
header: 'Tipe Transaksi',
|
|
||||||
accessorFn: (row) => {
|
|
||||||
if (row.increase > 0) return 'Peningkatan';
|
|
||||||
if (row.decrease > 0) return 'Penurunan';
|
|
||||||
return '-';
|
|
||||||
},
|
},
|
||||||
cell: (props) => {
|
{
|
||||||
const type = props.row.original.increase;
|
id: 'location',
|
||||||
const label = type > 0 ? 'Peningkatan' : type <= 0 ? 'Penurunan' : '-';
|
header: 'Lokasi',
|
||||||
|
accessorFn: (row) => row.location?.name ?? '-',
|
||||||
return (
|
|
||||||
<Badge variant='soft' color={type > 0 ? 'success' : 'error'}>
|
|
||||||
{label}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'project_flock',
|
||||||
id: 'created_by',
|
header: 'Flock',
|
||||||
header: 'Oleh',
|
accessorFn: (row) => row.project_flock?.flock_name ?? '-',
|
||||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
},
|
||||||
},
|
{
|
||||||
];
|
id: 'warehouse_name',
|
||||||
|
header: 'Gudang',
|
||||||
|
accessorFn: (row) => row.product_warehouse?.warehouse?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'product_name',
|
||||||
|
header: 'Nama Produk',
|
||||||
|
accessorFn: (row) => row.product_warehouse?.product?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quantity',
|
||||||
|
header: 'Kuantitas',
|
||||||
|
accessorFn: (row) => row.qty ?? '-',
|
||||||
|
cell: (row) => {
|
||||||
|
const value = row.row.original.increase + row.row.original.decrease;
|
||||||
|
return <div className='text-center'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price',
|
||||||
|
header: 'Harga',
|
||||||
|
accessorFn: (row) => (row.price ? formatCurrency(row.price) : '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grand_total',
|
||||||
|
header: 'Grand Total',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
row.grand_total ? formatCurrency(row.grand_total) : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transaction_type',
|
||||||
|
header: 'Tipe Transaksi',
|
||||||
|
accessorFn: (row) => row.transaction_subtype ?? '-',
|
||||||
|
cell: (row) => {
|
||||||
|
const subtype = row.row.original.transaction_subtype;
|
||||||
|
const increase = row.row.original.increase;
|
||||||
|
|
||||||
// Handler
|
const subtypeLabelMap: Record<string, string> = {
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
PURCHASE_IN: 'Pembelian',
|
||||||
const newVal = val as OptionType;
|
MARKETING_OUT: 'Penjualan',
|
||||||
setPageSize(newVal.value as number);
|
RECORDING_STOCK_OUT: 'Recording Stock',
|
||||||
};
|
RECORDING_DEPLETION_OUT: 'Recording Depletion',
|
||||||
|
RECORDING_EGG_IN: 'Recording Egg',
|
||||||
|
ADJUSTMENT_OUT: 'Penyesuaian',
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = subtypeLabelMap[subtype] || subtype || '-';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
increase > 0 ? 'success' : increase <= 0 ? 'error' : 'neutral'
|
||||||
|
}
|
||||||
|
text={label}
|
||||||
|
className={{
|
||||||
|
badge: 'whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'created_at',
|
||||||
|
header: 'Tanggal',
|
||||||
|
accessorFn: (row) =>
|
||||||
|
row.created_at ? formatDate(row.created_at, 'DD MMM YYYY') : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'created_by',
|
||||||
|
header: 'Oleh',
|
||||||
|
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[tableFilterState.pageSize, tableFilterState.page]
|
||||||
|
);
|
||||||
|
|
||||||
const updateSortingFilter = useCallback(
|
const updateSortingFilter = useCallback(
|
||||||
(
|
(
|
||||||
@@ -130,7 +154,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
[updateFilter]
|
[updateFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Effect
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const productCategorySortFilter = sorting.find(
|
const productCategorySortFilter = sorting.find(
|
||||||
(sortItem) => sortItem.id === 'productCategory'
|
(sortItem) => sortItem.id === 'productCategory'
|
||||||
@@ -149,89 +172,60 @@ const InventoryAdjustmentTable = () => {
|
|||||||
updateSortingFilter('stockSort', stockSortFilter);
|
updateSortingFilter('stockSort', stockSortFilter);
|
||||||
}, [sorting, updateSortingFilter]);
|
}, [sorting, updateSortingFilter]);
|
||||||
|
|
||||||
// Utils Function
|
|
||||||
const formatNumber = (value: string) => {
|
|
||||||
const numericValue = value.replace(/[^0-9.]/g, '');
|
|
||||||
const [integer, decimal] = numericValue.split('.');
|
|
||||||
const formattedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
||||||
return decimal ? `${formattedInteger}.${decimal}` : formattedInteger;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full'>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
{/* Header Section */}
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-full flex flex-row'>
|
<RequirePermission permissions='lti.inventory.create'>
|
||||||
<RequirePermission permissions='lti.inventory.create'>
|
<Button
|
||||||
<Button
|
href='/inventory/adjustment/add'
|
||||||
href='/inventory/adjustment/add'
|
color='primary'
|
||||||
variant='outline'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
color='primary'
|
>
|
||||||
className='w-full sm:w-fit'
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
>
|
Add Adjustment
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
</Button>
|
||||||
Tambah
|
</RequirePermission>
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-row justify-end'>
|
|
||||||
<SelectInput
|
|
||||||
label='Baris'
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
value={{
|
|
||||||
label: String(tableFilterState.pageSize),
|
|
||||||
value: tableFilterState.pageSize,
|
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{ wrapper: 'min-w-28' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table<InventoryAdjustment>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(inventoryAdjustments)
|
|
||||||
? inventoryAdjustments?.data
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={inventoryAdjustmentsColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={
|
|
||||||
isResponseSuccess(inventoryAdjustments)
|
|
||||||
? inventoryAdjustments?.meta?.page
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(inventoryAdjustments)
|
|
||||||
? inventoryAdjustments?.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20':
|
|
||||||
isResponseSuccess(inventoryAdjustments) &&
|
|
||||||
inventoryAdjustments?.data?.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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<div className='flex flex-col mb-4'>
|
||||||
|
<Table<InventoryAdjustment>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(inventoryAdjustments)
|
||||||
|
? inventoryAdjustments?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={inventoryAdjustmentsColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(inventoryAdjustments)
|
||||||
|
? inventoryAdjustments?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(inventoryAdjustments)
|
||||||
|
? inventoryAdjustments?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn('p-3 mb-0', {
|
||||||
|
'w-full':
|
||||||
|
isResponseSuccess(inventoryAdjustments) &&
|
||||||
|
inventoryAdjustments?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
headerColumnClassName: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,106 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
|
|
||||||
export const InventoryAdjustmentFormSchema = Yup.object({
|
export type InventoryAdjustmentFormSchemaType = {
|
||||||
product_category: Yup.mixed<OptionType>()
|
location: {
|
||||||
.nullable()
|
value: number;
|
||||||
.test(
|
label: string;
|
||||||
'is-valid-option',
|
} | null;
|
||||||
'Kategori Produk wajib diisi!',
|
location_id: number;
|
||||||
(value) => value !== null && value !== undefined
|
project_flock: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
project_flock_id: number;
|
||||||
|
kandang: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
kandang_id: number;
|
||||||
|
project_flock_kandang: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
product: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
product_id: number;
|
||||||
|
transaction_type: string;
|
||||||
|
transaction_subtype: string;
|
||||||
|
qty: number | string;
|
||||||
|
price: number | string;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InventoryAdjustmentFormSchema: Yup.ObjectSchema<InventoryAdjustmentFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
|
.typeError('Lokasi wajib diisi!'),
|
||||||
|
project_flock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.number()
|
||||||
|
.min(1, 'Project flock wajib diisi!')
|
||||||
|
.required('Project flock wajib diisi!')
|
||||||
|
.typeError('Project flock wajib diisi!'),
|
||||||
|
kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.number()
|
||||||
|
.min(1, 'Kandang wajib diisi!')
|
||||||
|
.required('Kandang wajib diisi!')
|
||||||
|
.typeError('Kandang wajib diisi!'),
|
||||||
|
project_flock_kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_kandang_id: Yup.number()
|
||||||
|
.default(0)
|
||||||
|
.typeError('Project Flock Kandang wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'is-valid-project-flock-kandang',
|
||||||
|
'Project Flock Kandang wajib diisi!',
|
||||||
|
(value) => value !== undefined && value !== null && value > 0
|
||||||
|
)
|
||||||
|
.required('Project Flock Kandang wajib diisi!'),
|
||||||
|
product: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
product_id: Yup.number()
|
||||||
|
.min(1, 'Produk wajib diisi!')
|
||||||
|
.required('Produk wajib diisi!')
|
||||||
|
.typeError('Produk wajib diisi!'),
|
||||||
|
transaction_type: Yup.string()
|
||||||
|
.min(1, 'Tipe transaksi wajib diisi!')
|
||||||
|
.oneOf(
|
||||||
|
['PEMBELIAN', 'PENJUALAN', 'RECORDING'],
|
||||||
|
'Tipe transaksi tidak valid'
|
||||||
|
)
|
||||||
|
.required('Tipe transaksi wajib diisi')
|
||||||
|
.typeError('Tipe transaksi wajib diisi!'),
|
||||||
|
transaction_subtype: Yup.string().required(
|
||||||
|
'Sub tipe transaksi wajib diisi'
|
||||||
),
|
),
|
||||||
|
qty: Yup.number()
|
||||||
product_category_id: Yup.number().nullable(),
|
.typeError('Kuantitas harus berupa angka')
|
||||||
|
.min(1, 'Minimal kuantitas adalah 1')
|
||||||
product: Yup.mixed<OptionType>()
|
.required('Kuantitas wajib diisi'),
|
||||||
.nullable()
|
price: Yup.number()
|
||||||
.test(
|
.typeError('Harga harus berupa angka')
|
||||||
'is-valid-option',
|
.min(0, 'Minimal harga adalah 0')
|
||||||
'Produk wajib diisi!',
|
.required('Harga wajib diisi'),
|
||||||
(value) => value !== null && value !== undefined
|
notes: Yup.string().required('Catatan wajib diisi!'),
|
||||||
),
|
});
|
||||||
|
|
||||||
product_id: Yup.number()
|
|
||||||
.nullable()
|
|
||||||
.required('Produk wajib diisi!')
|
|
||||||
.min(1, 'Produk wajib diisi!'),
|
|
||||||
|
|
||||||
warehouse: Yup.mixed<OptionType>()
|
|
||||||
.nullable()
|
|
||||||
.test(
|
|
||||||
'is-valid-option',
|
|
||||||
'Warehouse wajib diisi!',
|
|
||||||
(value) => value !== null && value !== undefined
|
|
||||||
),
|
|
||||||
|
|
||||||
warehouse_id: Yup.number()
|
|
||||||
.nullable()
|
|
||||||
.required('Warehouse wajib diisi!')
|
|
||||||
.min(1, 'Warehouse wajib diisi!'),
|
|
||||||
|
|
||||||
transaction_type: Yup.string()
|
|
||||||
.oneOf(['increase', 'decrease'], 'Tipe transaksi tidak valid')
|
|
||||||
.nullable()
|
|
||||||
.required('Tipe transaksi wajib diisi'),
|
|
||||||
|
|
||||||
quantity: Yup.number()
|
|
||||||
.typeError('Kuantitas harus berupa angka')
|
|
||||||
.min(1, 'Minimal kuantitas adalah 1')
|
|
||||||
.required('Kuantitas wajib diisi'),
|
|
||||||
|
|
||||||
note: Yup.string().required('Catatan wajib diisi!'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type InventoryAdjustmentFormValues = Yup.InferType<
|
export type InventoryAdjustmentFormValues = Yup.InferType<
|
||||||
typeof InventoryAdjustmentFormSchema
|
typeof InventoryAdjustmentFormSchema
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -11,37 +11,62 @@ import { MovementApi } from '@/services/api/inventory';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput from '@/components/input/SelectInput';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
}: {
|
}: {
|
||||||
type: 'dropdown' | 'collapse';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Movement, unknown>;
|
props: CellContext<Movement, unknown>;
|
||||||
}) => (
|
}) => {
|
||||||
<RowOptionsMenuWrapper type={type}>
|
const popoverId = `movement#${props.row.original.id}`;
|
||||||
<RequirePermission permissions='lti.inventory.transfer.detail'>
|
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||||
<Button
|
|
||||||
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<PopoverButton
|
||||||
|
tabIndex={0}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='primary'
|
color='none'
|
||||||
className='justify-start text-sm'
|
popoverTarget={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
Detail
|
</PopoverButton>
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
<PopoverContent
|
||||||
</RowOptionsMenuWrapper>
|
id={popoverId}
|
||||||
);
|
anchorName={popoverAnchorName}
|
||||||
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.inventory.transfer.detail'>
|
||||||
|
<Button
|
||||||
|
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
const {
|
const {
|
||||||
@@ -71,121 +96,108 @@ const MovementTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const movementColumns: ColumnDef<Movement>[] = useMemo(
|
||||||
const newVal = val as OptionType;
|
() => [
|
||||||
setPageSize(newVal.value as number);
|
{
|
||||||
setPage(1);
|
header: 'No',
|
||||||
};
|
cell: (props) =>
|
||||||
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
const movementColumns: ColumnDef<Movement>[] = [
|
props.row.index +
|
||||||
{
|
1,
|
||||||
header: '#',
|
|
||||||
cell: (props) =>
|
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.source_warehouse?.name,
|
|
||||||
header: 'Gudang Asal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.destination_warehouse?.name,
|
|
||||||
header: 'Gudang Tujuan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transfer_reason',
|
|
||||||
header: 'Catatan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transfer_date',
|
|
||||||
header: 'Tanggal',
|
|
||||||
cell: (props) =>
|
|
||||||
new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => {
|
|
||||||
const totalCost = row.deliveries?.reduce(
|
|
||||||
(sum, d) => sum + (d.shipping_cost_total || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
return totalCost?.toLocaleString('id-ID');
|
|
||||||
},
|
},
|
||||||
header: 'Biaya Pengiriman',
|
{
|
||||||
},
|
accessorFn: (row) => row.source_warehouse?.name,
|
||||||
{
|
header: 'Gudang Asal',
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props) => {
|
|
||||||
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
|
||||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
|
||||||
const currentRowRelativeIndex =
|
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{currentPageSize > 2 && (
|
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
|
||||||
<RowOptionsMenu type='dropdown' props={props} />
|
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu type='collapse' props={props} />
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
];
|
accessorFn: (row) => row.destination_warehouse?.name,
|
||||||
|
header: 'Gudang Tujuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transfer_reason',
|
||||||
|
header: 'Catatan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transfer_date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
cell: (props) =>
|
||||||
|
new Date(props.row.original.transfer_date).toLocaleDateString(
|
||||||
|
'id-ID'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => {
|
||||||
|
const totalCost = row.deliveries?.reduce(
|
||||||
|
(sum, d) => sum + (d.shipping_cost_total || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return totalCost?.toLocaleString('id-ID');
|
||||||
|
},
|
||||||
|
header: 'Biaya Pengiriman',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props: CellContext<Movement, unknown>) => {
|
||||||
|
const currentPageSize =
|
||||||
|
props.table.getPaginationRowModel().rows.length;
|
||||||
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
|
const currentRowRelativeIndex =
|
||||||
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowOptionsMenu
|
||||||
|
props={props}
|
||||||
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[tableFilterState.pageSize, tableFilterState.page]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full'>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
{/* Header Section */}
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
{/* Action Buttons */}
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<RequirePermission permissions='lti.inventory.transfer.create'>
|
<RequirePermission permissions='lti.inventory.transfer.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/inventory/movement/add'
|
href='/inventory/movement/add'
|
||||||
color='primary'
|
color='primary'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
Add Movement
|
Add Movement
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Movement'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-end gap-4'>
|
|
||||||
<SelectInput
|
|
||||||
label='Baris'
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
value={{
|
|
||||||
label: String(tableFilterState.pageSize),
|
|
||||||
value: tableFilterState.pageSize,
|
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper:
|
|
||||||
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Movement'
|
||||||
|
value={tableFilterState.search ?? ''}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
startAdornment={
|
||||||
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
|
input:
|
||||||
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<div className='flex flex-col mb-4'>
|
||||||
<Table<Movement>
|
<Table<Movement>
|
||||||
data={isResponseSuccess(movements) ? movements?.data : []}
|
data={isResponseSuccess(movements) ? movements?.data : []}
|
||||||
columns={movementColumns}
|
columns={movementColumns}
|
||||||
@@ -195,26 +207,20 @@ const MovementTable = () => {
|
|||||||
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
|
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={setSorting}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn('p-3 mb-0', {
|
||||||
'mb-20':
|
'w-full':
|
||||||
isResponseSuccess(movements) && movements?.data?.length === 0,
|
isResponseSuccess(movements) && movements?.data?.length === 0,
|
||||||
}),
|
}),
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
headerColumnClassName: 'text-nowrap',
|
||||||
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',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,8 @@
|
|||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
import { InventoryProductApi } from '@/services/api/inventory';
|
import { InventoryProductApi } from '@/services/api/inventory';
|
||||||
@@ -18,28 +13,59 @@ import { Icon } from '@iconify/react';
|
|||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
}: {
|
}: {
|
||||||
type: 'dropdown' | 'collapse';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<InventoryProduct, unknown>;
|
props: CellContext<InventoryProduct, unknown>;
|
||||||
}) => (
|
}) => {
|
||||||
<RowOptionsMenuWrapper type={type}>
|
const popoverId = `product#${props.row.original.id}`;
|
||||||
<RequirePermission permissions='lti.inventory.product_stock.detail'>
|
const popoverAnchorName = `--anchor-product#${props.row.original.id}`;
|
||||||
<Button
|
|
||||||
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<PopoverButton
|
||||||
|
tabIndex={0}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='primary'
|
color='none'
|
||||||
className='justify-start text-sm'
|
popoverTarget={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
Detail
|
</PopoverButton>
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
<PopoverContent
|
||||||
</RowOptionsMenuWrapper>
|
id={popoverId}
|
||||||
);
|
anchorName={popoverAnchorName}
|
||||||
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.inventory.product_stock.detail'>
|
||||||
|
<Button
|
||||||
|
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const InventoryProductTable = () => {
|
const InventoryProductTable = () => {
|
||||||
const {
|
const {
|
||||||
@@ -69,16 +95,10 @@ const InventoryProductTable = () => {
|
|||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const newVal = val as OptionType;
|
|
||||||
setPageSize(newVal.value as number);
|
|
||||||
setPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
const columns: ColumnDef<InventoryProduct>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
props.row.index +
|
props.row.index +
|
||||||
@@ -125,7 +145,7 @@ const InventoryProductTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props: CellContext<InventoryProduct, unknown>) => {
|
||||||
const currentPageSize =
|
const currentPageSize =
|
||||||
props.table.getPaginationRowModel().rows.length;
|
props.table.getPaginationRowModel().rows.length;
|
||||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
@@ -135,58 +155,57 @@ const InventoryProductTable = () => {
|
|||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RowOptionsMenu
|
||||||
{currentPageSize > 2 && (
|
props={props}
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
<RowOptionsMenu type='dropdown' props={props} />
|
/>
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu type='collapse' props={props} />
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[tableFilterState.pageSize, tableFilterState.page]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='w-full'>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
{/* Header Section */}
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
{/* Action Buttons */}
|
||||||
<div className='w-full flex flex-row gap-2'></div>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
</div>
|
<RequirePermission permissions='lti.inventory.product_stock.create'>
|
||||||
|
<Button
|
||||||
<div className='flex justify-between items-end gap-4'>
|
href='/inventory/product/add'
|
||||||
<DebouncedTextInput
|
color='primary'
|
||||||
name='search'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||||
placeholder='Cari Produk'
|
>
|
||||||
value={tableFilterState.search}
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
onChange={searchChangeHandler}
|
Add Product
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
</Button>
|
||||||
/>
|
</RequirePermission>
|
||||||
<SelectInput
|
|
||||||
label='Baris'
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
value={{
|
|
||||||
label: String(tableFilterState.pageSize),
|
|
||||||
value: tableFilterState.pageSize,
|
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper:
|
|
||||||
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Produk'
|
||||||
|
value={tableFilterState.search ?? ''}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
startAdornment={
|
||||||
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
|
input:
|
||||||
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<div className='flex flex-col mb-4'>
|
||||||
<Table<InventoryProduct>
|
<Table<InventoryProduct>
|
||||||
data={
|
data={
|
||||||
isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : []
|
isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : []
|
||||||
@@ -204,27 +223,21 @@ const InventoryProductTable = () => {
|
|||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
setSorting={setSorting}
|
setSorting={setSorting}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
containerClassName: cn('p-3 mb-0', {
|
||||||
'mb-20':
|
'w-full':
|
||||||
isResponseSuccess(inventoryProducts) &&
|
isResponseSuccess(inventoryProducts) &&
|
||||||
inventoryProducts?.data?.length === 0,
|
inventoryProducts?.data?.length === 0,
|
||||||
}),
|
}),
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
headerColumnClassName: 'text-nowrap',
|
||||||
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',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import PopoverButton from '@/components/popover/PopoverButton';
|
|||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter';
|
import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
const RowsOptionsMenu = ({
|
const RowsOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -214,32 +215,6 @@ const MarketingTable = () => {
|
|||||||
updateFilter('customer_id', '');
|
updateFilter('customer_id', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
// Product filter
|
|
||||||
if (tableFilterState.product_ids) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status filter
|
|
||||||
if (tableFilterState.status) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Customer filter
|
|
||||||
if (tableFilterState.customer_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
tableFilterState.product_ids,
|
|
||||||
tableFilterState.status,
|
|
||||||
tableFilterState.customer_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
setApproveAction('APPROVED');
|
setApproveAction('APPROVED');
|
||||||
confirmationModal.openModal();
|
confirmationModal.openModal();
|
||||||
@@ -588,28 +563,14 @@ const MarketingTable = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary':
|
|
||||||
activeFiltersCount > 0,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{activeFiltersCount > 0 && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
direction='bottom'
|
direction='bottom'
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} from './filter/ProjectFlockFilter';
|
} from './filter/ProjectFlockFilter';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -346,25 +347,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.period, periodOptions]);
|
}, [formik.values.period, periodOptions]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
if (tableFilterState.area_id) count += 1;
|
|
||||||
if (tableFilterState.location_id) count += 1;
|
|
||||||
if (tableFilterState.kandang_id) count += 1;
|
|
||||||
if (tableFilterState.category) count += 1;
|
|
||||||
if (tableFilterState.period) count += 1;
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
tableFilterState.area_id,
|
|
||||||
tableFilterState.location_id,
|
|
||||||
tableFilterState.kandang_id,
|
|
||||||
tableFilterState.category,
|
|
||||||
tableFilterState.period,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== FILTER DEPENDENCY HANDLERS =====
|
// ===== FILTER DEPENDENCY HANDLERS =====
|
||||||
const handleFilterAreaChange = (area: OptionType | null) => {
|
const handleFilterAreaChange = (area: OptionType | null) => {
|
||||||
const areaId = area?.value ? String(area.value) : undefined;
|
const areaId = area?.value ? String(area.value) : undefined;
|
||||||
@@ -961,25 +943,12 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import StatusBadge from '@/components/helper/StatusBadge';
|
|||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
@@ -511,36 +512,6 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
}, [formik.values.kandang_id, kandangOptions]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (tableFilterState.areaFilter) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.locationFilter) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.kandangFilter) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.projectFlockKandangFilter) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
tableFilterState.areaFilter,
|
|
||||||
tableFilterState.locationFilter,
|
|
||||||
tableFilterState.kandangFilter,
|
|
||||||
tableFilterState.projectFlockKandangFilter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
@@ -1264,30 +1235,17 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
<div className='flex flex-col mb-4 -mx-4 px-4'>
|
<div className='flex flex-col mb-4'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
<span className='loading loading-spinner loading-xl' />
|
<span className='loading loading-spinner loading-xl' />
|
||||||
@@ -1438,7 +1396,11 @@ const RecordingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={!formik.isValid || formik.isSubmitting}
|
disabled={
|
||||||
|
!formik.isValid ||
|
||||||
|
formik.isSubmitting ||
|
||||||
|
!formik.values.kandang_id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+118
-22
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject } from 'react';
|
import { RefObject, useState, useEffect } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -13,6 +14,10 @@ import { OptionType, useSelect } from '@/components/input/SelectInput';
|
|||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Flock } from '@/types/api/master-data/flock';
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
import { TransferToLayingFilter } from '@/types/api/production/transfer-to-laying';
|
||||||
|
import {
|
||||||
|
TransferToLayingFilterSchema,
|
||||||
|
TransferToLayingFilterValues,
|
||||||
|
} from '@/components/pages/production/transfer-to-laying/filter/TransferToLayingFilter';
|
||||||
|
|
||||||
interface TransferToLayingFilterModal {
|
interface TransferToLayingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
@@ -29,6 +34,36 @@ const TransferToLayingFilterModal = ({
|
|||||||
ref.current?.close();
|
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]);
|
||||||
|
|
||||||
// Flock Source
|
// Flock Source
|
||||||
const {
|
const {
|
||||||
setInputValue: setFlockSourceInputValue,
|
setInputValue: setFlockSourceInputValue,
|
||||||
@@ -49,13 +84,7 @@ const TransferToLayingFilterModal = ({
|
|||||||
category: 'LAYING',
|
category: 'LAYING',
|
||||||
});
|
});
|
||||||
|
|
||||||
const formik = useFormik<{
|
const formik = useFormik<TransferToLayingFilterValues>({
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
flockSource: { value: number; label: string }[];
|
|
||||||
flockDestination: { value: number; label: string }[];
|
|
||||||
status: { value: number; label: string }[];
|
|
||||||
}>({
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
@@ -63,15 +92,22 @@ const TransferToLayingFilterModal = ({
|
|||||||
flockDestination: [],
|
flockDestination: [],
|
||||||
status: [],
|
status: [],
|
||||||
},
|
},
|
||||||
|
validationSchema: TransferToLayingFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
const formattedValues = {
|
||||||
...values,
|
...values,
|
||||||
flockSource: values.flockSource.map((item) => item.value),
|
flockSource: values.flockSource
|
||||||
flockDestination: values.flockDestination.map((item) => item.value),
|
? (values.flockSource as OptionType[]).map((item) => item.value)
|
||||||
status: values.status.map((item) => item.value),
|
: [],
|
||||||
|
flockDestination: values.flockDestination
|
||||||
|
? (values.flockDestination as OptionType[]).map((item) => item.value)
|
||||||
|
: [],
|
||||||
|
status: values.status
|
||||||
|
? (values.status as OptionType[]).map((item) => item.value)
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues as TransferToLayingFilter);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
@@ -81,17 +117,17 @@ const TransferToLayingFilterModal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('flockSource', val as OptionType[]);
|
formik.setFieldValue('flockSource', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flockDestinationChangeHandler = (
|
const flockDestinationChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
formik.setFieldValue('flockDestination', val as OptionType[]);
|
formik.setFieldValue('flockDestination', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const statusChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('status', val as OptionType[]);
|
formik.setFieldValue('status', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -132,24 +168,84 @@ const TransferToLayingFilterModal = ({
|
|||||||
<DateInput
|
<DateInput
|
||||||
name='startDate'
|
name='startDate'
|
||||||
placeholder='Tanggal Awal'
|
placeholder='Tanggal Awal'
|
||||||
value={formik.values.startDate}
|
value={formik.values.startDate || ''}
|
||||||
onChange={formik.handleChange}
|
errorMessage={formik.errors.startDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('startDate', value);
|
||||||
|
|
||||||
|
if (value && formik.values.endDate) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(formik.values.endDate);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.startDate && Boolean(formik.errors.startDate)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
<DateInput
|
<DateInput
|
||||||
name='endDate'
|
name='endDate'
|
||||||
placeholder='Tanggal Akhir'
|
placeholder='Tanggal Akhir'
|
||||||
value={formik.values.endDate}
|
value={formik.values.endDate || ''}
|
||||||
onChange={formik.handleChange}
|
errorMessage={formik.errors.endDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('endDate', value);
|
||||||
|
|
||||||
|
if (value && formik.values.startDate) {
|
||||||
|
const startDateObj = new Date(formik.values.startDate);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
(formik.touched.endDate && Boolean(formik.errors.endDate)) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectInputCheckbox
|
<SelectInputCheckbox
|
||||||
label='Flock Asal'
|
label='Flock Asal'
|
||||||
placeholder='Flock Asal'
|
placeholder='Flock Asal'
|
||||||
value={formik.values.flockSource}
|
value={formik.values.flockSource as OptionType[]}
|
||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
options={flockSourceOptions}
|
options={flockSourceOptions}
|
||||||
isLoading={isLoadingFlockSourceOptions}
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
@@ -160,7 +256,7 @@ const TransferToLayingFilterModal = ({
|
|||||||
<SelectInputCheckbox
|
<SelectInputCheckbox
|
||||||
label='Flock Tujuan'
|
label='Flock Tujuan'
|
||||||
placeholder='Flock Tujuan'
|
placeholder='Flock Tujuan'
|
||||||
value={formik.values.flockDestination}
|
value={formik.values.flockDestination as OptionType[]}
|
||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
options={flockDestinationOptions}
|
options={flockDestinationOptions}
|
||||||
isLoading={isLoadingFlockDestinationOptions}
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
@@ -176,7 +272,7 @@ const TransferToLayingFilterModal = ({
|
|||||||
{ value: 'APPROVED', label: 'Disetujui' },
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
{ value: 'REJECTED', label: 'Ditolak' },
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
]}
|
]}
|
||||||
value={formik.values.status}
|
value={formik.values.status as OptionType[]}
|
||||||
onChange={statusChangeHandler}
|
onChange={statusChangeHandler}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
CellContext,
|
CellContext,
|
||||||
@@ -17,7 +17,6 @@ import { useModal } from '@/components/Modal';
|
|||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import Badge from '@/components/Badge';
|
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
@@ -34,6 +33,7 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
@@ -159,30 +159,6 @@ const TransferToLayingsTable = () => {
|
|||||||
TransferToLayingApi.getAllFetcher
|
TransferToLayingApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (tableFilterState.startDate && tableFilterState.endDate) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.flockSource.length > 0) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.flockDestination.length > 0) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tableFilterState.status.length > 0) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [tableFilterState]);
|
|
||||||
|
|
||||||
const isFilterActive = filterCount > 0;
|
|
||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -559,30 +535,19 @@ const TransferToLayingsTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={[
|
||||||
|
'page',
|
||||||
|
'pageSize',
|
||||||
|
'search',
|
||||||
|
'filter_by',
|
||||||
|
'sort_by',
|
||||||
|
]}
|
||||||
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': isFilterActive,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{isFilterActive && (
|
|
||||||
<Badge
|
|
||||||
className={{
|
|
||||||
badge:
|
|
||||||
'p-1.5 bg-[#FF3535] text-xs text-base-100 border border-base-300 rounded-lg',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export type TransferToLayingFilterType = {
|
||||||
|
startDate: string | null;
|
||||||
|
endDate: string | null;
|
||||||
|
flockSource: number[];
|
||||||
|
flockDestination: number[];
|
||||||
|
status: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TransferToLayingFilterSchema = yup.object({
|
||||||
|
startDate: yup.string().optional().nullable(),
|
||||||
|
endDate: yup
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-greater-than-start',
|
||||||
|
'Tanggal akhir tidak boleh masa lampau',
|
||||||
|
function (value) {
|
||||||
|
const { startDate } = this.parent;
|
||||||
|
if (!startDate || !value) return true;
|
||||||
|
return new Date(value) >= new Date(startDate);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
flockSource: yup.array().optional().nullable(),
|
||||||
|
flockDestination: yup.array().optional().nullable(),
|
||||||
|
status: yup.array().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TransferToLayingFilterValues = yup.InferType<
|
||||||
|
typeof TransferToLayingFilterSchema
|
||||||
|
>;
|
||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
import { generateUniformityPDF } from '@/components/pages/production/uniformity/export/UniformityExportPDF';
|
import { generateUniformityPDF } from '@/components/pages/production/uniformity/export/UniformityExportPDF';
|
||||||
import { generateUniformityExcel } from '@/components/pages/production/uniformity/export/UniformityExportExcel';
|
import { generateUniformityExcel } from '@/components/pages/production/uniformity/export/UniformityExportExcel';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import {
|
import {
|
||||||
UniformityTableFilterSchema,
|
UniformityTableFilterSchema,
|
||||||
@@ -192,16 +193,28 @@ const UniformityTable = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
setPage,
|
setPage,
|
||||||
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
location_id: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
kandang_id: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
search: 'search',
|
search: 'search',
|
||||||
|
start_date: 'start_date',
|
||||||
|
end_date: 'end_date',
|
||||||
|
location_id: 'location_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
kandang_id: 'kandang_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,12 +246,14 @@ const UniformityTable = () => {
|
|||||||
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
const [filterKandang, setFilterKandang] = useState<OptionType | null>(null);
|
||||||
const [filterProjectFlockKandangId, setFilterProjectFlockKandangId] =
|
const [filterProjectFlockKandangId, setFilterProjectFlockKandangId] =
|
||||||
useState<number | undefined>(undefined);
|
useState<number | undefined>(undefined);
|
||||||
const [filterStartDate, setFilterStartDate] = useState('');
|
|
||||||
const [filterEndDate, setFilterEndDate] = useState('');
|
|
||||||
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
||||||
useState<string>('');
|
useState<string>('');
|
||||||
const [, setFilterErrors] = useState<Record<string, string>>({});
|
const [, setFilterErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setFilterLocationInputValue,
|
setInputValue: setFilterLocationInputValue,
|
||||||
options: filterLocationOptions,
|
options: filterLocationOptions,
|
||||||
@@ -319,8 +334,8 @@ const UniformityTable = () => {
|
|||||||
// ===== FORMIK FILTER =====
|
// ===== FORMIK FILTER =====
|
||||||
const filterFormik = useFormik<UniformityTableFilterValues>({
|
const filterFormik = useFormik<UniformityTableFilterValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
start_date: filterStartDate,
|
start_date: tableFilterState.start_date,
|
||||||
end_date: filterEndDate,
|
end_date: tableFilterState.end_date,
|
||||||
location: filterLocation,
|
location: filterLocation,
|
||||||
project_flock: filterProjectFlock,
|
project_flock: filterProjectFlock,
|
||||||
project_flock_kandang_id: filterProjectFlockKandangId,
|
project_flock_kandang_id: filterProjectFlockKandangId,
|
||||||
@@ -329,8 +344,21 @@ const UniformityTable = () => {
|
|||||||
validationSchema: UniformityTableFilterSchema,
|
validationSchema: UniformityTableFilterSchema,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setFilterStartDate(values.start_date);
|
updateFilter('start_date', values.start_date);
|
||||||
setFilterEndDate(values.end_date);
|
updateFilter('end_date', values.end_date);
|
||||||
|
updateFilter(
|
||||||
|
'location_id',
|
||||||
|
values.location?.value ? String(values.location.value) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_id',
|
||||||
|
values.project_flock?.value ? String(values.project_flock.value) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'kandang_id',
|
||||||
|
values.kandang?.value ? String(values.kandang.value) : ''
|
||||||
|
);
|
||||||
|
|
||||||
setFilterLocation(values.location ?? null);
|
setFilterLocation(values.location ?? null);
|
||||||
setFilterProjectFlock(values.project_flock ?? null);
|
setFilterProjectFlock(values.project_flock ?? null);
|
||||||
setFilterKandang(values.kandang ?? null);
|
setFilterKandang(values.kandang ?? null);
|
||||||
@@ -356,11 +384,11 @@ const UniformityTable = () => {
|
|||||||
filterProjectFlockKandangId.toString()
|
filterProjectFlockKandangId.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filterStartDate) {
|
if (tableFilterState.start_date) {
|
||||||
queryParams.append('start_date', filterStartDate);
|
queryParams.append('start_date', tableFilterState.start_date);
|
||||||
}
|
}
|
||||||
if (filterEndDate) {
|
if (tableFilterState.end_date) {
|
||||||
queryParams.append('end_date', filterEndDate);
|
queryParams.append('end_date', tableFilterState.end_date);
|
||||||
}
|
}
|
||||||
queryParams.append('with_chart', 'true');
|
queryParams.append('with_chart', 'true');
|
||||||
}
|
}
|
||||||
@@ -379,8 +407,8 @@ const UniformityTable = () => {
|
|||||||
}, [
|
}, [
|
||||||
isSubmitted,
|
isSubmitted,
|
||||||
filterProjectFlockKandangId,
|
filterProjectFlockKandangId,
|
||||||
filterStartDate,
|
tableFilterState.start_date,
|
||||||
filterEndDate,
|
tableFilterState.end_date,
|
||||||
getTableFilterQueryString,
|
getTableFilterQueryString,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -456,30 +484,17 @@ const UniformityTable = () => {
|
|||||||
setFilterProjectFlock(null);
|
setFilterProjectFlock(null);
|
||||||
setFilterKandang(null);
|
setFilterKandang(null);
|
||||||
setFilterProjectFlockKandangId(undefined);
|
setFilterProjectFlockKandangId(undefined);
|
||||||
setFilterStartDate('');
|
|
||||||
setFilterEndDate('');
|
|
||||||
setFilterErrors({});
|
setFilterErrors({});
|
||||||
|
|
||||||
|
updateFilter('start_date', '');
|
||||||
|
updateFilter('end_date', '');
|
||||||
|
updateFilter('location_id', '');
|
||||||
|
updateFilter('project_flock_id', '');
|
||||||
|
updateFilter('kandang_id', '');
|
||||||
|
|
||||||
filterFormik.resetForm();
|
filterFormik.resetForm();
|
||||||
}, [filterFormik]);
|
filterModal.closeModal();
|
||||||
|
}, [filterFormik, updateFilter]);
|
||||||
const handleFilterStartDateChange = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setFilterStartDate(value);
|
|
||||||
filterFormik.setFieldValue('start_date', value);
|
|
||||||
},
|
|
||||||
[filterFormik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterEndDateChange = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setFilterEndDate(value);
|
|
||||||
filterFormik.setFieldValue('end_date', value);
|
|
||||||
},
|
|
||||||
[filterFormik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedRowIds = useMemo(() => {
|
const selectedRowIds = useMemo(() => {
|
||||||
return Object.keys(rowSelection)
|
return Object.keys(rowSelection)
|
||||||
@@ -662,11 +677,11 @@ const UniformityTable = () => {
|
|||||||
filterProjectFlockKandangId.toString()
|
filterProjectFlockKandangId.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filterStartDate) {
|
if (tableFilterState.start_date) {
|
||||||
queryParams.append('start_date', filterStartDate);
|
queryParams.append('start_date', tableFilterState.start_date);
|
||||||
}
|
}
|
||||||
if (filterEndDate) {
|
if (tableFilterState.end_date) {
|
||||||
queryParams.append('end_date', filterEndDate);
|
queryParams.append('end_date', tableFilterState.end_date);
|
||||||
}
|
}
|
||||||
queryParams.append('limit', '100');
|
queryParams.append('limit', '100');
|
||||||
queryParams.append('page', '1');
|
queryParams.append('page', '1');
|
||||||
@@ -677,7 +692,11 @@ const UniformityTable = () => {
|
|||||||
const response = await UniformityApi.getAllFetcher(url);
|
const response = await UniformityApi.getAllFetcher(url);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
}, [filterProjectFlockKandangId, filterStartDate, filterEndDate]);
|
}, [
|
||||||
|
filterProjectFlockKandangId,
|
||||||
|
tableFilterState.start_date,
|
||||||
|
tableFilterState.end_date,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
@@ -698,8 +717,8 @@ const UniformityTable = () => {
|
|||||||
location_name: locationName,
|
location_name: locationName,
|
||||||
project_flock_name: projectFlockName,
|
project_flock_name: projectFlockName,
|
||||||
kandang_name: kandangName,
|
kandang_name: kandangName,
|
||||||
start_date: filterStartDate,
|
start_date: tableFilterState.start_date,
|
||||||
end_date: filterEndDate,
|
end_date: tableFilterState.end_date,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
@@ -713,8 +732,8 @@ const UniformityTable = () => {
|
|||||||
filterLocation,
|
filterLocation,
|
||||||
filterProjectFlock,
|
filterProjectFlock,
|
||||||
filterKandang,
|
filterKandang,
|
||||||
filterStartDate,
|
tableFilterState.start_date,
|
||||||
filterEndDate,
|
tableFilterState.end_date,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
@@ -736,8 +755,8 @@ const UniformityTable = () => {
|
|||||||
location_name: locationName,
|
location_name: locationName,
|
||||||
project_flock_name: projectFlockName,
|
project_flock_name: projectFlockName,
|
||||||
kandang_name: kandangName,
|
kandang_name: kandangName,
|
||||||
start_date: filterStartDate,
|
start_date: tableFilterState.start_date,
|
||||||
end_date: filterEndDate,
|
end_date: tableFilterState.end_date,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
@@ -751,8 +770,8 @@ const UniformityTable = () => {
|
|||||||
filterLocation,
|
filterLocation,
|
||||||
filterProjectFlock,
|
filterProjectFlock,
|
||||||
filterKandang,
|
filterKandang,
|
||||||
filterStartDate,
|
tableFilterState.start_date,
|
||||||
filterEndDate,
|
tableFilterState.end_date,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -778,6 +797,23 @@ const UniformityTable = () => {
|
|||||||
}
|
}
|
||||||
}, [uniformities, rowSelection]);
|
}, [uniformities, rowSelection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [filterModal.open, dateErrorShown]);
|
||||||
|
|
||||||
// ===== TABLE COLUMNS DEFINITION =====
|
// ===== TABLE COLUMNS DEFINITION =====
|
||||||
const uniformityColumns: ColumnDef<Uniformity>[] = useMemo(
|
const uniformityColumns: ColumnDef<Uniformity>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -883,37 +919,6 @@ const UniformityTable = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== CALCULATE FILTER COUNT =====
|
|
||||||
const filterCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (filterStartDate && filterEndDate) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterLocation) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterProjectFlock) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterKandang) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [
|
|
||||||
filterStartDate,
|
|
||||||
filterEndDate,
|
|
||||||
filterLocation,
|
|
||||||
filterProjectFlock,
|
|
||||||
filterKandang,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isFilterActive = filterCount > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='@container w-full'>
|
<div className='@container w-full'>
|
||||||
@@ -932,30 +937,13 @@ const UniformityTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={tableFilterState}
|
||||||
color='none'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
onClick={filterModal.openModal}
|
onClick={filterModal.openModal}
|
||||||
className={cn(
|
className='px-3 py-2.5'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
/>
|
||||||
{
|
|
||||||
'border-primary-gradient text-primary': isFilterActive,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{isFilterActive && (
|
|
||||||
<Badge
|
|
||||||
className={{
|
|
||||||
badge:
|
|
||||||
'p-1.5 bg-[#FF3535] text-xs text-base-100 border border-base-300 rounded-lg',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -1279,7 +1267,38 @@ const UniformityTable = () => {
|
|||||||
placeholder='Tanggal Mulai'
|
placeholder='Tanggal Mulai'
|
||||||
value={filterFormik.values.start_date}
|
value={filterFormik.values.start_date}
|
||||||
errorMessage={filterFormik.errors.start_date}
|
errorMessage={filterFormik.errors.start_date}
|
||||||
onChange={handleFilterStartDateChange}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
filterFormik.setFieldValue('start_date', value);
|
||||||
|
|
||||||
|
if (value && filterFormik.values.end_date) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(
|
||||||
|
filterFormik.values.end_date
|
||||||
|
);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error(
|
||||||
|
'Tanggal akhir tidak boleh masa lampau',
|
||||||
|
{
|
||||||
|
duration: Infinity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
isError={
|
isError={
|
||||||
filterFormik.touched.start_date &&
|
filterFormik.touched.start_date &&
|
||||||
Boolean(filterFormik.errors.start_date)
|
Boolean(filterFormik.errors.start_date)
|
||||||
@@ -1291,11 +1310,38 @@ const UniformityTable = () => {
|
|||||||
placeholder='Tanggal Akhir'
|
placeholder='Tanggal Akhir'
|
||||||
value={filterFormik.values.end_date}
|
value={filterFormik.values.end_date}
|
||||||
errorMessage={filterFormik.errors.end_date}
|
errorMessage={filterFormik.errors.end_date}
|
||||||
onChange={handleFilterEndDateChange}
|
onChange={(e) => {
|
||||||
isError={
|
const value = e.target.value;
|
||||||
filterFormik.touched.end_date &&
|
filterFormik.setFieldValue('end_date', value);
|
||||||
Boolean(filterFormik.errors.end_date)
|
|
||||||
}
|
if (value && filterFormik.values.start_date) {
|
||||||
|
const startDateObj = new Date(
|
||||||
|
filterFormik.values.start_date
|
||||||
|
);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error(
|
||||||
|
'Tanggal akhir tidak boleh masa lampau',
|
||||||
|
{
|
||||||
|
duration: Infinity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isError={hasDateError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
@@ -69,59 +68,72 @@ const getStatusBadgeColor = (status: string): Color => {
|
|||||||
return statusBadgeColorMap[status] || 'neutral';
|
return statusBadgeColorMap[status] || 'neutral';
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== INTERFACES =====
|
// ===== ROW OPTIONS MENU =====
|
||||||
interface RowOptionsMenuProps {
|
|
||||||
type: 'dropdown' | 'collapse';
|
|
||||||
props: CellContext<Purchase, unknown>;
|
|
||||||
deleteClickHandler: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
}: RowOptionsMenuProps) => {
|
}: {
|
||||||
|
popoverPosition: 'bottom' | 'top';
|
||||||
|
props: CellContext<Purchase, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
const popoverId = `purchase#${props.row.original.id}`;
|
||||||
|
const popoverAnchorName = `--anchor-purchase#${props.row.original.id}`;
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<div className='relative'>
|
||||||
<RequirePermission permissions='lti.purchase.detail'>
|
<PopoverButton
|
||||||
<Button
|
tabIndex={0}
|
||||||
href={`/purchase/detail/?purchaseId=${props.row.original.id}`}
|
variant='ghost'
|
||||||
variant='ghost'
|
color='none'
|
||||||
color='primary'
|
popoverTarget={popoverId}
|
||||||
className='justify-start text-sm'
|
anchorName={popoverAnchorName}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
Detail
|
</PopoverButton>
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
|
|
||||||
{/*<Button*/}
|
<PopoverContent
|
||||||
{/* href={`/purchase/detail/edit/?purchaseId=${props.row.original.id}`}*/}
|
id={popoverId}
|
||||||
{/* variant='ghost'*/}
|
anchorName={popoverAnchorName}
|
||||||
{/* color='warning'*/}
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
{/* className='justify-start text-sm'*/}
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
{/*>*/}
|
>
|
||||||
{/* <Icon icon='material-symbols:edit-outline' width={16} height={16} />*/}
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
{/* Edit*/}
|
<RequirePermission permissions='lti.purchase.detail'>
|
||||||
{/*</Button>*/}
|
<Button
|
||||||
|
href={`/purchase/detail/?purchaseId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
onClick={closePopover}
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<RequirePermission permissions='lti.purchase.delete'>
|
<RequirePermission permissions='lti.purchase.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={() => {
|
||||||
variant='ghost'
|
deleteClickHandler();
|
||||||
color='error'
|
closePopover();
|
||||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
}}
|
||||||
>
|
variant='ghost'
|
||||||
<Icon
|
color='error'
|
||||||
icon='material-symbols:delete-outline-rounded'
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
width={16}
|
>
|
||||||
height={16}
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
className='justify-start text-sm'
|
Delete
|
||||||
/>
|
</Button>
|
||||||
Delete
|
</RequirePermission>
|
||||||
</Button>
|
</div>
|
||||||
</RequirePermission>
|
</PopoverContent>
|
||||||
</RowOptionsMenuWrapper>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -346,27 +358,11 @@ const PurchaseTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RowOptionsMenu
|
||||||
{currentPageSize > 2 && (
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
props={props}
|
||||||
<RowOptionsMenu
|
deleteClickHandler={deleteClickHandler}
|
||||||
type='dropdown'
|
/>
|
||||||
props={props}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
/>
|
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu
|
|
||||||
type='collapse'
|
|
||||||
props={props}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
/>
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -405,22 +401,22 @@ const PurchaseTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<RequirePermission permissions='lti.purchase.create'>
|
||||||
<RequirePermission permissions='lti.purchase.create'>
|
<Button
|
||||||
<Button
|
href='/purchase/add'
|
||||||
href='/purchase/add'
|
color='primary'
|
||||||
color='primary'
|
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-xl shadow-button-soft'
|
||||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
>
|
||||||
>
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
Add Purchase
|
||||||
Add Purchase
|
</Button>
|
||||||
</Button>
|
</RequirePermission>
|
||||||
</RequirePermission>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Search'
|
placeholder='Search'
|
||||||
@@ -441,59 +437,41 @@ const PurchaseTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-wrap justify-end gap-4'>
|
|
||||||
<SelectInput
|
|
||||||
label='Baris'
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
value={{
|
|
||||||
label: String(tableFilterState.pageSize),
|
|
||||||
value: tableFilterState.pageSize,
|
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full sm:w-24',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table<Purchase>
|
{/* Table Section */}
|
||||||
data={
|
<div className='flex flex-col mb-4'>
|
||||||
isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : []
|
<Table<Purchase>
|
||||||
}
|
data={
|
||||||
columns={purchaseColumns}
|
isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : []
|
||||||
pageSize={tableFilterState.pageSize}
|
}
|
||||||
page={
|
columns={purchaseColumns}
|
||||||
isResponseSuccess(purchaseRequests)
|
pageSize={tableFilterState.pageSize}
|
||||||
? purchaseRequests?.meta?.page
|
page={
|
||||||
: 0
|
isResponseSuccess(purchaseRequests)
|
||||||
}
|
? purchaseRequests?.meta?.page
|
||||||
totalItems={
|
: 0
|
||||||
isResponseSuccess(purchaseRequests)
|
}
|
||||||
? purchaseRequests?.meta?.total_results
|
totalItems={
|
||||||
: 0
|
isResponseSuccess(purchaseRequests)
|
||||||
}
|
? purchaseRequests?.meta?.total_results
|
||||||
onPageChange={setPage}
|
: 0
|
||||||
isLoading={isLoading}
|
}
|
||||||
sorting={sorting}
|
onPageChange={setPage}
|
||||||
setSorting={setSorting}
|
onPageSizeChange={setPageSize}
|
||||||
className={{
|
isLoading={isLoading}
|
||||||
containerClassName: cn({
|
sorting={sorting}
|
||||||
'mb-20':
|
setSorting={setSorting}
|
||||||
isResponseSuccess(purchaseRequests) &&
|
className={{
|
||||||
purchaseRequests?.data?.length === 0,
|
containerClassName: cn('p-3', {
|
||||||
}),
|
'w-full mb-20':
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
isResponseSuccess(purchaseRequests) &&
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
purchaseRequests?.data?.length === 0,
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
}),
|
||||||
headerColumnClassName:
|
headerColumnClassName: 'text-nowrap',
|
||||||
'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:
|
</div>
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== MODAL COMPONENTS ===== */}
|
{/* ===== MODAL COMPONENTS ===== */}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
||||||
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||||
import { ReportExpenseApi } from '@/services/api/report';
|
import { ReportExpenseApi } from '@/services/api/report';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -38,6 +38,7 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
interface ReportExpenseTabProps {
|
interface ReportExpenseTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -144,6 +145,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
setFilterParams({});
|
setFilterParams({});
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,20 +171,6 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
[formik.values.category]
|
[formik.values.category]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
if (filterParams.location_id) count += 1;
|
|
||||||
if (filterParams.supplier_id) count += 1;
|
|
||||||
if (filterParams.kandang_id) count += 1;
|
|
||||||
if (filterParams.nonstock_id) count += 1;
|
|
||||||
if (filterParams.realization_date) count += 1;
|
|
||||||
if (filterParams.category) count += 1;
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
@@ -312,25 +300,12 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
setTabActions(
|
setTabActions(
|
||||||
tabId,
|
tabId,
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={formik.values}
|
||||||
color='none'
|
|
||||||
onClick={() => filterModal.openModal()}
|
onClick={() => filterModal.openModal()}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -387,8 +362,7 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
hasFilters,
|
formik.values,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportPDF,
|
handleExportPDF,
|
||||||
|
|||||||
@@ -11,7 +11,19 @@ export type DebtSupplierFilterType = {
|
|||||||
export const DebtSupplierFilterSchema: yup.ObjectSchema<DebtSupplierFilterType> =
|
export const DebtSupplierFilterSchema: yup.ObjectSchema<DebtSupplierFilterType> =
|
||||||
yup.object({
|
yup.object({
|
||||||
startDate: yup.string().optional().notRequired(),
|
startDate: yup.string().optional().notRequired(),
|
||||||
endDate: yup.string().optional().notRequired(),
|
endDate: yup
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.notRequired()
|
||||||
|
.test(
|
||||||
|
'is-greater-than-start',
|
||||||
|
'Tanggal akhir tidak boleh masa lampau',
|
||||||
|
function (value) {
|
||||||
|
const startDate = this.parent.startDate;
|
||||||
|
if (!startDate || !value) return true;
|
||||||
|
return new Date(value) >= new Date(startDate);
|
||||||
|
}
|
||||||
|
),
|
||||||
supplierIds: yup
|
supplierIds: yup
|
||||||
.array()
|
.array()
|
||||||
.of(
|
.of(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
|||||||
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton';
|
||||||
import { OptionType } from '@/components/table/TableRowSizeSelector';
|
import { OptionType } from '@/components/table/TableRowSizeSelector';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
interface CustomerPaymentTabProps {
|
interface CustomerPaymentTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -118,6 +119,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDateErrorShown(false);
|
setDateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,30 +215,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.filter_by]);
|
}, [formik.values.filter_by]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
// Date filter (start_date + end_date = 1 filter)
|
|
||||||
if (filterParams.start_date || filterParams.end_date) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Customer filter
|
|
||||||
if (filterParams.customer_ids) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by type filter (hanya dihitung jika ada nilai yang dipilih)
|
|
||||||
if (filterParams.filter_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: customerPayment, isLoading } = useSWR(
|
const { data: customerPayment, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
@@ -380,25 +358,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
setTabActions(
|
setTabActions(
|
||||||
tabId,
|
tabId,
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={formik.values}
|
||||||
color='none'
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -455,8 +421,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
hasFilters,
|
formik.values,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
handleExportExcel,
|
handleExportExcel,
|
||||||
handleExportPdf,
|
handleExportPdf,
|
||||||
@@ -880,19 +845,29 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
|
|||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='start_date'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.start_date || ''}
|
||||||
|
errorMessage={formik.errors.start_date}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
|
isError={
|
||||||
|
formik.touched.start_date &&
|
||||||
|
Boolean(formik.errors.start_date)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.end_date || ''}
|
||||||
|
errorMessage={formik.errors.end_date}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
isError={
|
||||||
|
(formik.touched.end_date &&
|
||||||
|
Boolean(formik.errors.end_date)) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
});
|
});
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -106,6 +110,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
|
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== FORMIK SETUP =====
|
// ===== FORMIK SETUP =====
|
||||||
@@ -137,6 +142,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
filter_by: undefined,
|
filter_by: undefined,
|
||||||
});
|
});
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,6 +280,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={formik.values}
|
values={formik.values}
|
||||||
|
fieldGroups={[['startDate', 'endDate']]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -348,6 +355,23 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
};
|
};
|
||||||
}, [tabId, clearTabActions]);
|
}, [tabId, clearTabActions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [filterModal.open, dateErrorShown]);
|
||||||
|
|
||||||
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
const getTableColumns = (supplier?: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||||
{
|
{
|
||||||
id: 'no',
|
id: 'no',
|
||||||
@@ -722,7 +746,31 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
name='startDate'
|
name='startDate'
|
||||||
value={formik.values.startDate || ''}
|
value={formik.values.startDate || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
formik.setFieldValue('startDate', e.target.value || null);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('startDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.endDate) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(formik.values.endDate);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isError={
|
isError={
|
||||||
@@ -736,10 +784,36 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => {
|
|||||||
name='endDate'
|
name='endDate'
|
||||||
value={formik.values.endDate || ''}
|
value={formik.values.endDate || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
formik.setFieldValue('endDate', e.target.value || null);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('endDate', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.startDate) {
|
||||||
|
const startDateObj = new Date(formik.values.startDate);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isError={formik.touched.endDate && !!formik.errors.endDate}
|
isError={
|
||||||
|
(formik.touched.endDate && !!formik.errors.endDate) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
errorMessage={formik.errors.endDate}
|
errorMessage={formik.errors.endDate}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
|||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
|
import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
interface PurchasesPerSupplierTabProps {
|
interface PurchasesPerSupplierTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -146,6 +147,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDateErrorShown(false);
|
setDateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -253,43 +255,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.sort_by, sortByOptions]);
|
}, [formik.values.sort_by, sortByOptions]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (filterParams.start_date || filterParams.end_date) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.area_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.supplier_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.product_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.product_category_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.filter_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.sort_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: purchasePerSupplier, isLoading } = useSWR(
|
const { data: purchasePerSupplier, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
@@ -486,25 +451,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
setTabActions(
|
setTabActions(
|
||||||
tabId,
|
tabId,
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={formik.values}
|
||||||
color='none'
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -561,8 +514,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
hasFilters,
|
formik.values,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
filterModal.open,
|
filterModal.open,
|
||||||
setTabActions,
|
setTabActions,
|
||||||
@@ -891,18 +843,28 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
|
|||||||
<DateInput
|
<DateInput
|
||||||
name='start_date'
|
name='start_date'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.start_date || ''}
|
||||||
|
errorMessage={formik.errors.start_date}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
|
isError={
|
||||||
|
formik.touched.start_date &&
|
||||||
|
Boolean(formik.errors.start_date)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
<hr className='w-full max-w-3 h-px border-base-content/10' />
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.end_date || ''}
|
||||||
|
errorMessage={formik.errors.end_date}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isNestedModal
|
isNestedModal
|
||||||
isError={hasDateError}
|
isError={
|
||||||
|
(formik.touched.end_date &&
|
||||||
|
Boolean(formik.errors.end_date)) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
MARKETING_TYPE_OPTIONS,
|
MARKETING_TYPE_OPTIONS,
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
|
||||||
interface DailyMarketingTabProps {
|
interface DailyMarketingTabProps {
|
||||||
tabId: string;
|
tabId: string;
|
||||||
@@ -79,6 +80,10 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
// ===== FILTER STATE =====
|
// ===== FILTER STATE =====
|
||||||
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
const [filterParams, setFilterParams] = useState<FilterParams>({});
|
||||||
|
|
||||||
|
// ===== DATE ERROR STATE =====
|
||||||
|
const [dateErrorShown, setDateErrorShown] = useState(false);
|
||||||
|
const [hasDateError, setHasDateError] = useState(false);
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
// ===== OPTIONS =====
|
// ===== OPTIONS =====
|
||||||
@@ -137,6 +142,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
onReset: () => {
|
onReset: () => {
|
||||||
setFilterParams({});
|
setFilterParams({});
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,47 +208,6 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.marketing_type]);
|
}, [formik.values.marketing_type]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (filterParams.area_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.location_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.warehouse_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.customer_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.start_date || filterParams.end_date) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.filter_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.marketing_type) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.sort_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: dailyMarketings, isLoading } = useSWR(
|
const { data: dailyMarketings, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
@@ -412,30 +377,13 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={formik.values}
|
||||||
color='none'
|
fieldGroups={[['start_date', 'end_date']]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<Badge
|
|
||||||
className={{
|
|
||||||
badge:
|
|
||||||
'p-1.5 bg-[#FF3535] text-xs text-base-100 border border-base-300 rounded-lg',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -493,8 +441,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
searchValue,
|
searchValue,
|
||||||
hasFilters,
|
formik.values,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
filterModal.open,
|
filterModal.open,
|
||||||
setTabActions,
|
setTabActions,
|
||||||
@@ -506,6 +453,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
};
|
};
|
||||||
}, [tabId, clearTabActions]);
|
}, [tabId, clearTabActions]);
|
||||||
|
|
||||||
|
useEffectHook(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [dateErrorShown]);
|
||||||
|
|
||||||
|
useEffectHook(() => {
|
||||||
|
return () => {
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [filterModal.open, dateErrorShown]);
|
||||||
|
|
||||||
const getTableColumns = (): ColumnDef<DailyMarketingRow>[] => {
|
const getTableColumns = (): ColumnDef<DailyMarketingRow>[] => {
|
||||||
const tableColumns: ColumnDef<DailyMarketingRow>[] = [
|
const tableColumns: ColumnDef<DailyMarketingRow>[] = [
|
||||||
{
|
{
|
||||||
@@ -849,34 +813,76 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
placeholder='Pilih Tanggal Awal'
|
placeholder='Pilih Tanggal Awal'
|
||||||
value={formik.values.start_date || ''}
|
value={formik.values.start_date || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
formik.setFieldValue('start_date', e.target.value || null);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('start_date', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.end_date) {
|
||||||
|
const startDate = new Date(value);
|
||||||
|
const endDateObj = new Date(formik.values.end_date);
|
||||||
|
|
||||||
|
if (endDateObj < startDate) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setHasDateError(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
errorMessage={formik.errors.start_date}
|
||||||
isError={
|
isError={
|
||||||
!!formik.errors.start_date && formik.touched.start_date
|
!!formik.errors.start_date && formik.touched.start_date
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{formik.errors.start_date && formik.touched.start_date && (
|
|
||||||
<div className='text-error text-xs mt-1'>
|
|
||||||
{formik.errors.start_date}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DateInput
|
<DateInput
|
||||||
name='end_date'
|
name='end_date'
|
||||||
label='Tanggal Akhir'
|
label='Tanggal Akhir'
|
||||||
placeholder='Pilih Tanggal Akhir'
|
placeholder='Pilih Tanggal Akhir'
|
||||||
value={formik.values.end_date || ''}
|
value={formik.values.end_date || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
formik.setFieldValue('end_date', e.target.value || null);
|
const value = e.target.value;
|
||||||
|
formik.setFieldValue('end_date', value || null);
|
||||||
|
|
||||||
|
if (value && formik.values.start_date) {
|
||||||
|
const startDateObj = new Date(formik.values.start_date);
|
||||||
|
const endDate = new Date(value);
|
||||||
|
|
||||||
|
if (endDate < startDateObj) {
|
||||||
|
setHasDateError(true);
|
||||||
|
if (!dateErrorShown) {
|
||||||
|
toast.error('Tanggal akhir tidak boleh masa lampau', {
|
||||||
|
duration: Infinity,
|
||||||
|
});
|
||||||
|
setDateErrorShown(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasDateError(false);
|
||||||
|
if (dateErrorShown) {
|
||||||
|
toast.dismiss();
|
||||||
|
setDateErrorShown(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isError={!!formik.errors.end_date && formik.touched.end_date}
|
errorMessage={formik.errors.end_date}
|
||||||
|
isError={
|
||||||
|
(formik.errors.end_date && formik.touched.end_date) ||
|
||||||
|
hasDateError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{formik.errors.end_date && formik.touched.end_date && (
|
|
||||||
<div className='text-error text-xs mt-1'>
|
|
||||||
{formik.errors.end_date}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@/types/api/report/hpp-per-kandang';
|
} from '@/types/api/report/hpp-per-kandang';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF';
|
import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF';
|
||||||
import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX';
|
import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX';
|
||||||
@@ -136,6 +137,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
toast.dismiss();
|
toast.dismiss();
|
||||||
setDateErrorShown(false);
|
setDateErrorShown(false);
|
||||||
}
|
}
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,43 +235,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.show_unrecorded, showUnrecordedOptions]);
|
}, [formik.values.show_unrecorded, showUnrecordedOptions]);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (filterParams.period) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.area_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.location_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.kandang_id) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.weight_min || filterParams.weight_max) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.show_unrecorded !== undefined) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterParams.sort_by) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: hppPerKandang, isLoading } = useSWR(
|
const { data: hppPerKandang, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
@@ -486,25 +451,12 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
setTabActions(
|
setTabActions(
|
||||||
tabId,
|
tabId,
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={formik.values}
|
||||||
color='none'
|
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -561,8 +513,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
hasFilters,
|
formik.values,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
filterModal.open,
|
filterModal.open,
|
||||||
setTabActions,
|
setTabActions,
|
||||||
|
|||||||
+8
-34
@@ -7,6 +7,7 @@ import toast from 'react-hot-toast';
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable';
|
import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable';
|
||||||
@@ -237,6 +238,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
setFilterParams({});
|
setFilterParams({});
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
filterModal.closeModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,20 +326,6 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
[formik.values.kandang_id]
|
[formik.values.kandang_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== ACTIVE FILTERS COUNT =====
|
|
||||||
const activeFiltersCount = useMemo(() => {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (filterParams.area_id) count += 1;
|
|
||||||
if (filterParams.location_id) count += 1;
|
|
||||||
if (filterParams.project_flock_id) count += 1;
|
|
||||||
if (filterParams.project_flock_kandang_id) count += 1;
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}, [filterParams]);
|
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: projectFlockKandangsData, isLoading } = useSWR<
|
const { data: projectFlockKandangsData, isLoading } = useSWR<
|
||||||
BaseApiResponse<ProjectFlockKandang[]>
|
BaseApiResponse<ProjectFlockKandang[]>
|
||||||
@@ -539,25 +527,12 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
setTabActions(
|
setTabActions(
|
||||||
tabId,
|
tabId,
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
variant='outline'
|
values={filterParams}
|
||||||
color='none'
|
|
||||||
onClick={() => filterModal.openModal()}
|
onClick={() => filterModal.openModal()}
|
||||||
className={cn(
|
variant='outline'
|
||||||
'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all',
|
className='px-3 py-2.5'
|
||||||
{
|
/>
|
||||||
'border-primary-gradient text-primary': hasFilters,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
||||||
Filter
|
|
||||||
{hasFilters && (
|
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
|
||||||
{activeFiltersCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
@@ -614,8 +589,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => {
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
tabId,
|
tabId,
|
||||||
hasFilters,
|
filterParams,
|
||||||
activeFiltersCount,
|
|
||||||
isAnyExportLoading,
|
isAnyExportLoading,
|
||||||
exportToExcelHandler,
|
exportToExcelHandler,
|
||||||
exportToPdfHandler,
|
exportToPdfHandler,
|
||||||
|
|||||||
@@ -545,3 +545,19 @@ export const MARKETING_DATE_FILTER_TYPE_OPTIONS = [
|
|||||||
value: 'so_date',
|
value: 'so_date',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const TRANSACTION_TYPE_OPTIONS = [
|
||||||
|
{ label: 'Pembelian', value: 'PEMBELIAN' },
|
||||||
|
{ label: 'Penjualan', value: 'PENJUALAN' },
|
||||||
|
{ label: 'Recording', value: 'RECORDING' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TRANSACTION_SUBTYPE_OPTIONS = {
|
||||||
|
PEMBELIAN: { label: 'Pembelian', value: 'PURCHASE_IN' },
|
||||||
|
PENJUALAN: { label: 'Penjualan', value: 'MARKETING_OUT' },
|
||||||
|
RECORDING: [
|
||||||
|
{ label: 'Recording Stock', value: 'RECORDING_STOCK_OUT' },
|
||||||
|
{ label: 'Recording Depletion', value: 'RECORDING_DEPLETION_OUT' },
|
||||||
|
{ label: 'Recording Egg', value: 'RECORDING_EGG_IN' },
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|||||||
+19
-15
@@ -1,31 +1,35 @@
|
|||||||
import { Product } from '@/types/api/master-data/product';
|
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
|
||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { ProjectFlock } from '@/types/api/project-flock';
|
||||||
|
|
||||||
export type BaseInventoryAdjustment = {
|
export type BaseInventoryAdjustment = {
|
||||||
id: number;
|
id: number;
|
||||||
|
adj_number: string;
|
||||||
|
transaction_type: string;
|
||||||
|
transaction_subtype: string;
|
||||||
|
function_code: string;
|
||||||
|
qty: number;
|
||||||
|
price: number;
|
||||||
|
grand_total: number;
|
||||||
increase: number;
|
increase: number;
|
||||||
decrease: number;
|
decrease: number;
|
||||||
note: string;
|
notes: string;
|
||||||
|
location: Location;
|
||||||
|
project_flock: ProjectFlock;
|
||||||
product_warehouse_id: number;
|
product_warehouse_id: number;
|
||||||
product_warehouse: {
|
product_warehouse: ProductWarehouse;
|
||||||
id: number;
|
project_flock_kandang_id?: number;
|
||||||
quantity: number;
|
|
||||||
product_id: number;
|
|
||||||
warehouse_id: number;
|
|
||||||
product: Product;
|
|
||||||
warehouse: Warehouse;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InventoryAdjustment = BaseMetadata & BaseInventoryAdjustment;
|
export type InventoryAdjustment = BaseMetadata & BaseInventoryAdjustment;
|
||||||
|
|
||||||
export type CreateInventoryAdjustmentPayload = {
|
export type CreateInventoryAdjustmentPayload = {
|
||||||
|
project_flock_kandang_id: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
warehouse_id: number;
|
transaction_subtype: string;
|
||||||
transaction_type: string;
|
qty: number;
|
||||||
quantity: number;
|
price: number;
|
||||||
note: string;
|
notes: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateInventoryAdjustmentPayload =
|
export type UpdateInventoryAdjustmentPayload =
|
||||||
|
|||||||
Reference in New Issue
Block a user