Merge branch 'fix/uniformity-ttl-project-flock-issue' into 'development'

[FIX/FE] Fix Isssue on Uniformity (Week Calculation), Project Flock (Modal Position), Transfer To Laying (Copywriting)

See merge request mbugroup/lti-web-client!329
This commit is contained in:
Rivaldi A N S
2026-02-20 03:36:15 +00:00
7 changed files with 237 additions and 90 deletions
@@ -37,6 +37,7 @@ import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal';
import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store';
import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema';
import { useChickinStore } from '@/stores/production/chickin/chickin.store'; import { useChickinStore } from '@/stores/production/chickin/chickin.store';
import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
props, props,
@@ -195,6 +196,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const confirmModal = useModal(); const confirmModal = useModal();
const successModal = useModal(); const successModal = useModal();
const chickinApproveModal = useModal(); const chickinApproveModal = useModal();
const closingModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED' 'APPROVED'
); );
@@ -210,6 +212,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setChickinApproveLoading, setChickinApproveLoading,
} = useChickinStore(); } = useChickinStore();
const {
isClosingModalOpen,
isKandangClosed,
isClosingLoading,
closingCallback,
closeClosingModal,
setClosingLoading,
} = useProjectFlockClosingStore();
// ===== Fetch Data ===== // ===== Fetch Data =====
const { const {
data: projectFlocks, data: projectFlocks,
@@ -309,6 +320,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
} }
}, [isChickinApproveModalOpen, chickinApproveModal]); }, [isChickinApproveModalOpen, chickinApproveModal]);
useEffect(() => {
if (isClosingModalOpen) {
closingModal.openModal();
} else {
closingModal.closeModal();
}
}, [isClosingModalOpen, closingModal]);
useEffect(() => { useEffect(() => {
if (isSuccess) { if (isSuccess) {
successModal.openModal(); successModal.openModal();
@@ -1025,6 +1044,45 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
isLoading: isChickinApproveLoading, isLoading: isChickinApproveLoading,
}} }}
/> />
{/* Project Flock Closing Modal */}
<ConfirmationModal
ref={closingModal.ref}
type='error'
text={
!isKandangClosed
? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai'
: 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif'
}
className={{
modal: 'z-9999',
}}
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeClosingModal();
closingModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isClosingLoading,
onClick: async () => {
if (closingCallback) {
setClosingLoading(true);
try {
await closingCallback(!isKandangClosed ? 'close' : 'unclose');
} finally {
setClosingLoading(false);
closeClosingModal();
closingModal.closeModal();
refreshProjectFlocks();
}
}
},
}}
/>
</> </>
); );
}; };
@@ -17,9 +17,8 @@ import { Icon } from '@iconify/react';
import useSWR from 'swr'; import useSWR from 'swr';
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang'; import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useModal } from '@/components/Modal'; import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { ApprovalApi } from '@/services/api/approval'; import { ApprovalApi } from '@/services/api/approval';
@@ -53,9 +52,8 @@ const ProjectFlockClosingForm = ({
projectFlockKandang: ProjectFlockKandang; projectFlockKandang: ProjectFlockKandang;
}) => { }) => {
const router = useRouter(); const router = useRouter();
const closeModal = useModal();
const [isClosingLoading, setIsClosingLoading] = useState(false); const { openClosingModal } = useProjectFlockClosingStore();
const { data: closingData, isLoading } = useSWR( const { data: closingData, isLoading } = useSWR(
`${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`, `${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`,
@@ -80,29 +78,34 @@ const ProjectFlockClosingForm = ({
: true; : true;
}, [projectFlockKandangApprovals]); }, [projectFlockKandangApprovals]);
const confirmationModalCloseClickHandler = async () => { const handleCloseClick = () => {
setIsClosingLoading(true); const closingCallback = async (action: 'close' | 'unclose') => {
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
projectFlockKandang?.id as number, projectFlockKandang?.id as number,
{ {
closed_date: !isKandangClosed closed_date:
? formatDate(new Date(), 'YYYY-MM-DD') action === 'close' ? formatDate(new Date(), 'YYYY-MM-DD') : '',
: '', action,
action: !isKandangClosed ? 'close' : 'unclose', }
}
);
if (isResponseSuccess(deleteProjectFlockRes)) {
toast.success(deleteProjectFlockRes?.message as string);
router.push(
`/production/project-flock/detail?projectFlockId=${projectFlock.id}`
); );
}
if (isResponseError(deleteProjectFlockRes)) { if (isResponseSuccess(deleteProjectFlockRes)) {
toast.error(deleteProjectFlockRes?.message as string); toast.success(deleteProjectFlockRes?.message as string);
} router.push(
setIsClosingLoading(false); `/production/project-flock/detail?projectFlockId=${projectFlock.id}`
closeModal.closeModal(); );
}
if (isResponseError(deleteProjectFlockRes)) {
toast.error(deleteProjectFlockRes?.message as string);
}
};
openClosingModal(
projectFlockKandang,
projectFlock.id,
isKandangClosed,
closingCallback
);
}; };
// const errorStock = useMemo(() => { // const errorStock = useMemo(() => {
@@ -334,7 +337,7 @@ const ProjectFlockClosingForm = ({
color='error' color='error'
isLoading={isLoading} isLoading={isLoading}
disabled={!isCanCloseValid} disabled={!isCanCloseValid}
onClick={() => closeModal.openModal()} onClick={handleCloseClick}
> >
<Icon <Icon
icon={ icon={
@@ -347,25 +350,6 @@ const ProjectFlockClosingForm = ({
</Button> </Button>
</RequirePermission> </RequirePermission>
</div> </div>
<ConfirmationModal
ref={closeModal.ref}
type='error'
text={
!isKandangClosed
? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai'
: 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif'
}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isClosingLoading,
onClick: confirmationModalCloseClickHandler,
}}
/>
</div> </div>
</> </>
); );
@@ -83,6 +83,9 @@ const TransferToLayingFormModal = () => {
TransferToLayingFormValues | undefined TransferToLayingFormValues | undefined
>(undefined); >(undefined);
const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null); const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null);
const [submittedActionType, setSubmittedActionType] = useState<
'add' | 'edit' | null
>(null);
// Flock Source // Flock Source
const { const {
@@ -203,6 +206,7 @@ const TransferToLayingFormModal = () => {
}; };
setFormikLastValues(values); setFormikLastValues(values);
setSubmittedActionType(modalAction as 'add' | 'edit');
switch (modalAction) { switch (modalAction) {
case 'add': case 'add':
@@ -1059,10 +1063,21 @@ const TransferToLayingFormModal = () => {
<TransferToLayingConfirmationModal <TransferToLayingConfirmationModal
ref={successModal.ref} ref={successModal.ref}
type='success' type='success'
text='Data Berhasil Ditambahkan' text={
subtitleText='Data transfer to laying telah berhasil disimpan.' submittedActionType === 'edit'
? 'Data Berhasil Diperbarui'
: 'Data Berhasil Ditambahkan'
}
subtitleText={
submittedActionType === 'edit'
? 'Data transfer to laying telah berhasil diperbarui.'
: 'Data transfer to laying telah berhasil disimpan.'
}
transferToLayingForm={formikLastValues} transferToLayingForm={formikLastValues}
onClose={() => setFormikLastValues(undefined)} onClose={() => {
setFormikLastValues(undefined);
setSubmittedActionType(null);
}}
secondaryButton={undefined} secondaryButton={undefined}
/> />
</> </>
@@ -680,6 +680,7 @@ const TransferToLayingsTable = () => {
subtitleText='Are you sure you want to delete this data? ' subtitleText='Are you sure you want to delete this data? '
transferToLayingIds={selectedRowIds} transferToLayingIds={selectedRowIds}
primaryButton={{ primaryButton={{
text: 'Delete',
isLoading: isDeleteLoading, isLoading: isDeleteLoading,
color: 'error', color: 'error',
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
@@ -704,6 +705,7 @@ const TransferToLayingsTable = () => {
withNote withNote
noteLabel='Notes Approval' noteLabel='Notes Approval'
primaryButton={{ primaryButton={{
text: 'Approve',
isLoading: isApproveLoading, isLoading: isApproveLoading,
onClick: confirmationModalApproveClickHandler, onClick: confirmationModalApproveClickHandler,
}} }}
@@ -735,6 +737,7 @@ const TransferToLayingsTable = () => {
}, },
}} }}
primaryButton={{ primaryButton={{
text: 'Reject',
isLoading: isRejectLoading, isLoading: isRejectLoading,
color: 'error', color: 'error',
onClick: confirmationModalRejectClickHandler, onClick: confirmationModalRejectClickHandler,
@@ -203,16 +203,19 @@ const UniformityForm = ({
// ===== RECORDINGS DATA (FOR WEEK CALCULATION) ===== // ===== RECORDINGS DATA (FOR WEEK CALCULATION) =====
const recordingsUrl = useMemo(() => { const recordingsUrl = useMemo(() => {
if (!projectFlockKandangLookup?.project_flock_kandang_id) return null;
const params = new URLSearchParams({ const params = new URLSearchParams({
page: '1', page: '1',
limit: '100', limit: '100',
project_flock_kandang_id:
projectFlockKandangLookup.project_flock_kandang_id.toString(),
}); });
return `${RecordingApi.basePath}?${params.toString()}`; return `${RecordingApi.basePath}?${params.toString()}`;
}, []); }, [projectFlockKandangLookup?.project_flock_kandang_id]);
const { data: recordingsData } = useSWR( const { data: recordingsData } = useSWR(
recordingsUrl, recordingsUrl,
RecordingApi.getAllFetcher recordingsUrl ? RecordingApi.getAllFetcher : null
); );
// ===== FORM CONFIGURATION ===== // ===== FORM CONFIGURATION =====
@@ -400,50 +403,46 @@ const UniformityForm = ({
useEffect(() => { useEffect(() => {
if ( if (
projectFlockKandangLookup?.chick_in_date && projectFlockKandangLookup?.chick_in_date &&
projectFlockKandangLookup?.project_flock_kandang_id && projectFlockKandangLookup?.project_flock_kandang_id
isResponseSuccess(recordingsData) &&
recordingsData.data
) { ) {
const matchingRecordings = recordingsData.data.filter( const chickInDate = new Date(projectFlockKandangLookup.chick_in_date);
(recording: Recording) => chickInDate.setHours(0, 0, 0, 0);
recording.project_flock?.project_flock_kandang_id ===
projectFlockKandangLookup.project_flock_kandang_id
);
matchingRecordings.sort( let initialWeek = 18;
(a: Recording, b: Recording) =>
new Date(a.record_datetime).getTime() -
new Date(b.record_datetime).getTime()
);
const earliestRecording = matchingRecordings[0]; if (
isResponseSuccess(recordingsData) &&
recordingsData.data &&
recordingsData.data.length > 0
) {
const sortedRecordings = [...recordingsData.data].sort(
(a: Recording, b: Recording) =>
new Date(a.record_datetime).getTime() -
new Date(b.record_datetime).getTime()
);
if (earliestRecording) { const earliestRecording = sortedRecordings[0];
const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); if (earliestRecording?.project_flock?.production_standart?.week) {
chickInDate.setHours(0, 0, 0, 0); initialWeek =
earliestRecording.project_flock.production_standart.week;
const earliestRecordDate = new Date(earliestRecording.record_datetime);
earliestRecordDate.setHours(0, 0, 0, 0);
const initialWeek =
earliestRecording.project_flock?.production_standart?.week || 18;
if (formik.values.date) {
const selectedDate = new Date(formik.values.date);
selectedDate.setHours(0, 0, 0, 0);
const daysDiff = Math.floor(
(selectedDate.getTime() - chickInDate.getTime()) /
(1000 * 60 * 60 * 24)
);
const weeksDiff = Math.floor(daysDiff / 7);
formik.setFieldValue('week', initialWeek + weeksDiff);
} else {
formik.setFieldValue('week', initialWeek);
} }
} }
if (formik.values.date) {
const selectedDate = new Date(formik.values.date);
selectedDate.setHours(0, 0, 0, 0);
const daysDiff = Math.floor(
(selectedDate.getTime() - chickInDate.getTime()) /
(1000 * 60 * 60 * 24)
);
const weeksDiff = Math.floor(daysDiff / 7);
formik.setFieldValue('week', initialWeek + weeksDiff);
} else {
formik.setFieldValue('week', initialWeek);
}
} }
}, [ }, [
projectFlockKandangLookup?.chick_in_date, projectFlockKandangLookup?.chick_in_date,
@@ -0,0 +1,19 @@
'use client';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { createProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice';
import { ProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice';
export type ProjectFlockClosingStore = ProjectFlockClosingSlice;
export const useProjectFlockClosingStore = create<ProjectFlockClosingStore>()(
devtools(
(...args) => ({
...createProjectFlockClosingSlice(...args),
}),
{
name: 'ProjectFlockClosingStore',
}
)
);
@@ -0,0 +1,69 @@
import { StateCreator } from 'zustand';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
export type ProjectFlockClosingSlice = {
// State
isClosingModalOpen: boolean;
selectedProjectFlockKandang: ProjectFlockKandang | null;
projectFlockId: number | null;
isKandangClosed: boolean;
isClosingLoading: boolean;
closingCallback: ((action: 'close' | 'unclose') => Promise<void>) | null;
// Actions
openClosingModal: (
data: ProjectFlockKandang,
projectFlockId: number,
isClosed: boolean,
callback: (action: 'close' | 'unclose') => Promise<void>
) => void;
closeClosingModal: () => void;
setClosingLoading: (loading: boolean) => void;
resetClosing: () => void;
};
export const createProjectFlockClosingSlice: StateCreator<
ProjectFlockClosingSlice,
[],
[],
ProjectFlockClosingSlice
> = (set) => ({
// Initial state
isClosingModalOpen: false,
selectedProjectFlockKandang: null,
projectFlockId: null,
isKandangClosed: false,
isClosingLoading: false,
closingCallback: null,
// Actions
openClosingModal: (data, projectFlockId, isClosed, callback) =>
set({
isClosingModalOpen: true,
selectedProjectFlockKandang: data,
projectFlockId,
isKandangClosed: isClosed,
closingCallback: callback,
}),
closeClosingModal: () =>
set({
isClosingModalOpen: false,
selectedProjectFlockKandang: null,
projectFlockId: null,
isKandangClosed: false,
closingCallback: null,
}),
setClosingLoading: (loading) => set({ isClosingLoading: loading }),
resetClosing: () =>
set({
isClosingModalOpen: false,
selectedProjectFlockKandang: null,
projectFlockId: null,
isKandangClosed: false,
isClosingLoading: false,
closingCallback: null,
}),
});