Merge branch 'fix/project-flock' into 'development'

[FIX/FE] Project Flock

See merge request mbugroup/lti-web-client!314
This commit is contained in:
Rivaldi A N S
2026-02-06 02:58:02 +00:00
17 changed files with 1998 additions and 1125 deletions
+2
View File
@@ -68,6 +68,8 @@
--shadow-button-soft: --shadow-button-soft:
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200); 0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
--shadow-bg: 0px -2px 4px 0px #00000014;
} }
html { html {
+25 -13
View File
@@ -1,10 +1,10 @@
'use client'; 'use client';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import Drawer from '@/components/Drawer'; import React, { ReactNode, useEffect } from 'react';
import React, { ReactNode } from 'react';
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable'; import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import Modal, { useModal } from '@/components/Modal';
export default function ProjectFlockLayout({ export default function ProjectFlockLayout({
children, children,
@@ -23,9 +23,12 @@ export default function ProjectFlockLayout({
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing; const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
const formModal = useModal();
const handleBackdropClick = () => { const handleBackdropClick = () => {
const unsub = useUiStore.getState().subscribeIsValid((isValid) => { const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
if (isValid) { if (isValid) {
formModal.closeModal();
unsub(); // berhenti listen unsub(); // berhenti listen
router.push('/production/project-flock'); router.push('/production/project-flock');
} }
@@ -34,6 +37,14 @@ export default function ProjectFlockLayout({
toggleValidate(); toggleValidate();
}; };
useEffect(() => {
if (isOpen && !formModal.open) {
formModal.openModal();
} else {
formModal.closeModal();
}
}, [isOpen]);
return ( return (
<> <>
{/* List page always rendered */} {/* List page always rendered */}
@@ -43,18 +54,19 @@ export default function ProjectFlockLayout({
/> />
</div> </div>
{/* Render Drawer only on /add */} {/* Render Modal only on /add */}
<Drawer <Modal
open={isOpen} ref={formModal.ref}
setOpen={(v) => { position='end'
if (!v) router.push('/production/project-flock');
}}
closeOnBackdropClick={isDetail ? true : false}
onBackdropClick={handleBackdropClick} onBackdropClick={handleBackdropClick}
variant='right' className={{
zIndex='99999' modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
sidebarContent={isOpen && <div className=''>{children}</div>} }}
/> >
<div className='w-full sm:w-[446px] h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-hidden'>
{isOpen && children}
</div>
</Modal>
</> </>
); );
} }
+8 -3
View File
@@ -1,15 +1,16 @@
import { ReactNode } from 'react'; import { ReactNode, Ref } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
interface AlertProps { interface AlertProps {
ref?: Ref<HTMLDivElement> | undefined;
variant?: 'outline' | 'dash' | 'soft'; variant?: 'outline' | 'dash' | 'soft';
color?: 'info' | 'success' | 'warning' | 'error'; color?: 'info' | 'success' | 'warning' | 'error';
children?: ReactNode; children?: ReactNode;
className?: string; className?: string;
} }
const Alert = ({ children, variant, color, className }: AlertProps) => { const Alert = ({ children, ref, variant, color, className }: AlertProps) => {
const alertBaseClassName = cn('alert', { const alertBaseClassName = cn('alert', {
'alert-soft': variant === 'soft', 'alert-soft': variant === 'soft',
'alert-outline': variant === 'outline', 'alert-outline': variant === 'outline',
@@ -21,7 +22,11 @@ const Alert = ({ children, variant, color, className }: AlertProps) => {
'alert-error': color === 'error', 'alert-error': color === 'error',
}); });
return <div className={cn(alertBaseClassName, className)}>{children}</div>; return (
<div ref={ref} className={cn(alertBaseClassName, className)}>
{children}
</div>
);
}; };
export default Alert; export default Alert;
+3 -1
View File
@@ -9,6 +9,7 @@ import Button from '@/components/Button';
import { cn, formatDate } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
interface ApprovalStepsV2Props { interface ApprovalStepsV2Props {
title?: string;
approvals?: BaseApproval[]; approvals?: BaseApproval[];
steps: { steps: {
step_number: number; step_number: number;
@@ -23,6 +24,7 @@ interface ApprovalStepsV2Props {
} }
const ApprovalStepsV2 = ({ const ApprovalStepsV2 = ({
title = 'Progress Details',
approvals, approvals,
steps, steps,
maxVisibleSteps = 2, maxVisibleSteps = 2,
@@ -99,7 +101,7 @@ const ApprovalStepsV2 = ({
)} )}
> >
<h4 className='text-base font-medium text-base-content/50 font-roboto'> <h4 className='text-base font-medium text-base-content/50 font-roboto'>
Progress Details {title}
</h4> </h4>
<div <div
+7 -1
View File
@@ -1,24 +1,30 @@
import { ReactNode } from 'react';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
interface StatusBadgeProps { interface StatusBadgeProps {
color: Color; color: Color;
text: string; text: ReactNode;
className?: { className?: {
badge?: string; badge?: string;
status?: string; status?: string;
}; };
onClick?: () => void;
} }
const StatusBadge = ({ const StatusBadge = ({
color = 'neutral', color = 'neutral',
text, text,
className, className,
onClick,
}: StatusBadgeProps) => { }: StatusBadgeProps) => {
return ( return (
<Badge <Badge
variant='soft' variant='soft'
onClick={onClick}
className={{ className={{
badge: cn( badge: cn(
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content', 'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
@@ -27,7 +27,7 @@ export interface DrawerHeaderProps {
const DrawerHeader = ({ const DrawerHeader = ({
leftIcon = 'mdi:close', leftIcon = 'mdi:close',
leftIconSize = 24, leftIconSize = 20,
leftIconHref, leftIconHref,
leftIconOnClick, leftIconOnClick,
leftIconClassName, leftIconClassName,
@@ -43,7 +43,7 @@ const DrawerHeader = ({
icon={leftIcon} icon={leftIcon}
width={leftIconSize} width={leftIconSize}
height={leftIconSize} height={leftIconSize}
className={cn('cursor-pointer', leftIconClassName)} className={cn('cursor-pointer text-base-content ', leftIconClassName)}
/> />
); );
@@ -73,7 +73,7 @@ const DrawerHeader = ({
return ( return (
<div <div
className={cn( className={cn(
'flex flex-row justify-between items-center px-4 pt-4 pb-4 border-b border-base-content/10', 'flex flex-row justify-between items-center p-4 border-b border-base-content/10',
className className
)} )}
> >
@@ -82,7 +82,7 @@ const DrawerHeader = ({
{renderLeftIcon()} {renderLeftIcon()}
{showDivider && subtitle && ( {showDivider && subtitle && (
<div className='divider divider-horizontal p-0 m-0'></div> <div className='w-px h-full border-none bg-base-content/10' />
)} )}
{subtitle && ( {subtitle && (
+16 -1
View File
@@ -1,8 +1,10 @@
'use client';
import Alert from '@/components/Alert'; import Alert from '@/components/Alert';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState } from 'react'; import { useEffect, useRef } from 'react';
/** /**
* Alert Unique Error List * Alert Unique Error List
@@ -29,10 +31,22 @@ const AlertErrorList = ({
onClose: () => void; onClose: () => void;
title?: string; title?: string;
}) => { }) => {
const alertRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (formErrorList.length > 0) {
alertRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [formErrorList.length]);
if (formErrorList.length === 0) return null; if (formErrorList.length === 0) return null;
return ( return (
<Alert <Alert
ref={alertRef}
color='error' color='error'
className={cn( className={cn(
'w-full flex flex-col gap-2 px-3 rounded-lg', 'w-full flex flex-col gap-2 px-3 rounded-lg',
@@ -57,6 +71,7 @@ const AlertErrorList = ({
</span> </span>
</div> </div>
<Button <Button
type='button'
onClick={onClose} onClick={onClose}
variant='link' variant='link'
className={cn('ml-auto p-0 w-fit text-white', className?.button)} className={cn('ml-auto p-0 w-fit text-white', className?.button)}
@@ -53,7 +53,7 @@ const ChickinFormKandang = ({
}; };
return ( return (
<> <section className='w-full h-full sm:w-[446px] overflow-y-auto'>
<DrawerHeader <DrawerHeader
subtitle={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`} subtitle={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
leftIcon='mdi:arrow-left' leftIcon='mdi:arrow-left'
@@ -198,7 +198,7 @@ const ChickinFormKandang = ({
afterSubmit={afterSubmitFormChickin} afterSubmit={afterSubmitFormChickin}
/> />
</RequirePermission> </RequirePermission>
</> </section>
); );
}; };
@@ -0,0 +1,159 @@
'use client';
import { ChangeEventHandler, RefObject, useId, useState } from 'react';
import useSWR from 'swr';
import ConfirmationModal, {
ConfirmationModalProps,
} from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea';
import { ProjectFlockFormConfirmationTable } from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import { ProjectFlockFormValues } from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema';
import { Color } from '@/types/theme';
import { isResponseSuccess } from '@/lib/api-helper';
import { KandangApi } from '@/services/api/master-data';
interface ProjectFlockConfirmationModalProps
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error';
projectFlockIds?: number[];
projectFlockForm?: ProjectFlockFormValues;
onClose?: () => void;
withNote?: boolean;
noteLabel?: string;
rows?: number;
placeholder?: string;
primaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: (notes: string) => void;
};
}
const ProjectFlockConfirmationModal = ({
ref,
type = 'success',
projectFlockForm,
projectFlockIds,
onClose,
withNote,
rows = 4,
noteLabel,
placeholder = 'Alasan',
primaryButton,
secondaryButton,
...props
}: ProjectFlockConfirmationModalProps) => {
const randomId = useId();
const [notes, setNotes] = useState('');
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: '',
location_id: projectFlockForm?.location_id
? String(projectFlockForm?.location_id)
: '',
limit: '500',
}).toString()}`;
const {
data: kandang,
isLoading: isLoadingKandang,
mutate: refreshKandang,
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
const notesChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setNotes(e.target.value);
};
const closeModalHandler = () => {
onClose?.();
ref.current?.close();
};
return (
<ConfirmationModal
ref={ref}
iconPosition='left'
type={type}
primaryButton={{
...primaryButton,
text: primaryButton?.text ?? 'Oke',
color: primaryButton?.color ?? 'primary',
className: 'rounded-lg',
onClick: (e) => {
if (withNote) {
primaryButton?.onClick?.(notes);
} else if (primaryButton && primaryButton?.onClick) {
primaryButton?.onClick?.('');
} else {
closeModalHandler();
}
setNotes('');
},
}}
secondaryButton={
secondaryButton
? {
text: secondaryButton?.text ?? 'Cancel',
color: secondaryButton?.color ?? 'none',
onClick: (e) => {
if (secondaryButton && secondaryButton?.onClick) {
secondaryButton.onClick?.(e);
} else {
closeModalHandler();
}
setNotes('');
},
}
: undefined
}
className={{
modalBox: 'max-h-full',
}}
{...props}
>
<div className='flex flex-col gap-4'>
{!projectFlockIds && projectFlockForm && (
<ProjectFlockFormConfirmationTable
projectFlockForm={projectFlockForm}
kandangs={
isResponseSuccess(kandang) && kandang?.data ? kandang.data : []
}
/>
)}
{/* {projectFlockIds &&
!projectFlockForm &&
projectFlockIds.map((projectFlockId, idx) => (
<ProjectFlockFormConfirmationTable
key={idx}
projectFlockId={projectFlockId}
kandangs={
isResponseSuccess(kandang) && kandang?.data ? kandang.data : []
}
/>
))} */}
{withNote && (
<TextArea
name={randomId}
label={noteLabel}
placeholder={placeholder}
value={notes}
onChange={notesChangeHandler}
rows={rows}
/>
)}
</div>
</ConfirmationModal>
);
};
export default ProjectFlockConfirmationModal;
@@ -13,6 +13,7 @@ import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Dropdown from '@/components/Dropdown';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn, formatDate, formatTitleCase } from '@/lib/helper'; import { cn, formatDate, formatTitleCase } from '@/lib/helper';
@@ -29,6 +30,111 @@ import toast from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import StatusBadge from '@/components/helper/StatusBadge';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
const RowOptionsMenu = ({
props,
popoverPosition = 'bottom',
editClickHandler,
detailClickHandler,
deleteClickHandler,
}: {
props: CellContext<ProjectFlock, unknown>;
popoverPosition: 'bottom' | 'top';
editClickHandler: (id: number) => void;
detailClickHandler: (id: number) => void;
deleteClickHandler: () => void;
}) => {
// TODO: change this to real condition
const showEditButton = true;
const showDeleteButton = showEditButton;
const popoverId = `projectFlock#${props.row.original.id}`;
const popoverAnchorName = `--anchor-projectFlock#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
const detailClickHandlerWrapper = () => {
detailClickHandler(props.row.original.id);
closePopover();
};
const editClickHandlerWrapper = () => {
editClickHandler(props.row.original.id);
closePopover();
};
return (
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
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.production.project_flocks.detail'>
<Button
// href={`/production/project-flock/detail/?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='none'
onClick={detailClickHandlerWrapper}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:eye' width={20} height={20} />
View Details
</Button>
</RequirePermission>
{showEditButton && (
<RequirePermission permissions='lti.production.project_flocks.update'>
<Button
// href={`/production/project-flock/detail/edit/?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='none'
onClick={editClickHandlerWrapper}
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:pencil-square' width={20} height={20} />
Edit
</Button>
</RequirePermission>
)}
{showDeleteButton && (
<RequirePermission permissions='lti.production.project_flocks.delete'>
<hr className='mx-3 border-base-content/10 h-px' />
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full'
>
<Icon icon='heroicons:trash' width={20} height={20} />
Delete
</Button>
</RequirePermission>
)}
</div>
</PopoverContent>
</div>
);
};
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const { const {
@@ -62,6 +168,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const selectedRowIds = Object.keys(rowSelection) const selectedRowIds = Object.keys(rowSelection)
.filter((id) => rowSelection[id]) .filter((id) => rowSelection[id])
.map((id) => parseInt(id)); .map((id) => parseInt(id));
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null); const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>( const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null null
@@ -78,6 +185,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
); );
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
useState(false);
// ===== Fetch Data ===== // ===== Fetch Data =====
const { const {
@@ -175,14 +284,27 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
: null; : null;
}, [rowSelection]); }, [rowSelection]);
// const canApprove = useMemo(() => {
// if (!selectedSingleRow || isApproveLoading) return false;
// const isPengajuan = selectedSingleRow.approval?.step_number == 1;
// const isNotRejected = selectedSingleRow.approval?.action != 'REJECTED';
// return isPengajuan && isNotRejected;
// }, [selectedSingleRow, isApproveLoading]);
const canApprove = useMemo(() => { const canApprove = useMemo(() => {
if (!selectedSingleRow || isApproveLoading) return false; return selectedRowIds.every((id) => {
const projectFlock = isResponseSuccess(projectFlocks)
? projectFlocks?.data.find((row) => row.id === id)
: null;
const isPengajuan = selectedSingleRow.approval?.step_number == 1; const isProjectFlockRequesting = projectFlock?.approval?.step_number == 1;
const isNotRejected = selectedSingleRow.approval?.action != 'REJECTED'; const isProjectFlockNotRejected =
projectFlock?.approval?.action != 'REJECTED';
return isPengajuan && isNotRejected; return isProjectFlockRequesting && isProjectFlockNotRejected;
}, [selectedSingleRow, isApproveLoading]); });
}, [selectedRowIds, projectFlocks]);
// ====== COLUMNS ====== // ====== COLUMNS ======
const columns = useMemo<ColumnDef<ProjectFlock>[]>( const columns = useMemo<ColumnDef<ProjectFlock>[]>(
@@ -256,44 +378,39 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const approval = props.row.original.approval; const approval = props.row.original.approval;
const isRejected = approval?.action == 'REJECTED'; const isRejected = approval?.action == 'REJECTED';
const isApproved = approval?.action == 'APPROVED'; const isApproved = approval?.action == 'APPROVED';
return (
<Badge let latestApprovalStepName = approval.step_name;
variant='soft'
className={{ const badgeColor = isRejected
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
}}
color={
isRejected
? 'error' ? 'error'
: isApproved : isApproved
? approval?.step_number == 1 ? approval?.step_number == 1
? 'neutral' ? 'neutral'
: approval?.step_number == 2 : approval?.step_number == 2
? 'primary'
: approval?.step_number == 3
? 'success' ? 'success'
: 'neutral'
: 'neutral'
}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
approval?.step_number == 1
? 'neutral'
: approval?.step_number == 2
? 'primary'
: approval?.step_number == 3 : approval?.step_number == 3
? 'success' ? 'error'
: 'neutral' : 'neutral'
: 'neutral';
switch (approval.action.toLowerCase()) {
case 'pengajuan':
latestApprovalStepName = 'Pengajuan';
break;
case 'aktif':
latestApprovalStepName = 'Aktif';
break;
case 'Selesai':
latestApprovalStepName = 'Closing';
break;
} }
/>
{isRejected if (isRejected) {
? 'Ditolak' latestApprovalStepName = 'Ditolak';
: formatTitleCase(approval?.step_name || '')} }
</Badge>
return (
<StatusBadge color={badgeColor} text={latestApprovalStepName} />
); );
}, },
}, },
@@ -325,27 +442,88 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
cell: (props) => cell: (props) =>
formatDate(props.row.original.created_at, 'MMM DD, YYYY'), formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
}, },
{
id: 'actions',
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;
const detailClickHandler = (id: number) => {
router.push(
`/production/project-flock/detail/?projectFlockId=${id}`
);
};
const editClickHandler = (id: number) => {
router.push(
`/production/project-flock/detail/edit/?projectFlockId=${id}`
);
};
const deleteClickHandler = () => {
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
detailClickHandler={detailClickHandler}
editClickHandler={editClickHandler}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
], ],
[] []
); );
const exportToExcelHandler = async () => {
setIsLoadingExportingToExcel(true);
toast.error('Not implemented yet!');
setIsLoadingExportingToExcel(false);
};
const bulkApproveClickHandler = () => {
setApprovalAction('APPROVED');
confirmModal.openModal();
};
const bulkRejectClickHandler = () => {
setApprovalAction('REJECTED');
confirmModal.openModal();
};
return ( return (
<> <>
<div className='min-h-screen w-full p-4'> <div className='@container min-h-screen w-full'>
<div className='flex flex-col gap-2 mb-4'> <div className='flex flex-col mb-4'>
<div className='w-full flex flex-col justify-between items-end gap-2'> {/* <div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'> <div className='flex flex-col sm:flex-row gap-3 w-full'>
<RequirePermission permissions='lti.production.project_flocks.create'> <RequirePermission permissions='lti.production.project_flocks.create'>
<Button <Button
color='primary' color='primary'
className='w-full sm:w-fit'
onClick={() => { onClick={() => {
setRowSelection({}); setRowSelection({});
router.push('/production/project-flock/add'); router.push('/production/project-flock/add');
}} }}
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='heroicons:plus' width={20} height={20} />
Tambah Add Flock
</Button> </Button>
</RequirePermission> </RequirePermission>
<div className='ms-auto w-full sm:w-auto'> <div className='ms-auto w-full sm:w-auto'>
@@ -423,6 +601,158 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
}} }}
/> />
</div> </div>
</div> */}
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
<div className='w-fit flex flex-row gap-3 flex-wrap'>
<RequirePermission permissions='lti.production.project_flocks.create'>
<Button
color='primary'
onClick={() => {
setRowSelection({});
router.push('/production/project-flock/add');
}}
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} />
Add Flock
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && canApprove && (
<>
<hr className='w-px h-full border-none bg-base-content/10 hidden @sm:block' />
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='none'
onClick={bulkRejectClickHandler}
disabled={selectedRowIds.length === 0}
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:x-mark'
width={20}
height={20}
className='text-error'
/>
Reject
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='none'
onClick={bulkApproveClickHandler}
disabled={selectedRowIds.length === 0}
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:check'
width={20}
height={20}
className='text-success'
/>
Approve
</Button>
</RequirePermission>
</>
)}
</div>
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
<DebouncedTextInput
name='search'
placeholder='Search'
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',
}}
/>
<Button
variant='outline'
color='none'
// onClick={filterModal.openModal}
className={cn(
'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
align='end'
direction='bottom'
className={{
content:
'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden',
}}
trigger={
<Button
variant='outline'
color='none'
className='px-3 py-2.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft'
>
<div className='flex flex-row items-center gap-1.5'>
<Icon
icon='heroicons:cloud-arrow-down'
width={20}
height={20}
/>
<span>Export</span>
<div className='w-px self-stretch bg-base-content/10' />
<Icon
icon='heroicons:chevron-down'
width={14}
height={14}
/>
</div>
</Button>
}
>
<Button
variant='ghost'
color='none'
onClick={exportToExcelHandler}
isLoading={isLoadingExportingToExcel}
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
>
<Icon icon='heroicons:table-cells' width={20} height={20} />
Export to Excel
</Button>
</Dropdown>
</div>
</div> </div>
<Table<ProjectFlock> <Table<ProjectFlock>
@@ -448,26 +778,20 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
withCheckbox
className={{ className={{
containerClassName: cn({ containerClassName: cn('p-3', {
'mb-40': 'w-full mb-20':
isResponseSuccess(projectFlocks) && isResponseSuccess(projectFlocks) &&
projectFlocks?.data?.length > 0, projectFlocks?.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> </div>
<FloatingActionsButton {/* <FloatingActionsButton
actions={[ actions={[
{ {
action: 'DETAIL', action: 'DETAIL',
@@ -520,7 +844,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
onClose={() => { onClose={() => {
setRowSelection({}); setRowSelection({});
}} }}
/> /> */}
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -94,6 +94,7 @@ const ProjectFlockClosingForm = ({
return ( return (
<> <>
<section className='w-full h-full sm:w-[446px] overflow-y-auto'>
<DrawerHeader <DrawerHeader
leftIcon='mdi:arrow-left' leftIcon='mdi:arrow-left'
leftIconHref={`/production/project-flock/detail?projectFlockId=${projectFlock.id}`} leftIconHref={`/production/project-flock/detail?projectFlockId=${projectFlock.id}`}
@@ -158,11 +159,14 @@ const ProjectFlockClosingForm = ({
> >
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Kandang <Icon width={14} height={14} icon='mdi:circle-slice-8' /> Kandang
</div> </div>
<div className='col-span-2'>{projectFlockKandang.kandang?.name}</div> <div className='col-span-2'>
{projectFlockKandang.kandang?.name}
</div>
{/* Jumlah DOC */} {/* Jumlah DOC */}
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'> <div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Jumlah DOC <Icon width={14} height={14} icon='mdi:circle-slice-8' /> Jumlah
DOC
</div> </div>
<div className='col-span-2'> <div className='col-span-2'>
{formatNumber( {formatNumber(
@@ -318,6 +322,7 @@ const ProjectFlockClosingForm = ({
onClick: confirmationModalCloseClickHandler, onClick: confirmationModalCloseClickHandler,
}} }}
/> />
</section>
</> </>
); );
}; };
@@ -4,12 +4,7 @@ import Card from '@/components/Card';
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput'; import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
import Tooltip from '@/components/Tooltip'; import Tooltip from '@/components/Tooltip';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
formatCurrency,
formatDate,
formatNumber,
formatTitleCase,
} from '@/lib/helper';
import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlock } from '@/types/api/production/project-flock';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Link from 'next/link'; import Link from 'next/link';
@@ -20,16 +15,15 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import {
PROJECT_FLOCK_APPROVAL_LINE,
PROJECT_FLOCK_KANDANGS_APPROVAL_LINE,
} from '@/config/approval-line';
import useSWR from 'swr'; import useSWR from 'swr';
import { ProjectFlockKandangApi } from '@/services/api/production';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import ApprovalStepsV2 from '@/components/helper/ApprovalStepsV2';
import { APPROVAL_WORKFLOWS } from '@/config/constant';
import Table from '@/components/Table';
import { ProjectFlockFormConfirmationTableType } from '../form/ProjectFlockForm';
import { ColumnDef } from '@tanstack/react-table';
import StatusBadge from '@/components/helper/StatusBadge';
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
const ProjectFlockDetail = ({ const ProjectFlockDetail = ({
projectFlock, projectFlock,
@@ -40,7 +34,7 @@ const ProjectFlockDetail = ({
const deleteModal = useModal(); const deleteModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [openBudgets, setOpenBudget] = useState(false); const [openBudgets, setOpenBudget] = useState(false);
const [selectedKandangId, setSelectedKamdangId] = useState<string | null>( const [selectedKandangId, setSelectedKandangId] = useState<string | null>(
null null
); );
@@ -61,30 +55,94 @@ const ProjectFlockDetail = ({
: null : null
); );
const { const { data: projectFlockApprovalResponse } = useSWR(
approvals, projectFlock.id ? ['approval-project-flock', projectFlock.id] : undefined,
isLoading: approvalsLoading, ([, id]) => ProjectFlockApi.getApprovalLineHistory(Number(id))
refresh: refreshApprovals, );
} = useApprovalSteps({
latestApproval: projectFlock?.approval,
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCKS',
moduleId: projectFlock?.id?.toString() ?? '',
});
const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } = const projectFlockApproval = isResponseSuccess(projectFlockApprovalResponse)
useApprovalSteps({ ? projectFlockApprovalResponse.data
latestApproval: : undefined;
selectedKandangId && isResponseSuccess(projectFlockKandang)
? projectFlockKandang?.data?.approval const { data: projectFlockKandangApprovalResponse } = useSWR(
selectedKandang?.project_flock_kandang_id
? [
'approval-project-flock-kandang',
selectedKandang?.project_flock_kandang_id,
]
: undefined, : undefined,
approvalLines: PROJECT_FLOCK_KANDANGS_APPROVAL_LINE, ([, id]) => ProjectFlockKandangApi.getApprovalLineHistory(Number(id))
moduleName: 'PROJECT_FLOCK_KANDANGS', );
moduleId:
selectedKandangId && isResponseSuccess(projectFlockKandang) const projectFlockKandangApproval = isResponseSuccess(
? projectFlockKandang?.data?.id?.toString() projectFlockKandangApprovalResponse
: '', )
}); ? projectFlockKandangApprovalResponse.data
: undefined;
const confirmationTableColumns: ColumnDef<ProjectFlockFormConfirmationTableType>[] =
[
{
header: 'Label',
accessorKey: 'label',
enableSorting: false,
cell: ({ row }) => {
const isSubRow = row.depth > 0;
return (
<>
{!isSubRow && row.original.label}
{isSubRow && (
<div
className={cn('w-full min-h-full flex items-stretch gap-0')}
>
<div className='w-px mx-4 bg-base-content/10' />
<span className='p-3'>{row.original.label}</span>
</div>
)}
</>
);
},
},
{
header: 'Value',
accessorKey: 'value',
enableSorting: false,
cell: ({ row }) => row.original.value,
},
];
const confirmationTableData: ProjectFlockFormConfirmationTableType[] = [
{
label: 'Tanggal',
value: formatDate(projectFlock.created_at, 'DD MMMM YYYY'),
},
{
label: 'Area',
value: projectFlock.area.name ?? '-',
},
{
label: 'Lokasi',
value: projectFlock.location.name ?? '-',
},
{
label: 'Flock',
value: projectFlock.flock_name ?? '-',
},
{
label: 'Kategori',
value: projectFlock.category ?? '-',
},
{
label: 'Standar Produksi',
value: projectFlock.production_standard.name ?? '-',
},
{
label: 'Periode',
value: projectFlock.period ?? '-',
},
];
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -104,12 +162,14 @@ const ProjectFlockDetail = ({
return ( return (
<> <>
<div className='h-full w-full flex flex-col gap-4'> <div className='h-full w-full flex flex-col overflow-x-hidden overflow-y-auto'>
{/* Header */} {/* Header */}
<DrawerHeader <DrawerHeader
leftIcon='mdi:close' leftIcon='heroicons:chevron-left'
leftIconHref='/production/project-flock' leftIconHref='/production/project-flock'
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`} leftIconClassName='hover:text-gray-400'
subtitle='Detail Flock'
className='sticky top-0 z-10 bg-base-100'
> >
<RequirePermission permissions='lti.production.project_flocks.update'> <RequirePermission permissions='lti.production.project_flocks.update'>
<Link <Link
@@ -118,7 +178,7 @@ const ProjectFlockDetail = ({
> >
<Tooltip content='Edit' position='bottom'> <Tooltip content='Edit' position='bottom'>
<Button variant='link' className='p-0 text-neutral'> <Button variant='link' className='p-0 text-neutral'>
<Icon icon='mdi:square-edit-outline' width={20} height={20} /> <Icon icon='heroicons:pencil-square' width={20} height={20} />
</Button> </Button>
</Tooltip> </Tooltip>
</Link> </Link>
@@ -132,169 +192,57 @@ const ProjectFlockDetail = ({
}} }}
> >
<Tooltip content='Hapus' position='bottom'> <Tooltip content='Hapus' position='bottom'>
<Icon icon='mdi:trash-can-outline' width={20} height={20} /> <Icon icon='heroicons:trash' width={20} height={20} />
</Tooltip> </Tooltip>
</Button> </Button>
</RequirePermission> </RequirePermission>
</DrawerHeader> </DrawerHeader>
{/* Informasi Umum */} <ApprovalStepsV2
<div className='border-t-1 border-gray-300'> approvals={projectFlockApproval}
<div className='p-4 flex flex-col gap-4'> steps={APPROVAL_WORKFLOWS.PROJECT_FLOCKS}
<h2 className='text-2xl font-semibold'>Informasi Umum</h2> />
{/* Status Approval */}
{approvals && !approvalsLoading && ( <div className='w-full p-4 flex flex-col gap-3 border-b border-base-content/10'>
<div className='text-sm my-3'> <h4 className='text-base font-medium text-base-content/50 font-roboto'>
<ApprovalSteps approvals={approvals} /> Informasi Umum
</h4>
<Table<ProjectFlockFormConfirmationTableType>
columns={confirmationTableColumns}
data={confirmationTableData}
withPagination={false}
pageSize={10000}
expanded={true}
getSubRows={(row) => row.subRows}
className={{
headerRowClassName: 'border-b border-base-content/10',
bodyRowClassName: 'border-none',
bodySubRowClassName: () => 'border-none',
bodySubRowColumnClassName: () => 'first:p-0',
}}
/>
</div> </div>
)}
{/* Badge Row */} <div className='w-full p-4 flex flex-col gap-3 border-b border-base-content/10'>
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
Kandang Aktif
</h4>
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
<Badge <StatusBadge
variant='soft' color='info'
color={ text={`Kandang Aktif (${projectFlock.kandangs?.length})`}
projectFlock.approval?.step_number == 1 className={{ badge: 'w-fit text-nowrap' }}
? 'neutral' />
: projectFlock.approval?.step_number == 2
? 'primary' <StatusBadge
: projectFlock.approval?.step_number == 3
? 'success'
: undefined
}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
projectFlock.approval?.step_number == 1
? 'neutral'
: projectFlock.approval?.step_number == 2
? 'primary'
: projectFlock.approval?.step_number == 3
? 'success'
: undefined
}
/>{' '}
{projectFlock.approval?.step_name}
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='neutral' color='neutral'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon icon='mdi:bookmark' width={12} height={12} />
{` ${formatTitleCase(projectFlock.category ?? '')}`}
</Badge>
</div>
{/* Information Grid */}
<div className='grid grid-cols-3 gap-4'>
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:account' /> Submitted
</div>
<div className='col-span-2'>
<Badge
variant='soft'
color='neutral'
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
{projectFlock.created_user?.name}
</Badge>
</div>
{/* BARIS 1 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
</div>
<div className='col-span-2'>{projectFlock?.area?.name}</div>
{/* BARIS 2 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
</div>
<div className='col-span-2'>{projectFlock?.location?.name}</div>
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
</div>
<div className='col-span-2'>{projectFlock?.fcr?.name}</div>
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' />{' '}
Standard
</div>
<div className='col-span-2'>
{projectFlock?.production_standard?.name ?? '-'}
</div>
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:circle-slice-8' />{' '}
Kategori
</div>
<div className='col-span-2'>
{formatTitleCase(projectFlock.category ?? '')}
</div>
</div>
</div>
</div>
{/* Kandang Aktif */}
<div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Kandang Aktif</h2>
{kandangApprovals && !kandangApprovalsLoading && (
<ApprovalSteps approvals={kandangApprovals} />
)}
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={'primary'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'primary'}
/>{' '}
Kandang Aktif ({projectFlock.kandangs?.length})
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='neutral'
variant='soft'
className={{ badge: 'rounded-lg px-2 cursor-pointer' }}
onClick={() => { onClick={() => {
setOpenBudget(!openBudgets); setOpenBudget(!openBudgets);
}} }}
> text={
<>
{` ${formatCurrency( {` ${formatCurrency(
(projectFlock.project_budgets ?? []).reduce( (projectFlock.project_budgets ?? []).reduce(
(acc, curr) => acc + curr.price * curr.qty, (acc, curr) => acc + curr.price * curr.qty,
@@ -306,7 +254,10 @@ const ProjectFlockDetail = ({
width={12} width={12}
height={12} height={12}
/> />
</Badge> </>
}
className={{ badge: 'w-fit text-nowrap cursor-pointer' }}
/>
</div> </div>
{/* Card List Project Budgets */} {/* Card List Project Budgets */}
@@ -316,7 +267,7 @@ const ProjectFlockDetail = ({
key={budget.id} key={budget.id}
variant='bordered' variant='bordered'
className={{ className={{
wrapper: 'w-full', wrapper: 'w-full rounded-lg',
body: 'p-3', body: 'p-3',
}} }}
> >
@@ -341,11 +292,7 @@ const ProjectFlockDetail = ({
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<div className='flex flex-row gap-2 items-center text-gray-400'> <div className='flex flex-row gap-2 items-center text-gray-400'>
<Icon <Icon icon={'mdi:file-multiple'} width={14} height={14} />{' '}
icon={'mdi:file-multiple'}
width={14}
height={14}
/>{' '}
<span>Jumlah Pembelian</span> <span>Jumlah Pembelian</span>
</div> </div>
<div className='text-end text-gray-500'> <div className='text-end text-gray-500'>
@@ -378,7 +325,7 @@ const ProjectFlockDetail = ({
<Card <Card
variant='bordered' variant='bordered'
className={{ className={{
wrapper: 'w-full', wrapper: 'w-full rounded-lg',
body: 'p-3', body: 'p-3',
}} }}
> >
@@ -387,7 +334,7 @@ const ProjectFlockDetail = ({
className={{ className={{
radioWrapper: 'grid grid-cols-1 gap-6', radioWrapper: 'grid grid-cols-1 gap-6',
}} }}
onChange={(e) => setSelectedKamdangId(e.target.value)} onChange={(e) => setSelectedKandangId(e.target.value)}
value={selectedKandangId?.toString()} value={selectedKandangId?.toString()}
size='md' size='md'
color='neutral' color='neutral'
@@ -399,7 +346,7 @@ const ProjectFlockDetail = ({
className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`} className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`}
onClick={() => onClick={() =>
projectFlock?.approval?.step_number > 1 && projectFlock?.approval?.step_number > 1 &&
setSelectedKamdangId(kandang?.id?.toString()) setSelectedKandangId(kandang?.id?.toString())
} }
> >
<RadioGroupItem <RadioGroupItem
@@ -420,6 +367,12 @@ const ProjectFlockDetail = ({
))} ))}
</RadioGroup> </RadioGroup>
</Card> </Card>
<ApprovalStepsV2
approvals={projectFlockKandangApproval}
steps={APPROVAL_WORKFLOWS.PROJECT_FLOCK_KANDANGS}
/>
<div className='grid grid-cols-4 gap-3'> <div className='grid grid-cols-4 gap-3'>
<RequirePermission permissions='lti.production.chickins.detail'> <RequirePermission permissions='lti.production.chickins.detail'>
<Link <Link
@@ -460,7 +413,6 @@ const ProjectFlockDetail = ({
</div> </div>
</div> </div>
</div> </div>
</div>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -19,7 +19,7 @@ import {
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { FormikErrors, useFormik } from 'formik'; import { FormikErrors, useFormik } from 'formik';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { ReactNode, useEffect, useMemo, useState } from 'react';
import useSWR, { KeyedMutator } from 'swr'; import useSWR, { KeyedMutator } from 'swr';
import { import {
ProjectFlockBudgetsSchemaType, ProjectFlockBudgetsSchemaType,
@@ -47,6 +47,13 @@ import { useUiStore } from '@/stores/ui/ui.store';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import Tooltip from '@/components/Tooltip';
import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table';
import StatusBadge from '@/components/helper/StatusBadge';
import { getUniqueFormikErrors } from '@/lib/formik-helper';
import ProjectFlockConfirmationModal from '../ProjectFlockConfirmationModal';
interface ProjectFlockFormProps { interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail'; formType?: 'add' | 'edit' | 'detail';
@@ -56,6 +63,150 @@ interface ProjectFlockFormProps {
>; >;
} }
export interface ProjectFlockFormConfirmationTableType {
label: string;
value: ReactNode;
subRows?: ProjectFlockFormConfirmationTableType[];
}
export const ProjectFlockFormConfirmationTable = ({
projectFlockForm,
kandangs,
}: {
projectFlockForm?: ProjectFlockFormValues;
kandangs: Kandang[];
}) => {
const confirmationTableColumns: ColumnDef<ProjectFlockFormConfirmationTableType>[] =
[
{
header: 'Label',
accessorKey: 'label',
enableSorting: false,
cell: ({ row }) => {
const isSubRow = row.depth > 0;
return (
<>
{!isSubRow && row.original.label}
{isSubRow && (
<div
className={cn('w-full min-h-full flex items-stretch gap-0')}
>
<div className='w-px mx-4 bg-base-content/10' />
<span className='p-3'>{row.original.label}</span>
</div>
)}
</>
);
},
},
{
header: 'Value',
accessorKey: 'value',
enableSorting: false,
cell: ({ row }) => row.original.value,
},
];
const productsData: ProjectFlockFormConfirmationTableType[] =
projectFlockForm?.project_budgets?.map((product) => ({
label: 'Jenis Produk',
value: product.nonstock?.label ?? '-',
subRows: [
{
label: 'Jumlah Pembelian',
value: String(product.qty),
},
{
label: 'Harga Satuan',
value: String(formatCurrency(Number(product.price))),
},
{
label: 'Total Harga',
value: String(formatCurrency(Number(product.total_price))),
},
],
})) ?? [];
const confirmationTableData: ProjectFlockFormConfirmationTableType[] = [
{
label: 'Tanggal',
value: formatDate(Date.now(), 'DD MMMM YYYY'),
},
{
label: 'Area',
value: projectFlockForm?.area?.label ?? '-',
},
{
label: 'Lokasi',
value: projectFlockForm?.location?.label ?? '-',
},
{
label: 'Flock',
value: projectFlockForm?.flock?.label ?? '-',
},
{
label: 'Kategori',
value: projectFlockForm?.category ?? '-',
},
{
label: 'Standar Produksi',
value: projectFlockForm?.production_standard?.label ?? '-',
},
{
label: 'Informasi Kandang',
value: '',
subRows:
projectFlockForm?.kandang_ids?.map((kandang_id) => {
const kandang = kandangs.find((kandang) => kandang.id === kandang_id);
const kandangName = kandang?.name ?? '-';
const kandangAvailability = kandang?.status;
return {
label: kandangName,
value: (
<StatusBadge
color={
kandangAvailability === 'NON_ACTIVE' ? 'info' : 'neutral'
}
text={
kandangAvailability === 'NON_ACTIVE'
? 'Tersedia'
: 'Tidak Tersedia'
}
className={{ badge: 'text-nowrap' }}
/>
),
};
}) ?? [],
},
{
label: 'Estimasi Anggaran per Kandang',
value: '',
},
...productsData,
];
return (
<Table<ProjectFlockFormConfirmationTableType>
columns={confirmationTableColumns}
data={confirmationTableData}
withPagination={false}
pageSize={10000}
expanded={true}
getSubRows={(row) => row.subRows}
className={{
headerRowClassName: 'border-b border-base-content/10',
bodyRowClassName: 'border-none',
bodySubRowClassName: () => 'border-none',
bodySubRowColumnClassName: () => 'first:p-0',
}}
/>
);
};
const ProjectFlockForm = ({ const ProjectFlockForm = ({
formType = 'add', formType = 'add',
initialValues, initialValues,
@@ -64,6 +215,8 @@ const ProjectFlockForm = ({
// State // State
const router = useRouter(); const router = useRouter();
const [formStep, setFormStep] = useState<'form' | 'confirmation'>('form');
const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] =
useState(''); useState('');
const [selectedArea, setSelectedArea] = useState(''); const [selectedArea, setSelectedArea] = useState('');
@@ -87,6 +240,7 @@ const ProjectFlockForm = ({
const subscribeValidate = useUiStore((s) => s.subscribeValidate); const subscribeValidate = useUiStore((s) => s.subscribeValidate);
const setIsValid = useUiStore((s) => s.setIsValid); const setIsValid = useUiStore((s) => s.setIsValid);
const successModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@@ -285,7 +439,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(createProjectFlockRes)) { if (isResponseSuccess(createProjectFlockRes)) {
toast.success(createProjectFlockRes?.message as string); toast.success(createProjectFlockRes?.message as string);
handleReset(); handleReset();
router.push('/production/project-flock'); successModal.openModal();
} }
if (isResponseError(createProjectFlockRes)) { if (isResponseError(createProjectFlockRes)) {
setProjectFlockFormErrorMessage(createProjectFlockRes?.message as string); setProjectFlockFormErrorMessage(createProjectFlockRes?.message as string);
@@ -303,7 +457,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(updateProjectFlockRes)) { if (isResponseSuccess(updateProjectFlockRes)) {
toast.success(updateProjectFlockRes?.message as string); toast.success(updateProjectFlockRes?.message as string);
handleReset(); handleReset();
router.push('/production/project-flock'); successModal.openModal();
} }
if (isResponseError(updateProjectFlockRes)) { if (isResponseError(updateProjectFlockRes)) {
setProjectFlockFormErrorMessage(updateProjectFlockRes?.message as string); setProjectFlockFormErrorMessage(updateProjectFlockRes?.message as string);
@@ -320,6 +474,10 @@ const ProjectFlockForm = ({
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}; };
const [formikLastValues, setFormikLastValues] = useState<
ProjectFlockFormValues | undefined
>(undefined);
// Formik InitialValue // Formik InitialValue
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => { const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
const trimFlock = const trimFlock =
@@ -429,6 +587,8 @@ const ProjectFlockForm = ({
}), }),
}; };
setFormikLastValues(values);
switch (formType) { switch (formType) {
case 'add': case 'add':
await createProjectFlockHandler(payload); await createProjectFlockHandler(payload);
@@ -654,78 +814,119 @@ const ProjectFlockForm = ({
}); });
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { formErrorList, close, setFormErrorList, handleFormSubmit } =
useFormikErrorList(formik);
return ( return (
<> <>
<section className='w-full'> <form
onSubmit={(e) => {
e.preventDefault();
const submitHandler = async () => {
const validateFormErrorResult = await formik.validateForm();
const isFormError = Object.keys(validateFormErrorResult).length > 0;
if (isFormError) {
const errorMessages = getUniqueFormikErrors(
validateFormErrorResult
);
setFormErrorList(errorMessages);
}
if (isFormError) {
return;
}
if (formStep === 'form') {
setFormStep('confirmation');
return;
}
handleFormSubmit(e);
};
submitHandler();
}}
onReset={formik.handleReset}
className='w-full h-full sm:w-[446px] flex flex-col'
>
{/* Header */} {/* Header */}
<DrawerHeader <DrawerHeader
leftIcon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'} leftIcon={
leftIconSize={24} formType == 'add' ? 'heroicons:chevron-left' : 'mdi:arrow-left'
leftIconHref={
formType == 'add'
? '/production/project-flock'
: `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
} }
// leftIconHref={
// formType == 'add'
// ? '/production/project-flock'
// : `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
// }
leftIconOnClick={() => {
if (formStep === 'confirmation') {
setFormStep('form');
return;
}
if (formType == 'add') {
router.push('/production/project-flock');
} else {
router.push(
`/production/project-flock/detail?projectFlockId=${initialValues?.id}`
);
}
}}
leftIconClassName='hover:text-gray-400' leftIconClassName='hover:text-gray-400'
subtitle={formType == 'add' ? 'Add Flock' : 'Update Flock'} subtitle={formType == 'add' ? 'Add List Flock' : 'Update Flock'}
subtitleClassName='text-sm text-neutral' className='sticky top-0 z-10 bg-base-100'
showDivider
> >
{formType == 'edit' && ( {formType == 'edit' && (
<Button <Button
type='button'
variant='ghost'
color='none'
onClick={() => { onClick={() => {
if (initialValues?.id) { if (initialValues?.id) {
deleteModal.openModal(); deleteModal.openModal();
} }
}} }}
variant='link'
className='p-0 text-error' className='p-0 text-error'
> >
<Icon <Tooltip content='Hapus' position='bottom'>
icon='material-symbols:delete-outline-rounded' <Icon icon='heroicons:trash' width={20} height={20} />
width={20} </Tooltip>
height={20} </Button>
className='justify-start text-sm' )}
/>
{formType == 'add' && (
<Button
type='button'
variant='ghost'
color='none'
onClick={() => {
router.push('/production/project-flock');
}}
className='p-0 text-error'
>
<Tooltip content='Hapus' position='bottom'>
<Icon icon='heroicons:trash' width={20} height={20} />
</Tooltip>
</Button> </Button>
)} )}
</DrawerHeader> </DrawerHeader>
{projectFlockFormErrorMessage && (
<div className='my-4'>
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{projectFlockFormErrorMessage}</span>
<Button
onClick={() => {
setProjectFlockFormErrorMessage('');
}}
variant='link'
>
<Icon icon='material-symbols:close' width={24} height={24} />
</Button>
</div>
</div>
)}
<form <div className='w-auto h-full overflow-y-auto'>
className='w-auto h-auto' {formStep === 'form' && (
onSubmit={handleFormSubmit} <div className='h-full'>
onReset={formik.handleReset}
>
{/* Form Informasi Umum */} {/* Form Informasi Umum */}
<div className='divider mt-3'></div> <div className='flex flex-col p-4'>
<div className='flex flex-col gap-4 px-4'> <h2 className='text-base font-medium text-base-content/50 font-roboto'>
<h2 className='text-2xl font-semibold'>Informasi Umum</h2> Informasi Umum
<div className='flex flex-col gap-4'> </h2>
<SelectInput <SelectInput
required required
label='Area' label='Area'
placeholder='Pilih Area'
value={formik.values.area as OptionType} value={formik.values.area as OptionType}
onChange={areaChangeHandler} onChange={areaChangeHandler}
options={optionsArea} options={optionsArea}
@@ -742,6 +943,7 @@ const ProjectFlockForm = ({
<SelectInput <SelectInput
required required
label='Lokasi' label='Lokasi'
placeholder='Pilih Lokasi'
value={formik.values.location as OptionType} value={formik.values.location as OptionType}
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={ options={
@@ -763,6 +965,7 @@ const ProjectFlockForm = ({
<SelectInput <SelectInput
required required
label='Flock' label='Flock'
placeholder='Pilih Flock'
value={ value={
formik.values.flock_name formik.values.flock_name
? ({ ? ({
@@ -786,7 +989,8 @@ const ProjectFlockForm = ({
onMenuScrollToBottom={loadMoreFlock} onMenuScrollToBottom={loadMoreFlock}
isLoading={isLoadingFlocks} isLoading={isLoadingFlocks}
isError={ isError={
formik.touched.flock_name && Boolean(formik.errors.flock_name) formik.touched.flock_name &&
Boolean(formik.errors.flock_name)
} }
errorMessage={formik.errors.flock_name as string} errorMessage={formik.errors.flock_name as string}
isClearable isClearable
@@ -795,6 +999,7 @@ const ProjectFlockForm = ({
<SelectInput <SelectInput
required required
label='FCR' label='FCR'
placeholder='Pilih FCR'
value={formik.values.fcr as OptionType} value={formik.values.fcr as OptionType}
onChange={(val) => { onChange={(val) => {
optionChangeHandler(val, 'fcr'); optionChangeHandler(val, 'fcr');
@@ -803,7 +1008,9 @@ const ProjectFlockForm = ({
onMenuScrollToBottom={loadMoreFcr} onMenuScrollToBottom={loadMoreFcr}
options={optionsFcr} options={optionsFcr}
isLoading={isLoadingFcrs} isLoading={isLoadingFcrs}
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} isError={
formik.touched.fcr_id && Boolean(formik.errors.fcr_id)
}
errorMessage={formik.errors.fcr_id as string} errorMessage={formik.errors.fcr_id as string}
isClearable isClearable
isDisabled={formType != 'add'} isDisabled={formType != 'add'}
@@ -811,6 +1018,7 @@ const ProjectFlockForm = ({
<SelectInput <SelectInput
required required
label='Kategori' label='Kategori'
placeholder='Pilih Kategori'
value={formik.values.category_option as OptionType} value={formik.values.category_option as OptionType}
onChange={categoryChangeHandler} onChange={categoryChangeHandler}
options={FLOCK_CATEGORY_OPTIONS} options={FLOCK_CATEGORY_OPTIONS}
@@ -824,6 +1032,7 @@ const ProjectFlockForm = ({
<SelectInput <SelectInput
required required
label='Standar Produksi' label='Standar Produksi'
placeholder='Pilih Standar Produksi'
value={formik.values.production_standard as OptionType} value={formik.values.production_standard as OptionType}
onChange={(val) => { onChange={(val) => {
optionChangeHandler(val, 'production_standard'); optionChangeHandler(val, 'production_standard');
@@ -845,19 +1054,20 @@ const ProjectFlockForm = ({
label='Periode' label='Periode'
disabled disabled
readOnly readOnly
placeholder='Period' placeholder='Periode Flock'
value={selectedLocation ? inputPeriod : ''} value={selectedLocation ? inputPeriod : ''}
/> />
</div> </div>
</div>
{/* Form Pilih Kandang */} {/* Form Pilih Kandang */}
<div className='divider'></div>
<div className='flex flex-col gap-4 px-4 pb-4'> <div className='flex flex-col gap-3 p-4 border-y border-base-content/10'>
<h2 className='text-2xl font-semibold'>Pilih Kandang</h2> <h2 className='text-base font-medium text-base-content/50 font-roboto'>
Informasi Kandang
</h2>
<div className='overflow-x-auto duration-300 ease-in-out'> <div className='overflow-x-auto duration-300 ease-in-out'>
{isLoadingKandang && ( {isLoadingKandang && (
<span className='loading loading-dots loading-xl'></span> <span className='loading loading-dots loading-xl block mx-auto' />
)} )}
<ProjectFlockKandangTable <ProjectFlockKandangTable
listPeriods={ listPeriods={
@@ -874,53 +1084,67 @@ const ProjectFlockForm = ({
</div> </div>
{/* Card Estimasi Budget */} {/* Card Estimasi Budget */}
<div className='divider'></div> <div className='flex flex-col'>
<div className='flex flex-col gap-4 px-4 pb-4'> <div className='flex flex-col'>
<h2 className='text-2xl font-semibold'>
Estimasi Anggaran Per Flock
</h2>
<div className='flex flex-col gap-4'>
{formik.values.project_budgets && {formik.values.project_budgets &&
formik.values.project_budgets.length > 0 ? ( formik.values.project_budgets.length > 0 ? (
formik.values.project_budgets.map((budget, index) => ( formik.values.project_budgets.map((budget, index) => {
<Card const nonstockUomName = isResponseSuccess(nonstocks)
? (nonstocks.data.find(
(ns) => ns.id === budget.nonstock_id
)?.uom?.name ?? '')
: '';
return (
<div
key={index} key={index}
variant='bordered' className='flex flex-col p-4 border-b border-base-content/10'
className={{
wrapper: 'w-full',
body: 'p-3',
}}
> >
<div className='flex flex-col gap-2'> <div className='flex flex-row justify-between items-center'>
<div className='flex flex-row justify-between items-center mb-2'> <h2 className='text-base font-medium text-base-content/50 font-roboto'>
<div className='text-lg'>Anggaran ke-{index + 1}</div> Estimasi Anggaran Per Flock
</h2>
{formik.values.project_budgets.length > 1 && (
<Button <Button
type='button' type='button'
color='error' variant='ghost'
color='none'
onClick={() => onClick={() =>
onDeleteBudgetRowHandler( onDeleteBudgetRowHandler(
budget.nonstock_id as number, budget.nonstock_id as number,
index index
) )
} }
className='p-0 text-error'
> >
<Icon icon='mdi:trash' width={16} height={16} /> <Icon
icon='heroicons:trash'
width={20}
height={20}
/>
</Button> </Button>
)}
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<SelectInput <SelectInput
isClearable isClearable
label='Jenis Produk'
options={filteredNonStockOptions ?? []} options={filteredNonStockOptions ?? []}
isLoading={isLoadingNonstocks} isLoading={isLoadingNonstocks}
placeholder='Pilih barang non stock' placeholder='Pilih Jenis Produk'
value={formik.values.project_budgets[index].nonstock} value={
formik.values.project_budgets[index].nonstock
}
onInputChange={setInputValueNonstock} onInputChange={setInputValueNonstock}
onMenuScrollToBottom={loadMoreNonstock} onMenuScrollToBottom={loadMoreNonstock}
onChange={(val) => { onChange={(val) => {
const updatedBudgets = [ const updatedBudgets = [
...formik.values.project_budgets, ...formik.values.project_budgets,
]; ];
updatedBudgets[index].nonstock = val as OptionType; updatedBudgets[index].nonstock =
val as OptionType;
updatedBudgets[index].nonstock_id = updatedBudgets[index].nonstock_id =
(val as OptionType) (val as OptionType)
? (val as OptionType).value ? (val as OptionType).value
@@ -957,21 +1181,26 @@ const ProjectFlockForm = ({
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<NumberInput <NumberInput
name={`project_budgets[${index}].qty`} name={`project_budgets[${index}].qty`}
placeholder='Masukkan jumlah' label='Jumlah Pembelian'
placeholder='Masukkan Jumlah Pembelian'
value={formik.values.project_budgets[index].qty} value={formik.values.project_budgets[index].qty}
onChange={(e) => onChange={(e) =>
handleBudgetChange(index, 'qty', e.target.value) handleBudgetChange(index, 'qty', e.target.value)
} }
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
allowNegative={false} allowNegative={false}
endAdornment={ inputPrefix={
<div className='text-gray-500'> <>
{isResponseSuccess(nonstocks) {nonstockUomName && (
? (nonstocks.data.find( <div className='w-full h-full py-1 flex flex-row items-stretch justify-between gap-3'>
(ns) => ns.id === budget.nonstock_id <span className='text-sm text-base-content/60 self-center text-nowrap truncate'>
)?.uom?.name ?? '') {nonstockUomName}
: ''} </span>
<div className='w-px bg-base-content/10' />
</div> </div>
)}
</>
} }
errorMessage={ errorMessage={
( (
@@ -990,28 +1219,39 @@ const ProjectFlockForm = ({
)?.qty as string )?.qty as string
) )
} }
className={{
inputPrefix:
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
inputWrapper: cn('border-l-0 pl-0', {
'pl-5':
isResponseSuccess(nonstocks) &&
budget.nonstock_id,
}),
}}
/> />
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<NumberInput <NumberInput
name={`project_budgets[${index}].price`} name={`project_budgets[${index}].price`}
label='Harga Satuan (Rp)'
value={formik.values.project_budgets[index].price} value={formik.values.project_budgets[index].price}
onChange={(e) => onChange={(e) =>
handleBudgetChange(index, 'price', e.target.value) handleBudgetChange(
index,
'price',
e.target.value
)
} }
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
placeholder='Masukkan harga satuan' placeholder='Masukkan Harga Satuan (Rp)'
allowNegative={false} allowNegative={false}
startAdornment='Rp' inputPrefix={
endAdornment={ <div className='w-full h-full py-1 flex flex-row items-stretch justify-between gap-3'>
<div className='text-gray-500'> <span className='text-sm text-base-content/60 self-center text-nowrap truncate'>
{`Per ${ Rp
isResponseSuccess(nonstocks) </span>
? (nonstocks.data.find(
(ns) => ns.id === budget.nonstock_id <div className='w-px bg-base-content/10' />
)?.uom?.name ?? 'Item')
: 'Item'
}`}
</div> </div>
} }
errorMessage={ errorMessage={
@@ -1022,7 +1262,8 @@ const ProjectFlockForm = ({
)?.price as string )?.price as string
} }
isError={ isError={
formik.touched.project_budgets?.[index]?.price && formik.touched.project_budgets?.[index]
?.price &&
Boolean( Boolean(
( (
formik.errors.project_budgets?.[ formik.errors.project_budgets?.[
@@ -1031,11 +1272,17 @@ const ProjectFlockForm = ({
)?.price as string )?.price as string
) )
} }
className={{
inputPrefix:
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
inputWrapper: 'border-l-0 pl-5',
}}
/> />
</div> </div>
<div className='flex flex-row justify-between items-center'> <div className='flex flex-row justify-between items-center'>
<NumberInput <NumberInput
name={`project_budgets[${index}].total_price`} name={`project_budgets[${index}].total_price`}
label='Total Harga'
value={ value={
formik.values.project_budgets[index].total_price formik.values.project_budgets[index].total_price
} }
@@ -1047,11 +1294,16 @@ const ProjectFlockForm = ({
) )
} }
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
placeholder='Masukkan harga total' placeholder='Masukkan Total Harga'
allowNegative={false} allowNegative={false}
startAdornment='Rp' inputPrefix={
endAdornment={ <div className='w-full h-full py-1 flex flex-row items-stretch justify-between gap-3'>
<div className='text-gray-500'>Total</div> <span className='text-sm text-base-content/60 self-center text-nowrap truncate'>
Rp
</span>
<div className='w-px bg-base-content/10' />
</div>
} }
errorMessage={ errorMessage={
( (
@@ -1071,31 +1323,85 @@ const ProjectFlockForm = ({
)?.total_price as string )?.total_price as string
) )
} }
className={{
inputPrefix:
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
inputWrapper: 'border-l-0 pl-5',
}}
/> />
</div> </div>
</div> </div>
</Card> );
)) })
) : ( ) : (
<div className='text-center py-4 text-gray-400'> <div className='text-center py-4 text-gray-400'>
Tidak ada data estimasi anggaran. Tidak ada data estimasi anggaran.
</div> </div>
)} )}
<div className='w-full p-4'>
<Button <Button
type='button' type='button'
color='primary'
variant='outline'
onClick={onAddBudgetRowHandler} onClick={onAddBudgetRowHandler}
disabled={filteredNonStockOptions.length == 0} disabled={filteredNonStockOptions.length == 0}
color='success' className='w-full p-3 rounded-lg text-sm font-semibold hover:text-base-100 focus-visible:text-base-100'
className='w-fit self-center'
> >
<Icon icon='mdi:plus' width={16} height={16} /> Add Budget Tambah Produk
</Button> </Button>
</div> </div>
</div> </div>
</div>
<AlertErrorList formErrorList={formErrorList} onClose={close} /> {(formErrorList.length > 0 || projectFlockFormErrorMessage) && (
<div className='p-4'>
<AlertErrorList
formErrorList={formErrorList}
onClose={close}
/>
<div className='flex flex-row justify-center gap-2 flex-wrap my-6 px-4'> {projectFlockFormErrorMessage && (
<div className='my-4'>
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{projectFlockFormErrorMessage}</span>
<Button
type='button'
onClick={() => {
setProjectFlockFormErrorMessage('');
}}
variant='link'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
</Button>
</div>
</div>
)}
</div>
)}
</div>
)}
{formStep === 'confirmation' && (
<div className='p-4'>
<ProjectFlockFormConfirmationTable
projectFlockForm={formik.values}
kandangs={optionsKandang}
/>
</div>
)}
</div>
<div className='p-4 flex-1 flex flex-row justify-center gap-2 flex-wrap shadow-bg'>
{formType !== 'detail' && ( {formType !== 'detail' && (
<RequirePermission <RequirePermission
permissions={ permissions={
@@ -1106,19 +1412,29 @@ const ProjectFlockForm = ({
> >
<Button <Button
type='submit' type='submit'
color='primary'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={formik.isSubmitting} disabled={formik.isSubmitting}
className='px-4 w-full' className='w-full p-3 rounded-lg text-sm font-semibold text-base-100'
> >
<Icon icon='mdi:plus' width={24} height={24} /> Submit
{formType == 'add' ? 'Add Flock' : 'Update Flock'}
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
</div> </div>
</form> </form>
</section>
<ProjectFlockConfirmationModal
ref={successModal.ref}
type='success'
text='Data Berhasil Ditambahkan'
subtitleText='Data project flock telah berhasil disimpan.'
projectFlockForm={formikLastValues}
onClose={() => {
router.push('/production/project-flock');
setFormikLastValues(undefined);
}}
secondaryButton={undefined}
/>
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -1133,6 +1449,9 @@ const ProjectFlockForm = ({
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
className={{
modal: 'w-full sm:w-[446px]',
}}
/> />
</> </>
); );
@@ -2,6 +2,7 @@
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import Card from '@/components/Card'; import Card from '@/components/Card';
import StatusBadge from '@/components/helper/StatusBadge';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import PillBadge from '@/components/PillBadge'; import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table'; import Table from '@/components/Table';
@@ -32,6 +33,14 @@ const ProjectFlockKandangTable = ({
initialValues?: ProjectFlock; initialValues?: ProjectFlock;
formType: 'add' | 'edit' | 'detail'; formType: 'add' | 'edit' | 'detail';
}) => { }) => {
const availableKandang = listKandang.filter(
(kandang) => kandang.status == 'NON_ACTIVE'
).length;
const unavailableKandang = listKandang.filter(
(kandang) => kandang.status != 'NON_ACTIVE'
).length;
// Fungsi untuk menangani perubahan checkbox // Fungsi untuk menangani perubahan checkbox
const handleCheckboxChange = (kandang: Kandang, isChecked: boolean) => { const handleCheckboxChange = (kandang: Kandang, isChecked: boolean) => {
// Hanya izinkan perubahan jika tidak dalam mode 'detail' // Hanya izinkan perubahan jika tidak dalam mode 'detail'
@@ -57,48 +66,30 @@ const ProjectFlockKandangTable = ({
{listKandang.length > 0 ? ( {listKandang.length > 0 ? (
<> <>
{/* ... Bagian Badge Status ... */} {/* ... Bagian Badge Status ... */}
<div className='flex flex-row mb-4'> <div className='w-fit flex flex-row items-stretch gap-3 mb-3'>
<Badge <StatusBadge
variant='soft' color='info'
color='primary' text={`Tersedia (${availableKandang})`}
className={{ className={{ badge: 'text-nowrap' }}
badge: 'rounded-lg px-2', />
}}
> <div className='w-px border-none bg-base-content/10' />
<Icon icon='mdi:circle' width={12} height={12} />
Tersedia ( <StatusBadge
{
listKandang.filter((kandang) => kandang.status == 'NON_ACTIVE')
.length
}
)
</Badge>
<div className='divider divider-horizontal mx-1'></div>
<Badge
variant='soft'
color='neutral' color='neutral'
className={{ text={`Tidak Tersedia (${unavailableKandang})`}
badge: 'rounded-lg px-2', className={{ badge: 'text-nowrap' }}
}} />
>
<Icon icon='mdi:circle' width={12} height={12} />
Tidak Tersedia (
{
listKandang.filter((kandang) => kandang.status != 'NON_ACTIVE')
.length
}
)
</Badge>
</div> </div>
{/* --- */} {/* --- */}
<Card <Card
variant='bordered' variant='bordered'
className={{ className={{
wrapper: 'w-full rounded-lg', wrapper: 'w-full rounded-xl border border-base-content/5',
body: 'p-4', body: 'p-0',
}} }}
> >
<div className='flex flex-col gap-4 w-full'> <div className='flex flex-col w-full'>
{listKandang.map((kandang, index) => { {listKandang.map((kandang, index) => {
const kandangIdString = const kandangIdString =
kandang.id?.toString() ?? `temp-${index}`; kandang.id?.toString() ?? `temp-${index}`;
@@ -112,28 +103,36 @@ const ProjectFlockKandangTable = ({
formType == 'detail' || kandang.status != 'NON_ACTIVE'; formType == 'detail' || kandang.status != 'NON_ACTIVE';
return ( return (
<div key={index} className='flex flex-row justify-between'> <div
key={index}
className='w-full p-3 flex flex-row items-center justify-between'
>
<CheckboxInput <CheckboxInput
name={`kandang-${kandang.id}`} // Nama unik untuk setiap checkbox name={`kandang-${kandang.id}`} // Nama unik untuk setiap checkbox
label={kandang.name} label={kandang.name}
checked={isSelected} checked={isSelected}
disabled={isDisabled} disabled={isDisabled}
size='md'
onChange={(e) => onChange={(e) =>
handleCheckboxChange(kandang, e.currentTarget.checked) handleCheckboxChange(kandang, e.currentTarget.checked)
} }
/> classNames={{
<Badge inputWrapper: cn('gap-3 text-base-content/50', {
variant='soft' 'text-base-content/20': isDisabled,
color={ }),
kandang.status == 'NON_ACTIVE' ? 'primary' : 'neutral' label: 'cursor-pointer',
} checkbox: cn({
className={{ 'bg-base-200 border border-base-content/10 opacity-100':
badge: 'rounded-lg px-2', isDisabled,
}),
}} }}
> />
<Icon icon='mdi:circle' width={12} height={12} />
{kandang.status != 'NON_ACTIVE' && 'Tidak'} Tersedia <StatusBadge
</Badge> color={!isDisabled ? 'info' : 'neutral'}
text={!isDisabled ? 'Tersedia' : 'Tidak Tersedia'}
className={{ badge: 'w-fit' }}
/>
</div> </div>
); );
})} })}
+18
View File
@@ -446,6 +446,24 @@ export const APPROVAL_WORKFLOWS = {
step_number: 2, step_number: 2,
step_name: 'Aktif', step_name: 'Aktif',
}, },
{
step_number: 3,
step_name: 'Selesai',
},
],
PROJECT_FLOCK_KANDANGS: [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Disetujui',
},
{
step_number: 3,
step_name: 'Selesai',
},
], ],
RECORDINGS: [ RECORDINGS: [
{ {
@@ -5,7 +5,7 @@ import {
ClosingProjectFlockKandangPayload, ClosingProjectFlockKandangPayload,
CheckClosingResponse, CheckClosingResponse,
} from '@/types/api/production/project-flock-kandang'; } from '@/types/api/production/project-flock-kandang';
import { BaseApiResponse } from '@/types/api/api-general'; import { Approvals, BaseApiResponse } from '@/types/api/api-general';
import { httpClient } from '@/services/http/client'; import { httpClient } from '@/services/http/client';
import axios from 'axios'; import axios from 'axios';
@@ -181,6 +181,33 @@ export class ProjectFlockKandangService extends BaseApiService<
return undefined; return undefined;
} }
} }
async getApprovalLineHistory(
id: number,
page: number = 1,
limit: number = 100
) {
try {
const approvalHistoryRes = await httpClient<Approvals>('/approvals', {
query: {
module_name: 'PROJECT_FLOCK_KANDANGS',
module_id: id,
group_step_number: 'false',
page,
limit,
order_by_date: 'ASC',
},
});
return approvalHistoryRes;
} catch (error) {
if (axios.isAxiosError<Approvals>(error)) {
return error.response?.data;
}
return undefined;
}
}
} }
export const ProjectFlockKandangApi = new ProjectFlockKandangService( export const ProjectFlockKandangApi = new ProjectFlockKandangService(
@@ -5,6 +5,7 @@ import {
} from '@/types/api/production/project-flock'; } from '@/types/api/production/project-flock';
import { BaseApiService } from '@/services/api/base'; import { BaseApiService } from '@/services/api/base';
import { import {
Approvals,
BaseApiResponse, BaseApiResponse,
BaseGroupedApproval, BaseGroupedApproval,
ErrorApiResponse, ErrorApiResponse,
@@ -53,6 +54,33 @@ export class ProjectFlockService extends BaseApiService<
} }
} }
async getApprovalLineHistory(
id: number,
page: number = 1,
limit: number = 100
) {
try {
const approvalHistoryRes = await httpClient<Approvals>('/approvals', {
query: {
module_name: 'PROJECT_FLOCKS',
module_id: id,
group_step_number: 'false',
page,
limit,
order_by_date: 'ASC',
},
});
return approvalHistoryRes;
} catch (error) {
if (axios.isAxiosError<Approvals>(error)) {
return error.response?.data;
}
return undefined;
}
}
/** /**
* Lookup for Project Flock Kandang * Lookup for Project Flock Kandang
*/ */