fix(FE): closing project flock & merge development

This commit is contained in:
randy-ar
2025-12-11 00:32:54 +07:00
28 changed files with 434 additions and 2176 deletions
+1
View File
@@ -3,6 +3,7 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'export', output: 'export',
images: { unoptimized: true }, images: { unoptimized: true },
trailingSlash: true,
}; };
export default nextConfig; export default nextConfig;
+1
View File
@@ -7,4 +7,5 @@ const Marketing = () => {
</div> </div>
); );
}; };
export default Marketing; export default Marketing;
+19 -8
View File
@@ -1,18 +1,29 @@
'use client'; 'use client';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useAuth } from '@/services/hooks/useAuth';
import { redirectToSSO } from '@/lib/auth-helper';
export default function Home() { export default function Home() {
const { user, isLoadingUser } = useAuth();
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
useEffect(() => { useEffect(() => {
router.replace('/dashboard'); if (pathname === '/') {
}, [router]); router.replace('/dashboard');
}
}, [pathname]);
return ( if (isLoadingUser) {
<main className='w-full h-full min-h-screen flex flex-row justify-center items-center'> return (
<span className='loading loading-spinner loading-lg'></span> <main className='w-full h-full min-h-screen flex flex-row justify-center items-center'>
</main> <span className='loading loading-spinner loading-lg'></span>
); </main>
);
}
return <>Loading...</>;
} }
+1 -1
View File
@@ -15,7 +15,7 @@ export default function ProjectFlockLayout({
const router = useRouter(); const router = useRouter();
const toggleValidate = useUiStore((s) => s.toggleValidate); const toggleValidate = useUiStore((s) => s.toggleValidate);
const isAdd = pathname.endsWith('/add'); const isAdd = pathname.includes('/add');
const isEdit = pathname.includes('/detail/edit'); const isEdit = pathname.includes('/detail/edit');
const isDetail = pathname.includes('/detail'); const isDetail = pathname.includes('/detail');
const isChickin = pathname.includes('/chickin/add/kandang'); const isChickin = pathname.includes('/chickin/add/kandang');
@@ -1,49 +0,0 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const AddGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recording_id');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId && recordingId !== 'new' ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (
recordingId &&
recordingId !== 'new' &&
!isLoadingRecording &&
(!recording || !isResponseSuccess(recording))
) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{recordingId && recordingId !== 'new' && isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{(!recordingId ||
recordingId === 'new' ||
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
<GradingForm
type='add'
initialValues={
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
}
/>
)}
</div>
);
};
export default AddGrading;
@@ -1,53 +0,0 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const EditGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const gradingId = searchParams.get('gradingId');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
<GradingForm
type='edit'
initialValues={recording.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId || '0')
)}
/>
)}
</div>
);
};
export default EditGrading;
@@ -1,52 +0,0 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const DetailGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const gradingId = searchParams.get('gradingId');
const { data: grading, isLoading: isLoadingGrading } = useSWR(
gradingId ? [gradingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!gradingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingGrading && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
<GradingForm
type='detail'
initialValues={grading.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId)
)}
/>
)}
</div>
);
};
export default DetailGrading;
@@ -1,11 +0,0 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+64 -174
View File
@@ -1,197 +1,87 @@
'use client'; 'use client';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import useSWR from 'swr';
import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general'; import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// TODO: delete this later, DONT HARDCODE USER DATA import { redirectToSSO } from '@/lib/auth-helper';
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
} }
const RequireAuth = ({ children }: RequireAuthProps) => { const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter(); const { user, setUser, setIsLoadingUser } = useAuth();
const { setUser, setIsLoadingUser } = useAuth();
const { data: userResponse, isLoading: isLoadingUserResponse } = const {
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>( data: userResponse,
'/auth/sso/userinfo', isLoading: isLoadingUserResponse,
httpClientFetcher, error: userErrorResponse,
{ } = useSWR<
shouldRetryOnError: false, GetMeResponse & { ok?: boolean },
revalidateOnFocus: false, AxiosError<BaseApiResponse>,
revalidateOnReconnect: false, SWRHttpKey
refreshInterval: 0, >('/sso/userinfo', httpClientFetcher, {
} shouldRetryOnError: false,
); });
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
} }
}, [userResponse, setIsLoadingUser, setUser]); }, [userResponse, setUser]);
// TODO: uncomment this later // Explicitly handle 401 redirect from the component level
// if (isLoadingUserResponse && !userResponse) { useEffect(() => {
// return ( if (
// <div className='w-full flex flex-row justify-center items-center p-4'> isResponseError(userResponse) &&
// <span className='loading loading-spinner loading-xl' /> userErrorResponse?.response?.status === 401
// </div> ) {
// ); // Clear cache to prevent stale data from rendering children
// } // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate
setUser(undefined);
redirectToSSO();
}
}, [userErrorResponse, setUser, userResponse]);
return <>{children}</>; useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse]);
if (
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
(!userResponse && !userErrorResponse)
) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (userErrorResponse) {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
<p className='text-gray-600'>
Please try refreshing the page or contact support if the problem
persists.
</p>
<button
className='btn btn-primary'
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}
return <>{isResponseSuccess(userResponse) && user && children}</>;
}; };
export default RequireAuth; export default RequireAuth;
@@ -9,6 +9,7 @@ import { ProjectFlock } from '@/types/api/production/project-flock';
import { import {
ClosingExpense, ClosingExpense,
ProjectFlockKandang, ProjectFlockKandang,
StockItem,
} from '@/types/api/production/project-flock-kandang'; } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -41,7 +42,7 @@ const ProjectFlockClosingForm = ({
const confirmationModalCloseClickHandler = async () => { const confirmationModalCloseClickHandler = async () => {
setIsClosingLoading(true); setIsClosingLoading(true);
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
projectFlock?.id as number, projectFlockKandang?.id as number,
{ {
closed_date: formatDate(new Date(), 'YYYY-MM-DD'), closed_date: formatDate(new Date(), 'YYYY-MM-DD'),
action: isCanClose ? 'close' : 'unclose', action: isCanClose ? 'close' : 'unclose',
@@ -222,7 +223,7 @@ const ProjectFlockClosingForm = ({
<div className='divider'></div> <div className='divider'></div>
<div className='px-4 pb-4'> <div className='px-4 pb-4'>
<h2 className='text-2xl font-semibold'>Persediaan Gudang</h2> <h2 className='text-2xl font-semibold'>Persediaan Gudang</h2>
<Table<ProductWarehouse> <Table<StockItem>
data={ data={
isResponseSuccess(closingData) isResponseSuccess(closingData)
? closingData.data?.stock_remaining ? closingData.data?.stock_remaining
@@ -231,11 +232,11 @@ const ProjectFlockClosingForm = ({
columns={[ columns={[
{ {
header: 'Product', header: 'Product',
accessorKey: 'product.name', accessorKey: 'product_name',
}, },
{ {
header: 'Kategori', header: 'Kategori',
accessorKey: 'product.product_category.name', accessorKey: 'product_category',
}, },
{ {
header: 'Quantity', header: 'Quantity',
@@ -243,7 +244,7 @@ const ProjectFlockClosingForm = ({
}, },
{ {
header: 'UOM', header: 'UOM',
accessorKey: 'product.uom.name', accessorKey: 'uom',
}, },
]} ]}
className={{ className={{
@@ -35,28 +35,22 @@ const RowOptionsMenu = ({
deleteClickHandler, deleteClickHandler,
approveClickHandler, approveClickHandler,
rejectClickHandler, rejectClickHandler,
isGradingCompleted,
}: { }: {
type: 'dropdown' | 'collapse'; type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>; props: CellContext<Recording, unknown>;
deleteClickHandler: () => void; deleteClickHandler: () => void;
approveClickHandler: () => void; approveClickHandler: () => void;
rejectClickHandler: () => void; rejectClickHandler: () => void;
isGradingCompleted: (recording: Recording) => boolean;
}) => { }) => {
const isLayingCategory =
props.row.original.project_flock_category === 'LAYING';
const isRecordingApproved = (recording: Recording) => { const isRecordingApproved = (recording: Recording) => {
return ( return (
recording.approval?.action === 'APPROVED' && recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui' && recording.approval?.step_number === 2 &&
recording.approval?.step_number === 3 recording.approval?.step_name === 'Disetujui'
); );
}; };
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isGradingDone = isGradingCompleted(props.row.original);
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
@@ -78,7 +72,7 @@ const RowOptionsMenu = ({
<Icon icon='mdi:pencil-outline' width={16} height={16} /> <Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit Edit
</Button> </Button>
{!isApproved && !(isLayingCategory && !isGradingDone) && ( {!isApproved && (
<Button <Button
onClick={approveClickHandler} onClick={approveClickHandler}
variant='ghost' variant='ghost'
@@ -89,7 +83,7 @@ const RowOptionsMenu = ({
Approve Approve
</Button> </Button>
)} )}
{!isApproved && !(isLayingCategory && !isGradingDone) && ( {!isApproved && (
<Button <Button
onClick={rejectClickHandler} onClick={rejectClickHandler}
variant='ghost' variant='ghost'
@@ -386,33 +380,10 @@ const RecordingTable = () => {
RecordingApi.getAllFetcher RecordingApi.getAllFetcher
); );
const isRecordingFullyApproved = useCallback( const isRecordingApproved = useCallback((recording: Recording): boolean => {
(recording: Recording): boolean => {
return (
recording.approval?.action === 'APPROVED' &&
recording.approval?.step_name === 'Disetujui' &&
Number(recording.approval?.step_number) === 3
);
},
[]
);
const isRecordingApproved = useCallback(
(recording: Recording) => {
return isRecordingFullyApproved(recording);
},
[isRecordingFullyApproved]
);
const isGradingCompleted = useCallback((recording: Recording): boolean => {
if (recording.project_flock_category !== 'LAYING') {
return true;
}
return ( return (
recording.egg_grading_status === 'COMPLETED' || recording.approval?.action === 'APPROVED' &&
(recording.approval?.action === 'UPDATED' && recording.approval?.step_name === 'Disetujui'
recording.approval?.step_number === 2)
); );
}, []); }, []);
@@ -506,19 +477,9 @@ const RecordingTable = () => {
if (!isResponseSuccess(recordings) || !recordings.data) return []; if (!isResponseSuccess(recordings) || !recordings.data) return [];
return selectedRowIds.filter((id) => { return selectedRowIds.filter((id) => {
const recording = recordings.data.find((r) => r.id === id); const recording = recordings.data.find((r) => r.id === id);
if (!recording || isRecordingApproved(recording)) return false; return recording && !isRecordingApproved(recording);
if (recording.project_flock_category === 'GROWING') {
return true;
}
if (recording.project_flock_category === 'LAYING') {
return isGradingCompleted(recording);
}
return false;
}); });
}, [selectedRowIds, recordings, isRecordingApproved, isGradingCompleted]); }, [selectedRowIds, recordings, isRecordingApproved]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(recordings) && recordings.data) { if (isResponseSuccess(recordings) && recordings.data) {
@@ -530,14 +491,7 @@ const RecordingTable = () => {
(r) => r.id === parseInt(rowId) (r) => r.id === parseInt(rowId)
); );
if (recording && !isRecordingApproved(recording)) { if (recording && !isRecordingApproved(recording)) {
if (recording.project_flock_category === 'GROWING') { newSelection[rowId] = true;
newSelection[rowId] = true;
} else if (
recording.project_flock_category === 'LAYING' &&
isGradingCompleted(recording)
) {
newSelection[rowId] = true;
}
} }
} }
}); });
@@ -548,13 +502,7 @@ const RecordingTable = () => {
setRowSelection(newSelection); setRowSelection(newSelection);
} }
} }
}, [ }, [recordings, rowSelection, isRecordingApproved, setRowSelection]);
recordings,
rowSelection,
isRecordingApproved,
isGradingCompleted,
setRowSelection,
]);
return ( return (
<div className='w-full p-0 sm:p-4'> <div className='w-full p-0 sm:p-4'>
@@ -562,7 +510,7 @@ const RecordingTable = () => {
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'> <div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'> <div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button <Button
href='recording/add' href='/production/recording/add'
variant='outline' variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
@@ -640,40 +588,28 @@ const RecordingTable = () => {
id: 'select', id: 'select',
header: ({ table }) => { header: ({ table }) => {
const allRows = table.getRowModel().rows; const allRows = table.getRowModel().rows;
const selectableRows = allRows.filter((row) => {
const selectableGrowingRows = allRows.filter((row) => {
const recording = row.original; const recording = row.original;
return ( return !isRecordingApproved(recording);
recording.project_flock_category === 'GROWING' &&
!isRecordingApproved(recording)
);
}); });
const hasNoSelectableGrowing = selectableGrowingRows.length === 0; const hasNoSelectableRows = selectableRows.length === 0;
const handleSelectAllGrowing = () => { const handleSelectAll = () => {
const isAllSelected = selectableGrowingRows.every((row) => const isAllSelected = selectableRows.every((row) =>
row.getIsSelected() row.getIsSelected()
); );
allRows.forEach((row) => { selectableRows.forEach((row) => {
const recording = row.original; row.toggleSelected(!isAllSelected);
if (
recording.project_flock_category === 'GROWING' &&
!isRecordingApproved(recording)
) {
row.toggleSelected(!isAllSelected);
} else if (recording.project_flock_category === 'LAYING') {
row.toggleSelected(false);
}
}); });
}; };
const isAllGrowingSelected = const isAllSelected =
selectableGrowingRows.length > 0 && selectableRows.length > 0 &&
selectableGrowingRows.every((row) => row.getIsSelected()); selectableRows.every((row) => row.getIsSelected());
const isSomeGrowingSelected = selectableGrowingRows.some((row) => const isSomeSelected = selectableRows.some((row) =>
row.getIsSelected() row.getIsSelected()
); );
@@ -681,33 +617,20 @@ const RecordingTable = () => {
<div className='w-full flex flex-row justify-center'> <div className='w-full flex flex-row justify-center'>
<CheckboxInput <CheckboxInput
name='allRow' name='allRow'
checked={isAllGrowingSelected} checked={isAllSelected}
indeterminate={ indeterminate={isSomeSelected && !isAllSelected}
isSomeGrowingSelected && !isAllGrowingSelected onChange={handleSelectAll}
} disabled={hasNoSelectableRows}
onChange={handleSelectAllGrowing}
disabled={hasNoSelectableGrowing}
/> />
</div> </div>
); );
}, },
cell: ({ row }) => { cell: ({ row }) => {
const isApproved = isRecordingApproved(row.original);
const isLayingCategory =
row.original.project_flock_category === 'LAYING';
if (isLayingCategory) {
return null;
}
const isDisabled = !row.getCanSelect() || isApproved;
return ( return (
<div> <div>
<CheckboxInput <CheckboxInput
name='row' name='row'
checked={row.getIsSelected()} checked={row.getIsSelected()}
disabled={isDisabled}
indeterminate={row.getIsSomeSelected()} indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()} onChange={row.getToggleSelectedHandler()}
/> />
@@ -883,7 +806,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/> />
</RowDropdownOptions> </RowDropdownOptions>
)} )}
@@ -896,7 +818,6 @@ const RecordingTable = () => {
deleteClickHandler={deleteClickHandler} deleteClickHandler={deleteClickHandler}
approveClickHandler={approveClickHandler} approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler} rejectClickHandler={rejectClickHandler}
isGradingCompleted={isGradingCompleted}
/> />
</RowCollapseOptions> </RowCollapseOptions>
)} )}
@@ -4,7 +4,6 @@ import {
CreateGrowingRecordingPayload, CreateGrowingRecordingPayload,
CreateLayingRecordingPayload, CreateLayingRecordingPayload,
CreateEggPayload, CreateEggPayload,
CreateGradingPayload,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
type RecordingGrowingFormSchemaType = { type RecordingGrowingFormSchemaType = {
@@ -32,14 +31,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
eggs: { eggs: {
product_warehouse_id: number; product_warehouse_id: number;
qty: number | string; qty: number | string;
}[]; weight: number | string;
};
type RecordingGradingFormSchemaType = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number | string;
}[]; }[];
}; };
@@ -62,6 +54,7 @@ export type DepletionSchema = {
export type EggSchema = { export type EggSchema = {
product_warehouse_id: number; product_warehouse_id: number;
qty: number | string; qty: number | string;
weight: number | string;
}; };
const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({ const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
@@ -109,6 +102,10 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
.required('Jumlah telur wajib diisi!') .required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!') .min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'), .typeError('Jumlah telur harus berupa angka!'),
weight: Yup.number()
.required('Berat telur wajib diisi!')
.min(1, 'Berat telur minimal 1 gram!')
.typeError('Berat telur harus berupa angka!'),
}); });
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> = export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
@@ -190,30 +187,6 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'), .required('Project Flock Kandang wajib diisi!'),
}); });
export const RecordingGradingFormSchema: Yup.ObjectSchema<RecordingGradingFormSchemaType> =
Yup.object({
eggs_grading: Yup.array()
.of(
Yup.object({
recording_egg_id: Yup.number()
.required('Recording Egg ID wajib diisi!')
.min(1, 'Recording Egg ID minimal 1!')
.typeError('Recording Egg ID harus berupa angka!'),
grade: Yup.string()
.required('Grade telur wajib diisi!')
.typeError('Grade telur harus berupa string!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur minimal 1!')
.typeError('Jumlah telur harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data grading telur!')
.required('Data grading telur wajib diisi!'),
});
export const UpdateRecordingGradingFormSchema = RecordingGradingFormSchema;
export type RecordingGrowingFormValues = Yup.InferType< export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema typeof RecordingGrowingFormSchema
>; >;
@@ -222,10 +195,6 @@ export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema typeof RecordingLayingFormSchema
>; >;
export type RecordingGradingFormValues = Yup.InferType<
typeof RecordingGradingFormSchema
>;
type RecordingFormData = Partial<Recording> & { type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights']; body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks']; stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
@@ -295,26 +264,12 @@ export const getRecordingLayingFormInitialValues = (
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({ eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
product_warehouse_id: egg.product_warehouse_id, product_warehouse_id: egg.product_warehouse_id,
qty: egg.qty, qty: egg.qty,
weight: egg.weight,
})) ?? [ })) ?? [
{ {
product_warehouse_id: 0, product_warehouse_id: 0,
qty: '', qty: '',
}, weight: '',
],
});
export const getRecordingGradingFormInitialValues = (
initialValues?: Partial<CreateGradingPayload> & { recording_egg_id?: number }
): RecordingGradingFormValues => ({
eggs_grading: initialValues?.eggs_grading?.map((grading) => ({
recording_egg_id: grading.recording_egg_id,
grade: grading.grade,
qty: grading.qty,
})) ?? [
{
recording_egg_id: initialValues?.recording_egg_id ?? 0,
grade: '',
qty: '',
}, },
], ],
}); });
@@ -16,7 +16,6 @@ import CheckboxInput from '@/components/input/CheckboxInput';
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 { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import Tooltip from '@/components/Tooltip';
import { import {
ProjectFlockKandangApi, ProjectFlockKandangApi,
@@ -98,9 +97,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [recordingFormErrorMessage, setRecordingFormErrorMessage] = const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState(''); useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [newRecordingData, setNewRecordingData] = useState<Recording | null>( const [, setNewRecordingData] = useState<Recording | null>(null);
null
);
const [nextDayRecording, setNextDayRecording] = const [nextDayRecording, setNextDayRecording] =
useState<NextDayRecording | null>(null); useState<NextDayRecording | null>(null);
@@ -111,18 +108,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const isRecordingApproved = useCallback((recording?: Recording) => { const isRecordingApproved = useCallback((recording?: Recording) => {
return ( return (
recording?.approval?.action === 'APPROVED' && recording?.approval?.action === 'APPROVED' &&
recording?.approval?.step_name === 'Disetujui' && recording?.approval?.step_name === 'Disetujui'
recording?.approval?.step_number === 3
);
}, []);
const hasGradingData = useCallback((recording?: Recording) => {
if (!recording || !recording.eggs) return false;
return recording.eggs.some(
(egg) =>
egg.gradings &&
egg.gradings.length > 0 &&
egg.gradings.some((grading) => grading.qty > 0)
); );
}, []); }, []);
@@ -181,6 +167,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggs: (values.eggs ?? []).map((egg) => ({ eggs: (values.eggs ?? []).map((egg) => ({
product_warehouse_id: egg.product_warehouse_id, product_warehouse_id: egg.product_warehouse_id,
qty: Number(egg.qty) || 0, qty: Number(egg.qty) || 0,
weight:
typeof egg.weight === 'number'
? egg.weight
: parseFloat(String(egg.weight)) || 0,
})), })),
}; };
}, },
@@ -203,35 +193,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[router] [router]
); );
const createRecordingHandlerWithRedirect = useCallback(
async (
payload: CreateGrowingRecordingPayload | CreateLayingRecordingPayload,
redirectToGrading: boolean = false
) => {
const res = await RecordingApi.create(payload);
if (isResponseError(res)) {
setRecordingFormErrorMessage(res.message);
return null;
}
toast.success(res?.message as string);
if (res?.status === 'success' && res.data) {
setNewRecordingData(res.data);
return res.data;
}
if (redirectToGrading) {
toast.error(
'Gagal mendapatkan ID recording. Silakan coba dari halaman list.'
);
router.push('/production/recording');
}
return null;
},
[router]
);
const updateRecordingHandler = useCallback( const updateRecordingHandler = useCallback(
async ( async (
recordingId: number, recordingId: number,
@@ -650,7 +611,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const hasPakanFlag = product.product.flags?.includes('PAKAN'); const hasPakanFlag = product.product.flags?.includes('PAKAN');
const hasOvkFlag = product.product.flags?.includes('OVK'); const hasOvkFlag = product.product.flags?.includes('OVK');
// Only include products that are in the same location as the selected kandang
if (hasPakanFlag || hasOvkFlag) { if (hasPakanFlag || hasOvkFlag) {
options.push({ options.push({
value: product.id, value: product.id,
@@ -690,7 +650,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
depletionProductsData.data.forEach((product) => { depletionProductsData.data.forEach((product) => {
const productName = product.product.name; const productName = product.product.name;
// Filter for depletion-related products (culling, mati, afkir)
if ( if (
productName.toLowerCase().includes('culling') || productName.toLowerCase().includes('culling') ||
productName.toLowerCase().includes('mati') || productName.toLowerCase().includes('mati') ||
@@ -732,7 +691,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
eggProductsData.data.forEach((product) => { eggProductsData.data.forEach((product) => {
const productName = product.product.name; const productName = product.product.name;
// Filter for egg-related products
if ( if (
productName.toLowerCase().includes('telur') || productName.toLowerCase().includes('telur') ||
productName.toLowerCase().includes('egg') || productName.toLowerCase().includes('egg') ||
@@ -1019,54 +977,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
}, [formik.values.stocks, getStockUsageError, type]); }, [formik.values.stocks, getStockUsageError, type]);
const hasConsumableEggs = useMemo(() => {
if (!isLayingCategory) return false;
const layingValues = formik.values as RecordingLayingFormValues;
if (!layingValues.eggs || layingValues.eggs.length === 0) return false;
return layingValues.eggs.some((egg) => {
if (!egg.product_warehouse_id || Number(egg.qty) <= 0) return false;
const product = eggProducts.find(
(opt) => opt.value === egg.product_warehouse_id
);
if (!product) return false;
const productName = product.label.toLowerCase();
return (
productName.includes('konsumsi') &&
productName.includes('baik') &&
Number(egg.qty) > 0
);
});
}, [isLayingCategory, formik.values, eggProducts]);
const hasConsumableEggsInRecording = useCallback((recording?: Recording) => {
if (!recording || !recording.eggs || recording.eggs.length === 0)
return false;
return recording.eggs.some((egg) => {
if (!egg.product_warehouse || !egg.product_warehouse.product)
return false;
if (Number(egg.qty) <= 0) return false;
const productName = egg.product_warehouse.product.name.toLowerCase();
return (
productName.includes('konsumsi') &&
productName.includes('baik') &&
Number(egg.qty) > 0
);
});
}, []);
const hasConsumableEggsInCurrentRecording = useMemo(() => {
return (
hasConsumableEggsInRecording(initialValues) ||
hasConsumableEggsInRecording(newRecordingData || undefined)
);
}, [initialValues, newRecordingData, hasConsumableEggsInRecording]);
const isRepeaterInputError = ( const isRepeaterInputError = (
arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs', arrayName: 'body_weights' | 'stocks' | 'depletions' | 'eggs',
column: string, column: string,
@@ -1148,7 +1058,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (hasSameDayRecording) { if (hasSameDayRecording) {
toast.error( toast.error(
`Recording untuk hari ${nextDayRecording.next_day} sudah ada. `Recording untuk hari ${nextDayRecording.next_day} sudah ada.
Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.` Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.`
); );
return; return;
@@ -1278,7 +1188,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
// Body Weights Handlers
const addBodyWeight = () => { const addBodyWeight = () => {
const newBodyWeights = [ const newBodyWeights = [
...(formik.values.body_weights || []), ...(formik.values.body_weights || []),
@@ -1397,7 +1306,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedBodyWeights([]); setSelectedBodyWeights([]);
}; };
// Stocks Handlers
const addStock = () => { const addStock = () => {
const newStocks = [ const newStocks = [
...(formik.values.stocks || []), ...(formik.values.stocks || []),
@@ -1430,7 +1338,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedStocks([]); setSelectedStocks([]);
}; };
// Depletions Handlers
const addDepletion = () => { const addDepletion = () => {
const newDepletions = [ const newDepletions = [
...(formik.values.depletions || []), ...(formik.values.depletions || []),
@@ -1465,7 +1372,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setSelectedDepletions([]); setSelectedDepletions([]);
}; };
// Eggs Handlers
const addEgg = () => { const addEgg = () => {
const newEggs = [ const newEggs = [
...((formik.values as RecordingLayingFormValues).eggs || []), ...((formik.values as RecordingLayingFormValues).eggs || []),
@@ -1485,6 +1391,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik] [formik]
); );
const handleEggWeightChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value) || 0;
formik.setFieldValue(`eggs.${idx}.weight`, value);
},
[formik]
);
const removeEgg = (idx: number) => { const removeEgg = (idx: number) => {
const updatedEggs = ( const updatedEggs = (
formik.values as RecordingLayingFormValues formik.values as RecordingLayingFormValues
@@ -1569,47 +1483,37 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Kembali Kembali
</Button> </Button>
{type === 'detail' && {type === 'detail' && !isRecordingApproved(initialValues) && (
!isRecordingApproved(initialValues) && <div className='flex flex-row gap-2'>
(!isLayingCategory || hasGradingData(initialValues)) && ( <Button
<div className='flex flex-row gap-2'> variant='outline'
<Button color='success'
variant='outline' onClick={() => {
color='success' setApprovalNotes('');
onClick={() => { approveModal.openModal();
setApprovalNotes(''); }}
approveModal.openModal(); isLoading={isApproveLoading}
}} className='w-full sm:w-fit'
isLoading={isApproveLoading} >
className='w-full sm:w-fit' <Icon icon='material-symbols:check' width={24} height={24} />
> Approve
<Icon </Button>
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<Button <Button
variant='outline' variant='outline'
color='error' color='error'
onClick={() => { onClick={() => {
setApprovalNotes(''); setApprovalNotes('');
rejectModal.openModal(); rejectModal.openModal();
}} }}
isLoading={isRejectLoading} isLoading={isRejectLoading}
className='w-full sm:w-fit' className='w-full sm:w-fit'
> >
<Icon <Icon icon='material-symbols:close' width={24} height={24} />
icon='material-symbols:close' Reject
width={24} </Button>
height={24} </div>
/> )}
Reject
</Button>
</div>
)}
</div> </div>
<h1 className='text-2xl font-bold text-center'> <h1 className='text-2xl font-bold text-center'>
@@ -1916,7 +1820,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.body_weights?.map((bw, idx) => ( {formik.values.body_weights?.map((bw, idx) => (
<tr key={`body-weight-${idx}`}> <tr key={`body-weight-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`body-weight-${idx}`} name={`body-weight-${idx}`}
checked={selectedBodyWeights.includes(idx)} checked={selectedBodyWeights.includes(idx)}
@@ -2166,7 +2070,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.stocks?.map((stock, idx) => ( {formik.values.stocks?.map((stock, idx) => (
<tr key={`stock-${idx}`}> <tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`stock-${idx}`} name={`stock-${idx}`}
checked={selectedStocks.includes(idx)} checked={selectedStocks.includes(idx)}
@@ -2386,7 +2290,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{formik.values.depletions?.map((depletion, idx) => ( {formik.values.depletions?.map((depletion, idx) => (
<tr key={`depletion-${idx}`}> <tr key={`depletion-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`depletion-${idx}`} name={`depletion-${idx}`}
checked={selectedDepletions.includes(idx)} checked={selectedDepletions.includes(idx)}
@@ -2587,6 +2491,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
</th> </th>
<th>
Berat (gram)
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th> <th>Action</th>
)} )}
@@ -2597,7 +2510,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
(egg, idx) => ( (egg, idx) => (
<tr key={`egg-${idx}`}> <tr key={`egg-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='!align-middle'> <td className='align-middle!'>
<CheckboxInput <CheckboxInput
name={`egg-${idx}`} name={`egg-${idx}`}
checked={selectedEggs.includes(idx)} checked={selectedEggs.includes(idx)}
@@ -2662,32 +2575,55 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</td> </td>
<td> <td>
<div className='flex flex-col gap-1'> <NumberInput
<NumberInput required
required name={`eggs.${idx}.qty`}
name={`eggs.${idx}.qty`} value={egg.qty ?? ''}
value={egg.qty ?? ''} onChange={handleEggQtyChangeWrapper(idx)}
onChange={handleEggQtyChangeWrapper(idx)} onBlur={formik.handleBlur}
onBlur={formik.handleBlur} decimalScale={0}
decimalScale={0} allowNegative={false}
allowNegative={false} thousandSeparator=','
thousandSeparator=',' decimalSeparator='.'
decimalSeparator='.' isError={
isError={ isRepeaterInputError('eggs', 'qty', idx).isError
isRepeaterInputError('eggs', 'qty', idx) }
.isError errorMessage={
} isRepeaterInputError('eggs', 'qty', idx)
errorMessage={ .errorMessage
isRepeaterInputError('eggs', 'qty', idx) }
.errorMessage readOnly={type === 'detail'}
} className={{
readOnly={type === 'detail'} wrapper: 'w-full min-w-24',
className={{ }}
wrapper: 'w-full min-w-24', placeholder='Masukkan jumlah telur'
}} />
placeholder='Masukkan jumlah telur' </td>
/> <td>
</div> <NumberInput
required
name={`eggs.${idx}.weight`}
value={egg.weight ?? ''}
onChange={handleEggWeightChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('eggs', 'weight', idx)
.isError
}
errorMessage={
isRepeaterInputError('eggs', 'weight', idx)
.errorMessage
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan berat telur (gram)...'
/>
</td> </td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td> <td>
@@ -2779,46 +2715,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
{/* Right side actions */} {/* Right side actions */}
<div className='flex flex-col sm:flex-row sm:justify-end gap-2 w-full sm:w-auto'> <div className='flex flex-col sm:flex-row sm:justify-end gap-2 w-full sm:w-auto'>
{type === 'detail' && isLayingCategory && (
<Tooltip
content={
hasConsumableEggsInCurrentRecording
? 'Lanjut ke proses grading untuk telur konsumsi baik'
: 'Hanya bisa melanjutkan ke grading jika ada Telur Konsumsi Baik'
}
position='left'
color={
hasConsumableEggsInCurrentRecording ? 'info' : 'warning'
}
>
<Button
type='button'
color='primary'
disabled={!hasConsumableEggsInCurrentRecording}
className='w-full sm:w-auto'
onClick={() => {
const recordingId =
newRecordingData?.id || initialValues?.id;
if (recordingId) {
router.push(
`/production/recording/grading/add?recording_id=${recordingId}`
);
} else {
toast.error(
'Recording ID tidak ditemukan. Silakan refresh halaman.'
);
}
}}
>
<Icon icon='material-symbols:egg' width={24} height={24} />
{hasGradingData(initialValues) ||
hasGradingData(newRecordingData || undefined)
? 'Edit Grading'
: 'Lanjut ke Grading'}
</Button>
</Tooltip>
)}
{type === 'edit' && ( {type === 'edit' && (
<div className='flex flex-col sm:flex-row gap-2 w-full sm:w-auto'> <div className='flex flex-col sm:flex-row gap-2 w-full sm:w-auto'>
<Button <Button
@@ -2870,78 +2766,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
> >
Submit Submit
</Button> </Button>
{isLayingCategory && (
<Tooltip
content={
hasConsumableEggs
? 'Lanjut ke proses grading untuk telur konsumsi baik'
: 'Hanya bisa melanjutkan ke grading jika ada Telur Konsumsi Baik'
}
position='left'
color={hasConsumableEggs ? 'info' : 'warning'}
>
<Button
type='button'
color='info'
className='px-4'
isLoading={formik.isSubmitting}
disabled={
hasExceededStock ||
!formik.isValid ||
formik.isSubmitting ||
!hasConsumableEggs
}
onClick={async () => {
if (!formik.isValid) {
await formik.validateForm();
return;
}
setRecordingFormErrorMessage('');
formik.setSubmitting(true);
try {
if (isLayingCategory) {
const layingValues =
formik.values as RecordingLayingFormValues;
const layingPayload =
createLayingPayload(layingValues);
const recordingData =
await createRecordingHandlerWithRedirect(
layingPayload as CreateLayingRecordingPayload,
true
);
if (recordingData?.id) {
toast.success(
'Recording berhasil disimpan! Mengalihkan ke form Grading...'
);
setTimeout(() => {
router.push(
`/production/recording/grading/add?recording_id=${recordingData.id}`
);
}, 1000);
}
}
} catch {
toast.error(
'Gagal membuat recording. Silakan coba lagi.'
);
} finally {
formik.setSubmitting(false);
}
}}
>
<Icon
icon='material-symbols:egg'
width={24}
height={24}
/>
Next Step: Grading
</Button>
</Tooltip>
)}
</div> </div>
)} )}
</div> </div>
@@ -2979,8 +2803,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Approve Confirmation Modal */} {/* Approve Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' && {(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) && !isRecordingApproved(initialValues) && (
(!isLayingCategory || hasGradingData(initialValues)) && (
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
@@ -3002,8 +2825,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Reject Confirmation Modal */} {/* Reject Confirmation Modal */}
{(type as 'add' | 'edit' | 'detail') === 'detail' && {(type as 'add' | 'edit' | 'detail') === 'detail' &&
!isRecordingApproved(initialValues) && !isRecordingApproved(initialValues) && (
(!isLayingCategory || hasGradingData(initialValues)) && (
<ConfirmationModalWithNotes <ConfirmationModalWithNotes
ref={rejectModal.ref} ref={rejectModal.ref}
type='error' type='error'
File diff suppressed because it is too large Load Diff
@@ -16,7 +16,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { cn, formatDate, formatCurrency } from '@/lib/helper'; import { cn, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -136,14 +136,6 @@ const PurchaseTable = () => {
? formatDate(props.row.original.po_date, 'DD MMM YYYY') ? formatDate(props.row.original.po_date, 'DD MMM YYYY')
: '-', : '-',
}, },
{
accessorKey: 'due_date',
header: 'Jatuh Tempo',
cell: (props) =>
props.row.original.due_date
? formatDate(props.row.original.due_date, 'DD MMM YYYY')
: '-',
},
{ {
header: 'Aging', header: 'Aging',
cell: (props) => { cell: (props) => {
@@ -156,11 +148,6 @@ const PurchaseTable = () => {
return `${diffDays} hari`; return `${diffDays} hari`;
}, },
}, },
{
accessorKey: 'grand_total',
header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.row.original.grand_total),
},
{ {
header: 'Aksi', header: 'Aksi',
cell: (props) => { cell: (props) => {
@@ -52,6 +52,8 @@ const PurchaseOrderAcceptApprovalForm = ({
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
useState(''); useState('');
const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const isRepeaterInputError = ( const isRepeaterInputError = (
idx: number, idx: number,
@@ -64,7 +66,6 @@ const PurchaseOrderAcceptApprovalForm = ({
| 'expedition_vendor_id' | 'expedition_vendor_id'
| 'received_qty' | 'received_qty'
| 'transport_per_item' | 'transport_per_item'
| 'transport_total'
): { isError: boolean; errorMessage: string } => { ): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.items?.[idx]; const touchedItem = formik.touched.items?.[idx];
const errorItem = formik.errors.items?.[idx] as const errorItem = formik.errors.items?.[idx] as
@@ -163,6 +164,7 @@ const PurchaseOrderAcceptApprovalForm = ({
validateOnBlur: true, validateOnBlur: true,
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreateAcceptApprovalRequestPayload = { const payload: CreateAcceptApprovalRequestPayload = {
action: 'APPROVED',
notes: values.notes || '', notes: values.notes || '',
items: items:
values.items?.map((formItem) => { values.items?.map((formItem) => {
@@ -181,10 +183,6 @@ const PurchaseOrderAcceptApprovalForm = ({
typeof formItem.transport_per_item === 'string' typeof formItem.transport_per_item === 'string'
? parseFloat(formItem.transport_per_item) || 0 ? parseFloat(formItem.transport_per_item) || 0
: formItem.transport_per_item || 0, : formItem.transport_per_item || 0,
transport_total:
typeof formItem.transport_total === 'string'
? parseFloat(formItem.transport_total) || 0
: formItem.transport_total || 0,
}; };
}) || [], }) || [],
}; };
@@ -239,9 +237,8 @@ const PurchaseOrderAcceptApprovalForm = ({
vehicle_number: item.vehicle_number || '', vehicle_number: item.vehicle_number || '',
expedition_vendor: null, expedition_vendor: null,
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: item.total_qty || '',
transport_per_item: '', transport_per_item: '',
transport_total: '',
}; };
}); });
formik.setFieldValue('items', updatedItems); formik.setFieldValue('items', updatedItems);
@@ -301,7 +298,7 @@ const PurchaseOrderAcceptApprovalForm = ({
// ===== PURCHASE ITEM OPERATIONS ===== // ===== PURCHASE ITEM OPERATIONS =====
const handlePurchaseItemChange = ( const handlePurchaseItemChange = (
idx: number, idx: number,
field: 'received_qty' | 'transport_per_item' | 'transport_total', field: 'received_qty' | 'transport_per_item',
value: string | number value: string | number
) => { ) => {
const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value;
@@ -318,26 +315,6 @@ const PurchaseOrderAcceptApprovalForm = ({
: parseFloat( : parseFloat(
formik.values.items?.[idx]?.transport_per_item as string formik.values.items?.[idx]?.transport_per_item as string
) || 0; ) || 0;
if (receivedQty > 0 && transportPerItem >= 0) {
const calculatedTransportTotal = receivedQty * transportPerItem;
formik.setFieldValue(
`items.${idx}.transport_total`,
calculatedTransportTotal
);
}
}
if (field === 'transport_total') {
const receivedQty =
parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0;
if (receivedQty > 0 && numValue >= 0) {
const calculatedTransportPerItem = numValue / receivedQty;
formik.setFieldValue(
`items.${idx}.transport_per_item`,
calculatedTransportPerItem
);
}
} }
}; };
@@ -386,10 +363,6 @@ const PurchaseOrderAcceptApprovalForm = ({
Transport/Item Transport/Item
<span className='text-error'>*</span> <span className='text-error'>*</span>
</th> </th>
<th>
Total Transport
<span className='text-error'>*</span>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -657,37 +630,6 @@ const PurchaseOrderAcceptApprovalForm = ({
}} }}
/> />
</td> </td>
<td>
<NumberInput
required
name={`items.${idx}.transport_total`}
value={formItem?.transport_total || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'transport_total',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan total transport'
allowNegative={false}
decimalScale={2}
thousandSeparator=','
decimalSeparator='.'
inputPrefix={'Rp'}
isError={
isRepeaterInputError(idx, 'transport_total').isError
}
errorMessage={
isRepeaterInputError(idx, 'transport_total')
.errorMessage
}
className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
}}
/>
</td>
</tr> </tr>
); );
})} })}
@@ -732,7 +674,8 @@ const PurchaseOrderAcceptApprovalForm = ({
disabled={ disabled={
!formik.isValid || !formik.isValid ||
formik.isSubmitting || formik.isSubmitting ||
hasQuantityExceededErrors hasQuantityExceededErrors ||
isRejected
} }
> >
Submit Submit
@@ -23,10 +23,12 @@ type PurchaseRequestStaffApprovalFormSchemaType = {
}; };
type PurchaseRequestManagerApprovalFormSchemaType = { type PurchaseRequestManagerApprovalFormSchemaType = {
action: 'APPROVED' | 'REJECTED';
notes: string | null; notes: string | null;
}; };
type PurchaseRequestAcceptApprovalFormSchemaType = { type PurchaseRequestAcceptApprovalFormSchemaType = {
action: 'APPROVED' | 'REJECTED';
notes: string | null; notes: string | null;
items: { items: {
purchase_item?: { purchase_item?: {
@@ -45,7 +47,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
expedition_vendor_id: number; expedition_vendor_id: number;
received_qty: number | string; received_qty: number | string;
transport_per_item: number | string; transport_per_item: number | string;
transport_total: number | string;
}[]; }[];
}; };
@@ -83,7 +84,6 @@ export type PurchaseAcceptApprovalItemSchema = {
expedition_vendor_id: number; expedition_vendor_id: number;
received_qty: number | string; received_qty: number | string;
transport_per_item: number | string; transport_per_item: number | string;
transport_total: number | string;
}; };
export type PurchaseDeleteItemsSchema = { export type PurchaseDeleteItemsSchema = {
@@ -152,6 +152,10 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManagerApprovalFormSchemaType> = const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManagerApprovalFormSchemaType> =
Yup.object({ Yup.object({
action: Yup.mixed<'APPROVED' | 'REJECTED'>()
.oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED')
.required('Action wajib diisi!')
.default('APPROVED'),
notes: Yup.string().nullable().default(null), notes: Yup.string().nullable().default(null),
}); });
@@ -230,20 +234,6 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
} }
) )
.typeError('Biaya transport per item harus berupa angka!'), .typeError('Biaya transport per item harus berupa angka!'),
transport_total: Yup.mixed<string | number>()
.required('Total biaya transport wajib diisi!')
.test(
'is-valid-transport-total',
'Total biaya transport harus berupa angka lebih dari atau sama dengan 0!',
function (value) {
if (value === '' || value === null || value === undefined)
return false;
const numValue =
typeof value === 'string' ? parseFloat(value) : value;
return !isNaN(numValue) && numValue >= 0;
}
)
.typeError('Total biaya transport harus berupa angka!'),
}); });
export const PurchaseRequestStaffApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestStaffApprovalFormSchemaType> = export const PurchaseRequestStaffApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestStaffApprovalFormSchemaType> =
@@ -368,6 +358,7 @@ export const PurchaseRequestManagerApprovalFormDefaultValues = (
purchase?: Purchase purchase?: Purchase
): PurchaseRequestManagerApprovalFormSchemaType => { ): PurchaseRequestManagerApprovalFormSchemaType => {
return { return {
action: 'APPROVED',
notes: purchase?.notes ?? null, notes: purchase?.notes ?? null,
}; };
}; };
@@ -378,6 +369,10 @@ export type PurchaseRequestManagerApprovalFormValues = Yup.InferType<
export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestAcceptApprovalFormSchemaType> = export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestAcceptApprovalFormSchemaType> =
Yup.object({ Yup.object({
action: Yup.mixed<'APPROVED' | 'REJECTED'>()
.oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED')
.required('Action wajib diisi!')
.default('APPROVED'),
notes: Yup.string().nullable().default(null), notes: Yup.string().nullable().default(null),
items: Yup.array() items: Yup.array()
.of(PurchaseAcceptApprovalItemObjectSchema) .of(PurchaseAcceptApprovalItemObjectSchema)
@@ -388,6 +383,7 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType = export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
{ {
action: 'APPROVED',
notes: '', notes: '',
items: [ items: [
{ {
@@ -399,7 +395,6 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
transport_per_item: '', transport_per_item: '',
transport_total: '',
}, },
], ],
}; };
@@ -408,6 +403,7 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
purchase?: Purchase purchase?: Purchase
): PurchaseRequestAcceptApprovalFormSchemaType => { ): PurchaseRequestAcceptApprovalFormSchemaType => {
return { return {
action: 'APPROVED',
notes: purchase?.notes ?? null, notes: purchase?.notes ?? null,
items: purchase?.items items: purchase?.items
? purchase.items.map((item) => ({ ? purchase.items.map((item) => ({
@@ -419,7 +415,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
transport_per_item: '', transport_per_item: '',
transport_total: '',
})) }))
: [ : [
{ {
@@ -431,7 +426,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
expedition_vendor_id: 0, expedition_vendor_id: 0,
received_qty: '', received_qty: '',
transport_per_item: '', transport_per_item: '',
transport_total: '',
}, },
], ],
}; };
@@ -61,7 +61,7 @@ const PurchaseOrderStaffApprovalForm = ({
return 'add'; return 'add';
} }
const currentStep = initialValues?.approval?.step_number || 1; const currentStep = initialValues?.latest_approval?.step_number || 1;
switch (currentStep) { switch (currentStep) {
case 1: case 1:
@@ -77,7 +77,9 @@ const PurchaseOrderStaffApprovalForm = ({
// Step 4+ (Penerimaan Barang dan selesai), tidak boleh edit kalau sudah disetujui // Step 4+ (Penerimaan Barang dan selesai), tidak boleh edit kalau sudah disetujui
return 'edit'; return 'edit';
} }
}, [rawDataApprovals, propType, initialValues?.approval?.step_number]); }, [rawDataApprovals, propType, initialValues?.latest_approval?.step_number]);
const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -93,16 +95,16 @@ const PurchaseOrderStaffApprovalForm = ({
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const canUpdatePurchaseItems = useMemo(() => { const canUpdatePurchaseItems = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.approval.step_number; const currentStep = initialValues.latest_approval.step_number;
return currentStep >= 3; return currentStep >= 3;
}, [initialValues?.approval]); }, [initialValues?.latest_approval]);
const canShowDeleteAddButtons = useMemo(() => { const canShowDeleteAddButtons = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.approval.step_number; const currentStep = initialValues.latest_approval.step_number;
// Step 2 (Staff Purchase) dengan mode 'add' tidak boleh add/delete items // Step 2 (Staff Purchase) dengan mode 'add' tidak boleh add/delete items
// User hanya boleh input harga dan total harga untuk items yang sudah ada // User hanya boleh input harga dan total harga untuk items yang sudah ada
@@ -112,7 +114,7 @@ const PurchaseOrderStaffApprovalForm = ({
// Step 3 (Manager Purchase) boleh add/delete items // Step 3 (Manager Purchase) boleh add/delete items
return currentStep === 3; return currentStep === 3;
}, [initialValues?.approval, type]); }, [initialValues?.latest_approval, type]);
const isRepeaterInputError = ( const isRepeaterInputError = (
idx: number, idx: number,
@@ -719,7 +721,10 @@ const PurchaseOrderStaffApprovalForm = ({
'min-w-52 md:min-w-72 lg:min-w-80', 'min-w-52 md:min-w-72 lg:min-w-80',
}} }}
bottomLabel={ bottomLabel={
'Previous: ' + purchaseItem.product.name type === 'edit'
? 'Previous: ' +
purchaseItem.product.name
: undefined
} }
/> />
</td> </td>
@@ -819,7 +824,11 @@ const PurchaseOrderStaffApprovalForm = ({
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputPrefix={'Rp'} inputPrefix={'Rp'}
bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`} bottomLabel={
type === 'edit'
? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`
: undefined
}
isError={ isError={
isRepeaterInputError( isRepeaterInputError(
formItemIndex, formItemIndex,
@@ -857,7 +866,11 @@ const PurchaseOrderStaffApprovalForm = ({
thousandSeparator=',' thousandSeparator=','
decimalSeparator='.' decimalSeparator='.'
inputPrefix={'Rp'} inputPrefix={'Rp'}
bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`} bottomLabel={
type === 'edit'
? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`
: undefined
}
isError={ isError={
isRepeaterInputError( isRepeaterInputError(
formItemIndex, formItemIndex,
@@ -1131,7 +1144,7 @@ const PurchaseOrderStaffApprovalForm = ({
color='primary' color='primary'
className='px-4' className='px-4'
isLoading={formik.isSubmitting} isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting} disabled={!formik.isValid || formik.isSubmitting || isRejected}
> >
Submit Submit
</Button> </Button>
@@ -7,7 +7,6 @@ type PurchaseRequestFormSchemaType = {
label: string; label: string;
} | null; } | null;
supplier_id: number; supplier_id: number;
credit_term: number;
area?: { area?: {
value: number; value: number;
label: string; label: string;
@@ -78,10 +77,6 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
credit_term: Yup.number()
.required('Jangka waktu kredit wajib diisi!')
.min(0, 'Jangka waktu kredit tidak boleh kurang dari 0!')
.typeError('Jangka waktu kredit wajib diisi!'),
supplier_id: Yup.number() supplier_id: Yup.number()
.required('Supplier wajib dipilih!') .required('Supplier wajib dipilih!')
.min(1, 'Supplier wajib dipilih!') .min(1, 'Supplier wajib dipilih!')
@@ -124,7 +119,6 @@ export const getPurchaseRequestFormInitialValues = (
} }
: null, : null,
supplier_id: initialValues?.supplier?.id ?? 0, supplier_id: initialValues?.supplier?.id ?? 0,
credit_term: initialValues?.credit_term ?? 0,
area: initialValues?.area area: initialValues?.area
? { ? {
value: initialValues.area.id, value: initialValues.area.id,
@@ -185,10 +185,6 @@ const PurchaseRequestForm = ({
typeof values.supplier_id === 'string' typeof values.supplier_id === 'string'
? parseInt(values.supplier_id) || 0 ? parseInt(values.supplier_id) || 0
: values.supplier_id || 0, : values.supplier_id || 0,
credit_term:
typeof values.credit_term === 'string'
? parseInt(values.credit_term) || 0
: values.credit_term || 0,
notes: values.notes || '', notes: values.notes || '',
items: (values.items || []).map((item) => ({ items: (values.items || []).map((item) => ({
warehouse_id: Number(item.warehouse_id) || 0, warehouse_id: Number(item.warehouse_id) || 0,
@@ -342,27 +338,6 @@ const PurchaseRequestForm = ({
}; };
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const updateCreditTermBasedOnSupplier = useCallback(
(supplierId: number) => {
if (supplierId > 0 && isResponseSuccess(supplierRawData)) {
const supplierData = supplierRawData.data.find(
(s: Supplier) => s.id === supplierId
);
if (supplierData?.due_date) {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', supplierData.due_date.toString());
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
}
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
}
},
[supplierRawData]
);
const resetPurchaseItems = useCallback(() => { const resetPurchaseItems = useCallback(() => {
if (formik.values.items) { if (formik.values.items) {
formik.values.items.forEach((_, idx) => { formik.values.items.forEach((_, idx) => {
@@ -377,16 +352,6 @@ const PurchaseRequestForm = ({
}, []); }, []);
// ===== SIDE EFFECTS ===== // ===== SIDE EFFECTS =====
useEffect(() => {
if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) {
updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id));
resetPurchaseItems();
} else {
formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', '');
resetPurchaseItems();
}
}, [formik.values.supplier_id]);
// ===== FORM HANDLERS ===== // ===== FORM HANDLERS =====
const handleSupplierChange = useCallback( const handleSupplierChange = useCallback(
@@ -402,23 +367,6 @@ const PurchaseRequestForm = ({
[] []
); );
const handleCreditTermChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
formik.setFieldTouched('credit_term', true);
formik.setFieldValue('credit_term', value);
},
[]
);
const handleCreditTermBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
formik.handleBlur(e);
},
[formik]
);
const handleAreaChange = useCallback( const handleAreaChange = useCallback(
(val: OptionType | OptionType[] | null) => { (val: OptionType | OptionType[] | null) => {
const area = val as OptionType | null; const area = val as OptionType | null;
@@ -499,7 +447,7 @@ const PurchaseRequestForm = ({
body: 'flex flex-col gap-6', body: 'flex flex-col gap-6',
}} }}
> >
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}> <div className={'grid grid-cols-1 md:grid-cols-3 gap-6'}>
<SelectInput <SelectInput
required required
label='Vendor' label='Vendor'
@@ -518,29 +466,6 @@ const PurchaseRequestForm = ({
isClearable isClearable
/> />
<NumberInput
required={!!formik.values.supplier_id}
label='Jatuh tempo (hari)'
name='credit_term'
value={formik.values.credit_term || ''}
onChange={handleCreditTermChange}
onBlur={handleCreditTermBlur}
isError={
formik.touched.credit_term &&
Boolean(formik.errors.credit_term)
}
errorMessage={formik.errors.credit_term as string}
readOnly={type === 'detail' || !formik.values.supplier_id}
disabled={type === 'detail' || !formik.values.supplier_id}
allowNegative={false}
decimalScale={0}
placeholder={
!formik.values.supplier_id
? 'Pilih Vendor terlebih dahulu'
: 'Masukkan jumlah hari jatuh tempo'
}
/>
<SelectInput <SelectInput
label='Area' label='Area'
placeholder='Pilih Area...' placeholder='Pilih Area...'
@@ -565,7 +490,7 @@ const PurchaseRequestForm = ({
isClearable={type !== 'detail'} isClearable={type !== 'detail'}
/> />
<div className={'col-span-2'}> <div className={'md:col-span-3'}>
<TextInput <TextInput
label='Notes' label='Notes'
name='notes' name='notes'
@@ -27,6 +27,7 @@ import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrde
import Card from '@/components/Card'; import Card from '@/components/Card';
import { import {
CreateAcceptApprovalRequestPayload,
CreateManagerApprovalRequestPayload, CreateManagerApprovalRequestPayload,
CreateStaffApprovalRequestPayload, CreateStaffApprovalRequestPayload,
Purchase, Purchase,
@@ -88,6 +89,8 @@ const PurchaseOrderDetail = ({
const staffApprovalModal = useModal(); const staffApprovalModal = useModal();
const staffRejectionModal = useModal(); const staffRejectionModal = useModal();
const acceptApprovalModal = useModal(); const acceptApprovalModal = useModal();
const acceptRejectionModal = useModal();
const managerRejectionModal = useModal();
const editModal = useModal(); const editModal = useModal();
const penerimaanBarangModal = useModal(); const penerimaanBarangModal = useModal();
const deleteModal = useModal(); const deleteModal = useModal();
@@ -156,9 +159,9 @@ const PurchaseOrderDetail = ({
}, [goodsReceiptItems]); }, [goodsReceiptItems]);
const approvalStep = useMemo(() => { const approvalStep = useMemo(() => {
if (!initialValues?.approval) return null; if (!initialValues?.latest_approval) return null;
return initialValues.approval.step_number; return initialValues.latest_approval.step_number;
}, [initialValues?.approval]); }, [initialValues?.latest_approval]);
const { const {
approvals, approvals,
@@ -166,7 +169,7 @@ const PurchaseOrderDetail = ({
rawDataApprovals, rawDataApprovals,
refresh: refreshApprovals, refresh: refreshApprovals,
} = useApprovalSteps({ } = useApprovalSteps({
latestApproval: initialValues?.approval, latestApproval: initialValues?.latest_approval,
approvalLines: PURCHASE_ORDER_APPROVAL_LINE, approvalLines: PURCHASE_ORDER_APPROVAL_LINE,
moduleName: 'PURCHASES', moduleName: 'PURCHASES',
moduleId: initialValues?.id?.toString() ?? '', moduleId: initialValues?.id?.toString() ?? '',
@@ -177,19 +180,22 @@ const PurchaseOrderDetail = ({
}); });
const showApprovalButton = const showApprovalButton =
approvalStep !== null && approvalStep >= 1 && approvalStep <= 3; approvalStep !== null &&
approvalStep >= 1 &&
approvalStep <= 3 &&
initialValues?.latest_approval?.action !== 'REJECTED';
const canDeleteItems = useMemo(() => { const canDeleteItems = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.approval.step_number; const currentStep = initialValues.latest_approval.step_number;
const hasReachedStep5 = rawDataApprovals?.some( const hasReachedStep5 = rawDataApprovals?.some(
(approval) => approval.step_number === 5 (approval) => approval.step_number === 5
); );
return currentStep === 3 && !hasReachedStep5; return currentStep === 3 && !hasReachedStep5;
}, [initialValues?.approval, rawDataApprovals]); }, [initialValues?.latest_approval, rawDataApprovals]);
const handleApprovalClick = () => { const handleApprovalClick = () => {
if (!approvalStep) return; if (!approvalStep) return;
@@ -216,24 +222,30 @@ const PurchaseOrderDetail = ({
case 1: case 1:
staffRejectionModal.openModal(); staffRejectionModal.openModal();
break; break;
case 2:
managerRejectionModal.openModal();
break;
case 3:
acceptRejectionModal.openModal();
break;
default: default:
break; break;
} }
}; };
const canShowPurchaseOrderInvoice = useMemo(() => { const canShowPurchaseOrderInvoice = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.approval.step_number; const currentStep = initialValues.latest_approval.step_number;
return currentStep >= 3; return currentStep >= 3;
}, [initialValues?.approval]); }, [initialValues?.latest_approval]);
const canShowPenerimaanBarang = useMemo(() => { const canShowPenerimaanBarang = useMemo(() => {
if (!initialValues?.approval) return false; if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.approval.step_number; const currentStep = initialValues.latest_approval.step_number;
return currentStep === 5; return currentStep === 5;
}, [initialValues?.approval]); }, [initialValues?.latest_approval]);
const totalBeforeTax = useMemo(() => { const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce( return purchaseOrderItems.reduce(
@@ -296,6 +308,33 @@ const PurchaseOrderDetail = ({
[initialValues?.id, searchParams, refetchData] [initialValues?.id, searchParams, refetchData]
); );
const createAcceptApprovalHandler = useCallback(
async (payload: CreateAcceptApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequestId) {
toast.error('Purchase Request ID is required');
return;
}
const res = await PurchaseApi.acceptApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
toast.error(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals();
refetchData?.();
},
[initialValues?.id, searchParams, refreshApprovals, refetchData]
);
// ===== MODAL HANDLERS ===== // ===== MODAL HANDLERS =====
const handleStaffApprovalModalClose = useCallback(() => { const handleStaffApprovalModalClose = useCallback(() => {
refreshApprovals(); refreshApprovals();
@@ -544,11 +583,6 @@ const PurchaseOrderDetail = ({
accessorKey: 'transport_per_item', accessorKey: 'transport_per_item',
cell: (props) => formatCurrency(props.getValue() as number), cell: (props) => formatCurrency(props.getValue() as number),
}, },
{
header: 'Transport Total',
accessorKey: 'transport_total',
cell: (props) => formatCurrency(props.getValue() as number),
},
]; ];
const summaryData = [ const summaryData = [
@@ -647,7 +681,7 @@ const PurchaseOrderDetail = ({
<div className='space-y-4'> <div className='space-y-4'>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Area Area
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
@@ -657,7 +691,7 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Lokasi Lokasi
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
@@ -671,7 +705,7 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Gudang Gudang
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
@@ -685,7 +719,7 @@ const PurchaseOrderDetail = ({
<div className='space-y-4'> <div className='space-y-4'>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Nama Vendor Nama Vendor
</span> </span>
<span className='text-gray-900 font-medium ml-3 break-all'> <span className='text-gray-900 font-medium ml-3 break-all'>
@@ -696,7 +730,7 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Kategori Vendor Kategori Vendor
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
@@ -706,18 +740,7 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Tgl. Jatuh Tempo
</span>
<span className='text-gray-900 ml-3 break-all'>
: {formatDate(purchaseData.due_date, 'D MMM YYYY')} (
{purchaseData.credit_term} hari)
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Nomor Nomor
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
@@ -727,7 +750,7 @@ const PurchaseOrderDetail = ({
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Nomor PO Nomor PO
</span> </span>
<div className='ml-3'> <div className='ml-3'>
@@ -925,6 +948,7 @@ const PurchaseOrderDetail = ({
color: 'success', color: 'success',
onClick: async (notes) => { onClick: async (notes) => {
const payload: CreateManagerApprovalRequestPayload = { const payload: CreateManagerApprovalRequestPayload = {
action: 'APPROVED',
notes: notes || null, notes: notes || null,
}; };
@@ -1041,6 +1065,61 @@ const PurchaseOrderDetail = ({
}} }}
/> />
{/* Accept Rejection Modal */}
<ConfirmationModalWithNotes
ref={acceptRejectionModal.ref}
type='error'
text='Apakah Anda yakin ingin menolak (reject) penerimaan barang ini?'
placeholder='(Opsional) Masukkan alasan penolakan...'
rows={4}
closeOnBackdrop
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateAcceptApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
items: [],
};
await createAcceptApprovalHandler(payload);
await refetchData?.();
acceptRejectionModal.closeModal();
},
}}
secondaryButton={{
text: 'Batal',
}}
/>
{/* Manager Rejection Modal */}
<ConfirmationModalWithNotes
ref={managerRejectionModal.ref}
type='error'
text='Apakah Anda yakin ingin menolak (reject) approval manajer untuk permintaan pembelian ini?'
placeholder='(Opsional) Masukkan alasan penolakan...'
rows={4}
closeOnBackdrop
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refetchData?.();
managerRejectionModal.closeModal();
},
}}
secondaryButton={{
text: 'Batal',
}}
/>
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
@@ -309,9 +309,6 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
{purchaseData?.supplier?.alias || ''}) {purchaseData?.supplier?.alias || ''})
</Text> </Text>
<Text>{purchaseData?.supplier?.category || '-'}</Text> <Text>{purchaseData?.supplier?.category || '-'}</Text>
<Text>
Credit Term: {purchaseData?.credit_term || 0} hari
</Text>
<Text> <Text>
Due Date:{' '} Due Date:{' '}
{purchaseData?.due_date {purchaseData?.due_date
+4 -16
View File
@@ -26,7 +26,7 @@ export const PROJECT_FLOCK_KANDANGS_APPROVAL_LINE: ApprovalLine = [
}, },
{ {
step_number: 3, step_number: 3,
step_name: 'Closed', step_name: 'Selesai',
}, },
] as const; ] as const;
@@ -70,14 +70,10 @@ export const MARKETING_APPROVAL_LINE: ApprovalLine = [
export const RECORDING_APPROVAL_LINE: ApprovalLine = [ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
@@ -85,14 +81,10 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
@@ -100,14 +92,10 @@ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [ export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] as const;
+1 -5
View File
@@ -233,14 +233,10 @@ export const APPROVAL_WORKFLOWS = [
steps: [ steps: [
{ {
step_number: 1, step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan', step_name: 'Pengajuan',
}, },
{ {
step_number: 3, step_number: 2,
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
], ],
-24
View File
@@ -9,8 +9,6 @@ import {
CreateRecordingPayload, CreateRecordingPayload,
Recording, Recording,
UpdateRecordingPayload, UpdateRecordingPayload,
CreateGradingPayload,
UpdateGradingPayload,
NextDayRecording, NextDayRecording,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
@@ -64,28 +62,6 @@ export class RecordingService extends BaseApiService<
}); });
} }
async createGrading(
payload: CreateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>('gradings', {
method: 'POST',
payload,
});
}
async updateGrading(
gradingId: number,
payload: UpdateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'PUT',
payload,
}
);
}
async deleteGrading( async deleteGrading(
gradingId: number gradingId: number
): Promise<BaseApiResponse<unknown> | undefined> { ): Promise<BaseApiResponse<unknown> | undefined> {
+19 -1
View File
@@ -56,8 +56,26 @@ export type ClosingExpense = {
reference_number: string; reference_number: string;
}; };
// "flag_name": "PAKAN",
// "product_warehouse_id": 14,
// "product_id": 8,
// "product_name": "281 SPECIAL STARTER",
// "product_category": "Bahan Baku",
// "uom": "Kilogram",
// "quantity": 1100
export type StockItem = {
flag_name: string;
product_warehouse_id: number;
product_id: number;
product_name: string;
product_category: string;
uom: string;
quantity: number;
};
export type CheckClosingResponse = { export type CheckClosingResponse = {
unfinished_expenses: number; unfinished_expenses: number;
stock_remaining: ProductWarehouse[]; stock_remaining: StockItem[];
expenses: ClosingExpense[]; expenses: ClosingExpense[];
}; };
+6 -42
View File
@@ -9,8 +9,7 @@ export type ProductionMetrics = {
cum_intake: number; cum_intake: number;
fcr_value: number; fcr_value: number;
total_chick_qty: number; total_chick_qty: number;
daily_depletion_rate?: number; cum_depletion: number;
cum_depletion?: number;
}; };
export type BaseRecording = { export type BaseRecording = {
@@ -18,42 +17,33 @@ export type BaseRecording = {
project_flock_kandang_id: number; project_flock_kandang_id: number;
record_datetime: string; record_datetime: string;
day: number; day: number;
created_by: User; project_flock_category?: 'GROWING' | 'LAYING';
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingBW = { export type RecordingBW = {
id: number;
recording_id: number;
avg_weight: number; avg_weight: number;
qty: number; qty: number;
total_weight: number; total_weight: number;
}; };
export type RecordingDepletion = { export type RecordingDepletion = {
id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
export type RecordingStock = { export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
usage_amount?: number; usage_amount?: number;
usage_qty: number;
qty: number;
pending_qty: number; pending_qty: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
}; };
export type RecordingEgg = { export type RecordingEgg = {
id: number; id: number;
recording_id: number;
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
created_by: User; weight: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
gradings?: { gradings?: {
grade: string; grade: string;
@@ -71,19 +61,12 @@ export type GradingEgg = {
export type Recording = BaseMetadata & export type Recording = BaseMetadata &
BaseRecording & { BaseRecording & {
project_flock_category?: 'GROWING' | 'LAYING';
approval?: BaseApproval; approval?: BaseApproval;
egg_grading_status?: string | null; created_user: User;
egg_grading_pending_qty?: number | null;
egg_grading_completed_qty?: number | null;
body_weights?: RecordingBW[]; body_weights?: RecordingBW[];
depletions?: RecordingDepletion[]; depletions?: RecordingDepletion[];
stocks?: RecordingStock[]; stocks?: RecordingStock[];
eggs?: RecordingEgg[]; eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[]; grading_eggs?: GradingEgg[];
}; };
@@ -108,27 +91,10 @@ export type CreateGrowingRecordingPayload = {
}[]; }[];
}; };
export type CreateGradingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type UpdateGradingPayload = CreateGradingPayload;
export type CreateGradingRecordingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type CreateEggPayload = { export type CreateEggPayload = {
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;
weight: number;
}; };
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
@@ -137,11 +103,9 @@ export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
export type CreateRecordingPayload = export type CreateRecordingPayload =
| CreateGrowingRecordingPayload | CreateGrowingRecordingPayload
| CreateLayingRecordingPayload | CreateLayingRecordingPayload;
| CreateGradingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload;
+4 -7
View File
@@ -42,7 +42,6 @@ export type PurchaseItem = {
expedition_vendor_name?: string | null; expedition_vendor_name?: string | null;
received_qty?: number | null; received_qty?: number | null;
transport_per_item?: number | null; transport_per_item?: number | null;
transport_total?: number | null;
}; };
export type BasePurchase = { export type BasePurchase = {
@@ -52,9 +51,7 @@ export type BasePurchase = {
po_document_path?: string | null; po_document_path?: string | null;
po_date: string; po_date: string;
supplier: Supplier; supplier: Supplier;
credit_term: number;
due_date: string; due_date: string;
grand_total: number;
notes?: string | null; notes?: string | null;
deleted_at?: string | null; deleted_at?: string | null;
created_by: number; created_by: number;
@@ -62,14 +59,13 @@ export type BasePurchase = {
location?: Location; location?: Location;
warehouse?: Warehouse; warehouse?: Warehouse;
items?: PurchaseItem[]; items?: PurchaseItem[];
approval?: BaseApproval; latest_approval?: BaseApproval;
}; };
export type Purchase = BaseMetadata & BasePurchase; export type Purchase = BaseMetadata & BasePurchase;
export type CreatePurchaseRequestPayload = { export type CreatePurchaseRequestPayload = {
supplier_id: number; supplier_id: number;
credit_term: number;
notes?: string | null; notes?: string | null;
items: { items: {
warehouse_id: number; warehouse_id: number;
@@ -103,11 +99,13 @@ export type UpdateStaffApprovalRequestPayload = {
}; };
export type CreateManagerApprovalRequestPayload = { export type CreateManagerApprovalRequestPayload = {
action: 'APPROVED' | 'REJECTED';
notes?: string | null; notes?: string | null;
}; };
export type CreateAcceptApprovalRequestPayload = { export type CreateAcceptApprovalRequestPayload = {
notes?: string; action: 'APPROVED' | 'REJECTED';
notes?: string | null;
items: { items: {
purchase_item_id: number; purchase_item_id: number;
received_date: string; received_date: string;
@@ -117,7 +115,6 @@ export type CreateAcceptApprovalRequestPayload = {
expedition_vendor_id: number; expedition_vendor_id: number;
received_qty: number; received_qty: number;
transport_per_item: number; transport_per_item: number;
transport_total: number;
}[]; }[];
}; };