mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
fix(resolve): fix resolve MR
This commit is contained in:
Generated
+1
-11
@@ -1855,7 +1855,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1925,7 +1924,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -2449,7 +2447,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3063,8 +3060,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.3.10",
|
||||
@@ -3520,7 +3516,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3694,7 +3689,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6173,7 +6167,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -6204,7 +6197,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -7091,7 +7083,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7259,7 +7250,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const EditMarketingDelivery = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const soId = searchParams.get('marketingId');
|
||||
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(`get-so-${soId}`, () =>
|
||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<MarketingForm
|
||||
formType='add_deliver'
|
||||
initialValues={marketing.data}
|
||||
afterSubmit={() => {
|
||||
refreshMarketing();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditMarketingDelivery;
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
|
||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||
|
||||
const AddSalesOrder = () => {
|
||||
return (
|
||||
<div className='size-full p-4'>
|
||||
<SalesForm />
|
||||
<MarketingForm formType='add' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const EditMarketingDelivery = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const soId = searchParams.get('marketingId');
|
||||
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(`get-so-${soId}`, () =>
|
||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
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 (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isResponseSuccess(marketing) &&
|
||||
marketing.data.latest_approval.step_number != 3
|
||||
) {
|
||||
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
|
||||
router.back();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<MarketingForm
|
||||
formType='edit_deliver'
|
||||
initialValues={marketing.data}
|
||||
afterSubmit={() => {
|
||||
refreshMarketing();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditMarketingDelivery;
|
||||
@@ -0,0 +1,11 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,20 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail';
|
||||
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const DetailSalesOrder = () => {
|
||||
const DetailMarketing = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
const soId = searchParams.get('marketingId');
|
||||
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
|
||||
MarketingApi.getSingle(id)
|
||||
);
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
||||
|
||||
if (!soId) {
|
||||
router.back();
|
||||
@@ -35,10 +37,13 @@ const DetailSalesOrder = () => {
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<SalesOrderDetail initialValues={marketing.data} />
|
||||
<MarketingDetail
|
||||
initialValues={marketing.data}
|
||||
refresh={refreshMarketing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailSalesOrder;
|
||||
export default DetailMarketing;
|
||||
@@ -0,0 +1,11 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
+15
-5
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
|
||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
@@ -10,10 +10,14 @@ const EditSalesOrder = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
const soId = searchParams.get('marketingId');
|
||||
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
|
||||
MarketingApi.getSingle(id)
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(`get-so-${soId}`, () =>
|
||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
@@ -34,7 +38,13 @@ const EditSalesOrder = () => {
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<SalesForm formType='edit' initialValues={marketing.data} />
|
||||
<MarketingForm
|
||||
formType='edit'
|
||||
initialValues={marketing.data}
|
||||
afterSubmit={() => {
|
||||
refreshMarketing();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
||||
|
||||
const Marketing = () => {
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<MarketingTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Marketing;
|
||||
@@ -1,10 +0,0 @@
|
||||
import SalesOrderTable from '@/components/pages/marketing/sales-orders/SalesOrderTable';
|
||||
|
||||
const SalesOrder = () => {
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<SalesOrderTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default SalesOrder;
|
||||
@@ -11,10 +11,6 @@ const AddChickin = () => {
|
||||
return (
|
||||
<>
|
||||
<section className='w-full p-4'>
|
||||
<FormHeader
|
||||
title='Daftar Kandang Project Flock'
|
||||
backUrl='/production/project-flock'
|
||||
/>
|
||||
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { ChickinApi } from '@/services/api/production/chickin';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
Chickin,
|
||||
ChickinApprovalPayload,
|
||||
} from '@/types/api/production/chickin';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
/**
|
||||
* TODO: Refactor code - pindahin detail ke reuseable component
|
||||
* setelah implement approval and reject
|
||||
*/
|
||||
|
||||
const DetailChickin = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const chickinId = searchParams.get('chickinId');
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const confirmModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
const chickinModal = useModal();
|
||||
const {
|
||||
data: chickin,
|
||||
isLoading,
|
||||
mutate: refreshChickin,
|
||||
} = useSWR(chickinId, (id: number) => ChickinApi.getSingle(id));
|
||||
|
||||
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
|
||||
// chickin.data?.approval.step_number == 1 ? false : true
|
||||
true
|
||||
);
|
||||
const [isRejectedDisabled, setIsRejectedDisabled] =
|
||||
useState(!isApprovedDisabled);
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
|
||||
);
|
||||
|
||||
if (!chickinId) {
|
||||
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 (!isLoading && (!chickin || isResponseError(chickin))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isResponseSuccess(chickin)) {
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const confirmationModalClickHandler = async ({
|
||||
action = 'APPROVED',
|
||||
}: {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
}) => {
|
||||
if (chickin?.data.id === undefined) return;
|
||||
setIsApproveLoading(true);
|
||||
const approveChickinRes = await ChickinApi.customRequest<
|
||||
BaseApiResponse<Chickin>,
|
||||
ChickinApprovalPayload
|
||||
>(`/approvals`, {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
action: action,
|
||||
approvable_ids: [chickin.data.id],
|
||||
},
|
||||
});
|
||||
|
||||
if (isResponseSuccess(approveChickinRes)) {
|
||||
if (refreshChickin) {
|
||||
await refreshChickin();
|
||||
}
|
||||
toast.success(approveChickinRes.message as string);
|
||||
}
|
||||
if (isResponseError(approveChickinRes)) {
|
||||
toast.error(approveChickinRes?.message as string);
|
||||
}
|
||||
confirmModal.closeModal();
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
const deleteProjectFlockRes = await ChickinApi.delete(
|
||||
chickin.data?.id as number
|
||||
);
|
||||
|
||||
if (isResponseSuccess(deleteProjectFlockRes)) {
|
||||
toast.success(deleteProjectFlockRes?.message as string);
|
||||
router.push('/production/chickin');
|
||||
}
|
||||
if (isResponseError(deleteProjectFlockRes)) {
|
||||
toast.error(deleteProjectFlockRes?.message as string);
|
||||
}
|
||||
deleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-4 flex flex-col justify-center gap-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(chickin) && (
|
||||
<>
|
||||
{/* <div className='w-full flex flex-col sm:flex-row gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='success'
|
||||
onClick={(() => {
|
||||
if (chickin?.data.id) {
|
||||
setApprovalAction('APPROVED');
|
||||
confirmModal.openModal();
|
||||
}
|
||||
})}
|
||||
disabled={!chickin?.data.id || isApprovedDisabled}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
if (chickin?.data.id) {
|
||||
setApprovalAction('REJECTED');
|
||||
confirmModal.openModal();
|
||||
}
|
||||
}}
|
||||
disabled={!chickin?.data.id || isRejectedDisabled}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='mdi:times' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</div> */}
|
||||
<Card
|
||||
title='Informasi Umum'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-4 mt-4'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Flock</div>
|
||||
<div className='text-sm'>
|
||||
{
|
||||
chickin?.data?.project_flock_kandang?.project_flock?.flock
|
||||
?.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Area</div>
|
||||
<div className='text-sm'>
|
||||
{
|
||||
chickin.data.project_flock_kandang?.project_flock.area
|
||||
.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Kategori</div>
|
||||
<div className='text-sm'>
|
||||
{chickin.data.project_flock_kandang?.project_flock.category}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Lokasi</div>
|
||||
<div className='text-sm'>
|
||||
{
|
||||
chickin.data.project_flock_kandang?.project_flock.location
|
||||
.name
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Periode</div>
|
||||
<div className='text-sm'>
|
||||
{chickin.data.project_flock_kandang?.project_flock.period}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Kandang</div>
|
||||
<div className='text-sm'>
|
||||
{chickin.data.project_flock_kandang?.kandang.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Detail Chickin'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-4 mt-4'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Flock Kandang</div>
|
||||
<div className='text-sm'>
|
||||
{
|
||||
chickin?.data?.project_flock_kandang?.project_flock?.flock
|
||||
?.name
|
||||
}{' '}
|
||||
- {chickin.data.project_flock_kandang?.kandang.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Tanggal Chickin</div>
|
||||
<div className='text-sm'>
|
||||
{chickin.data.chick_in_date
|
||||
? new Date(chickin.data.chick_in_date).toLocaleDateString(
|
||||
'id-ID'
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Jumlah (Ekor)</div>
|
||||
<div className='text-sm'>
|
||||
{chickin.data.quantity?.toLocaleString('id-ID')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='font-semibold text-sm'>Catatan</div>
|
||||
<div className='text-sm'>{chickin.data.note}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className='w-full flex flex-col sm:flex-row gap-2'>
|
||||
<Button
|
||||
color='error'
|
||||
onClick={() => {
|
||||
deleteModal.openModal();
|
||||
}}
|
||||
>
|
||||
<Icon icon='mdi:times' width={24} height={24} />
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={() => {
|
||||
chickinModal.openModal();
|
||||
}}
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${chickin?.data?.project_flock_kandang?.project_flock.flock?.name} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal ref={chickinModal.ref}>
|
||||
<div className='flex flex-row justify-between items-center'>
|
||||
<h1 className='text-xl font-semibold text-center mb-6'>
|
||||
Chickin Kandang -{' '}
|
||||
{chickin?.data?.project_flock_kandang &&
|
||||
chickin?.data?.project_flock_kandang.kandang?.name}
|
||||
</h1>
|
||||
<Button
|
||||
color='error'
|
||||
variant='link'
|
||||
onClick={chickinModal.closeModal}
|
||||
>
|
||||
<Icon
|
||||
className='text-black'
|
||||
icon='uil:times'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={confirmModal.ref}
|
||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${
|
||||
approvalAction == 'APPROVED' ? 'approve' : 'reject'
|
||||
} chickin berikut? (${
|
||||
chickin?.data?.project_flock_kandang?.project_flock?.flock?.name
|
||||
} - ${chickin?.data.project_flock_kandang?.kandang.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isApproveLoading,
|
||||
onClick: () => {
|
||||
confirmationModalClickHandler({
|
||||
action: approvalAction,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailChickin;
|
||||
@@ -80,7 +80,10 @@ const DateInput = ({
|
||||
|
||||
// --- Sync value props ---
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
if (!value) {
|
||||
setDisplayValue('');
|
||||
return;
|
||||
}
|
||||
if (isRange && typeof value === 'object') {
|
||||
const from = value.from ? new Date(value.from) : undefined;
|
||||
const to = value.to ? new Date(value.to) : undefined;
|
||||
|
||||
@@ -8,7 +8,6 @@ export interface TextAreaProps {
|
||||
label?: string;
|
||||
bottomLabel?: string;
|
||||
name: string;
|
||||
rows?: number;
|
||||
value?: string | number;
|
||||
placeholder?: string;
|
||||
className?: {
|
||||
@@ -24,8 +23,11 @@ export interface TextAreaProps {
|
||||
required?: boolean;
|
||||
isLoading?: boolean;
|
||||
errorMessage?: string;
|
||||
startAdornment?: ReactNode;
|
||||
endAdornment?: ReactNode;
|
||||
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
||||
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
const TextArea = ({
|
||||
@@ -33,18 +35,20 @@ const TextArea = ({
|
||||
bottomLabel,
|
||||
name,
|
||||
value,
|
||||
rows = 3,
|
||||
placeholder,
|
||||
className,
|
||||
isError,
|
||||
isValid,
|
||||
errorMessage,
|
||||
startAdornment,
|
||||
endAdornment,
|
||||
disabled = false,
|
||||
required = false,
|
||||
onChange,
|
||||
onBlur,
|
||||
readOnly = false,
|
||||
isLoading = false,
|
||||
rows = 3,
|
||||
}: TextAreaProps) => {
|
||||
return (
|
||||
<div
|
||||
@@ -75,34 +79,35 @@ const TextArea = ({
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{startAdornment && startAdornment}
|
||||
|
||||
<div className='relative size-full'>
|
||||
<textarea
|
||||
className={cn(
|
||||
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
rows={rows}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className='absolute right-3 bottom-3 z-10'>
|
||||
{isLoading && <span className='loading loading-spinner' />}
|
||||
</div>
|
||||
<textarea
|
||||
className={cn(
|
||||
'textarea h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all bg-white',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
</div>
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
rows={rows}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
{(isLoading || endAdornment) && (
|
||||
<div className='flex flex-row gap-2'>
|
||||
{isLoading && <span className='loading loading-spinner' />}
|
||||
|
||||
{endAdornment && endAdornment}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isError && bottomLabel && (
|
||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||
|
||||
@@ -50,6 +50,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
||||
...primaryButton,
|
||||
onClick: () => {
|
||||
primaryButton?.onClick?.(notes);
|
||||
setNotes('');
|
||||
},
|
||||
}}
|
||||
secondaryButton={secondaryButton}
|
||||
|
||||
+265
-67
@@ -5,31 +5,38 @@ import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import Table from '@/components/Table';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatVechicleNumber } from '@/lib/helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
import { CellContext, Row } from '@tanstack/react-table';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const RowsOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
deliveryClickHandler,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Marketing, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
deliveryClickHandler?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
@@ -44,7 +51,7 @@ const RowsOptionsMenu = ({
|
||||
>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Button
|
||||
href={`/marketing/sales-orders/detail/?salesOrderId=${props.row.original.id}`}
|
||||
href={`/marketing/detail?marketingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
@@ -52,15 +59,39 @@ const RowsOptionsMenu = ({
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/marketing/sales-orders/detail/edit/?salesOrderId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
{props.row.original.latest_approval.step_number != 1 && (
|
||||
<Button
|
||||
href={
|
||||
props.row.original.latest_approval.step_number == 3
|
||||
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
|
||||
: props.row.original.latest_approval.step_number == 2
|
||||
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (props.row.original.latest_approval.step_number == 2) {
|
||||
deliveryClickHandler?.();
|
||||
}
|
||||
}}
|
||||
variant='ghost'
|
||||
color='success'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:truck' width={16} height={16} />
|
||||
Deliver
|
||||
</Button>
|
||||
)}
|
||||
{props.row.original.latest_approval.step_number != 3 && (
|
||||
<Button
|
||||
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
@@ -75,19 +106,18 @@ const RowsOptionsMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SalesOrderTable = () => {
|
||||
const MarketingTable = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [approveAction, setApproveAction] = useState<
|
||||
'approve' | 'reject' | null
|
||||
>(null);
|
||||
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const selectedRowIds = Object.keys(rowSelection).filter(
|
||||
(id) => rowSelection[id]
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data: marketing,
|
||||
@@ -98,6 +128,7 @@ const SalesOrderTable = () => {
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const productsModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
|
||||
const searchChangeHandler = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -116,12 +147,12 @@ const SalesOrderTable = () => {
|
||||
);
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApproveAction('approve');
|
||||
setApproveAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApproveAction('reject');
|
||||
setApproveAction('REJECTED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
@@ -130,6 +161,93 @@ const SalesOrderTable = () => {
|
||||
productsModal.openModal();
|
||||
};
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
selectedItem?.id as number
|
||||
);
|
||||
if (isResponseSuccess(deleteMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.success(deleteMarketingRes?.message as string);
|
||||
}
|
||||
if (isResponseError(deleteMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.error(deleteMarketingRes?.message as string);
|
||||
}
|
||||
refreshMarketing();
|
||||
deleteModal.closeModal();
|
||||
};
|
||||
|
||||
const allData = isResponseSuccess(marketing) ? marketing.data : [];
|
||||
const selectedRowsData = allData.filter(
|
||||
(row) => rowSelection[row.id.toString()]
|
||||
);
|
||||
|
||||
const hasApprovable = selectedRowsData.some(
|
||||
(row) => row.latest_approval.step_number === 1
|
||||
);
|
||||
const hasRejectable = selectedRowsData.some(
|
||||
(row) => row.latest_approval.step_number === 2
|
||||
);
|
||||
|
||||
const disableApprove = !hasApprovable || hasRejectable;
|
||||
// const disableReject = !hasRejectable || hasApprovable;
|
||||
|
||||
const idsToProcess =
|
||||
approveAction === 'APPROVED'
|
||||
? selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id)
|
||||
: selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 2)
|
||||
.map((row) => row.id);
|
||||
|
||||
const approveMarketingHandler = async (notes: string) => {
|
||||
let idsToProcess: number[] = [];
|
||||
|
||||
if (approveAction === 'APPROVED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id);
|
||||
} else if (approveAction === 'REJECTED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 2)
|
||||
.map((row) => row.id);
|
||||
}
|
||||
|
||||
if (idsToProcess.length === 0) {
|
||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||
confirmationModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
||||
idsToProcess,
|
||||
approveAction,
|
||||
notes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(approveMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.success(approveMarketingRes?.message as string);
|
||||
setRowSelection({});
|
||||
}
|
||||
if (isResponseError(approveMarketingRes)) {
|
||||
confirmationModal.closeModal();
|
||||
toast.error(approveMarketingRes?.message as string);
|
||||
}
|
||||
refreshMarketing();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
||||
deliveryModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refreshMarketing?.();
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${selectedItem?.id}`
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -144,13 +262,18 @@ const SalesOrderTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||
const step = row.original.latest_approval?.step_number;
|
||||
return step === 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<TableToolbar
|
||||
addButton={{
|
||||
href: '/marketing/sales-orders/add',
|
||||
href: '/marketing/add/sales-orders',
|
||||
label: 'Tambah Sales Order',
|
||||
}}
|
||||
search={{
|
||||
@@ -169,51 +292,75 @@ const SalesOrderTable = () => {
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
className='justify-start text-sm'
|
||||
disabled={!selectedRowIds.length}
|
||||
disabled={disableApprove}
|
||||
>
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
className='justify-start text-sm'
|
||||
disabled={!selectedRowIds.length}
|
||||
disabled={disableReject}
|
||||
>
|
||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={isResponseSuccess(marketing) ? marketing.data : []}
|
||||
data={allData}
|
||||
columns={[
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
header: ({ table }) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectableRows = allRows.filter(getRowCanSelect);
|
||||
|
||||
const allSelected =
|
||||
selectableRows.length > 0 &&
|
||||
selectableRows.every((row) => row.getIsSelected());
|
||||
|
||||
const someSelected =
|
||||
selectableRows.some((row) => row.getIsSelected()) &&
|
||||
!allSelected;
|
||||
|
||||
const toggleSelectableRows = () => {
|
||||
const shouldSelect = !allSelected;
|
||||
selectableRows.forEach((row) =>
|
||||
row.toggleSelected(shouldSelect)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected}
|
||||
onChange={toggleSelectableRows}
|
||||
disabled={selectableRows.length === 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const canSelect = getRowCanSelect(row);
|
||||
return (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!canSelect}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_number',
|
||||
@@ -222,9 +369,12 @@ const SalesOrderTable = () => {
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
header: 'Tanggal',
|
||||
cell: (props) => {
|
||||
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'approval.step_name',
|
||||
accessorKey: 'latest_approval.step_name',
|
||||
header: 'Status',
|
||||
},
|
||||
{
|
||||
@@ -232,15 +382,25 @@ const SalesOrderTable = () => {
|
||||
header: 'Customer',
|
||||
},
|
||||
{
|
||||
accessorKey: 'grand_total',
|
||||
accessorFn: (row) =>
|
||||
row.sales_order
|
||||
?.map((product) => product.total_price)
|
||||
.reduce((a, b) => a + b, 0) ?? 0,
|
||||
header: 'Grand Total',
|
||||
cell: (props) => {
|
||||
return formatCurrency(
|
||||
props.row.original?.sales_order
|
||||
?.map((product) => product.total_price)
|
||||
.reduce((a, b) => a + b, 0) ?? 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketing_products.length',
|
||||
header: 'Product Details',
|
||||
cell: (props) => {
|
||||
if (props?.row?.original?.marketing_products?.length) {
|
||||
if (props?.row?.original?.marketing_products?.length > 1) {
|
||||
if (props?.row?.original?.sales_order?.length) {
|
||||
if (props?.row?.original?.sales_order?.length > 1) {
|
||||
return (
|
||||
<Button
|
||||
variant='link'
|
||||
@@ -250,12 +410,11 @@ const SalesOrderTable = () => {
|
||||
productsClickHandler(props?.row?.original);
|
||||
}}
|
||||
>
|
||||
Lihat {props?.row?.original?.marketing_products?.length}{' '}
|
||||
Produk
|
||||
Lihat {props?.row?.original?.sales_order?.length} Produk
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
const product = props?.row?.original?.marketing_products[0];
|
||||
const product = props?.row?.original?.sales_order[0];
|
||||
return <>{product?.product_warehouse?.product?.name}</>;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +433,15 @@ const SalesOrderTable = () => {
|
||||
const isLast2Rows =
|
||||
currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {};
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedItem(props.row.original);
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
setSelectedItem(props.row.original);
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -284,6 +451,7 @@ const SalesOrderTable = () => {
|
||||
type='dropdown'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
deliveryClickHandler={deliveryClickHandler}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
@@ -294,6 +462,7 @@ const SalesOrderTable = () => {
|
||||
type='collapse'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
deliveryClickHandler={deliveryClickHandler}
|
||||
/>
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
@@ -330,16 +499,45 @@ const SalesOrderTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approveAction === 'approve' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approveAction} data penjualan (${selectedRowIds.length} data)?`}
|
||||
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: confirmationModal.closeModal,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
||||
onClick: approveMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: deleteModal.closeModal,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
onClick: deleteMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModalWithNotes
|
||||
ref={deliveryModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approveAction === 'approve' ? 'success' : 'error',
|
||||
color: 'success',
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -361,10 +559,10 @@ const SalesOrderTable = () => {
|
||||
<Icon icon='mdi:close' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<Table<MarketingProduct>
|
||||
<Table<BaseSalesOrder>
|
||||
data={
|
||||
isResponseSuccess(marketing) && selectedItem
|
||||
? (selectedItem?.marketing_products ?? [])
|
||||
? (selectedItem?.sales_order ?? [])
|
||||
: []
|
||||
}
|
||||
columns={[
|
||||
@@ -403,4 +601,4 @@ const SalesOrderTable = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SalesOrderTable;
|
||||
export default MarketingTable;
|
||||
@@ -0,0 +1,461 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import Table from '@/components/Table';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import {
|
||||
BaseDelivery,
|
||||
BaseSalesOrder,
|
||||
Marketing,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||
import DeliveryOrderExport from '../pdf/DeliveryOrderExport';
|
||||
|
||||
const MarketingDetail = ({
|
||||
initialValues,
|
||||
refresh,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
refresh?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState(
|
||||
initialValues?.sales_order
|
||||
?.map((item) => item.total_price)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
const {
|
||||
approvals,
|
||||
isLoading: isLoadingApproval,
|
||||
refresh: refreshApproval,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.latest_approval,
|
||||
approvalLines: MARKETING_APPROVAL_LINE,
|
||||
moduleName: 'MARKETINGS',
|
||||
moduleId: initialValues?.id as number as unknown as string,
|
||||
});
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
// const rejectClickHandler = () => {
|
||||
// setApprovalAction('REJECTED');
|
||||
// confirmationModal.openModal();
|
||||
// };
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||
setIsLoading(true);
|
||||
const res = await SalesOrderApi.singleApproval(
|
||||
initialValues?.id as number,
|
||||
approvalAction,
|
||||
notes
|
||||
);
|
||||
setIsLoading(false);
|
||||
confirmationModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||
setIsLoading(true);
|
||||
const res = await SalesOrderApi.delivery(
|
||||
initialValues?.id as number,
|
||||
notes
|
||||
);
|
||||
setIsLoading(false);
|
||||
deliveryModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col w-full gap-4'>
|
||||
<FormHeader title='Detail Sales Order' backUrl='/marketing' />
|
||||
{!isLoadingApproval && approvals && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.latest_approval?.step_number != 3 && (
|
||||
<>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 1}
|
||||
>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 2}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button> */}
|
||||
</>
|
||||
)}
|
||||
{initialValues?.latest_approval?.step_number == 2 && (
|
||||
<Button
|
||||
color='success'
|
||||
href={`/marketing/add/delivery-orders?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:truck' width={24} height={24} />
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title='Informasi Penjualan'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width='45%' className='font-semibold'>
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.customer?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Status</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.latest_approval?.step_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{formatDate(initialValues?.so_date, 'DD MMM yyyy')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Total Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{formatCurrency(grandTotal as number)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Catatan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.notes ?? '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Dokumen</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
<SalesOrderExport data={initialValues} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
{initialValues?.sales_order && (
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<Table<BaseSalesOrder>
|
||||
data={initialValues?.sales_order}
|
||||
columns={[
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.warehouse.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Produk',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.product.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Harga Satuan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.unit_price);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{initialValues?.delivery_order && (
|
||||
<Card
|
||||
title='Informasi Pengiriman'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
{initialValues?.delivery_order.map((delivery, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-row gap-3'>
|
||||
<div className='font-semibold'>
|
||||
Nomor DO : {delivery.do_number}
|
||||
</div>
|
||||
</div>
|
||||
<Table<BaseDelivery>
|
||||
data={delivery.deliveries}
|
||||
columns={[
|
||||
{
|
||||
header: 'Tanggal Pengiriman',
|
||||
accessorFn() {
|
||||
return formatDate(
|
||||
delivery.delivery_date,
|
||||
'DD MMM yyyy'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'No. Polisi',
|
||||
accessorFn(row) {
|
||||
return formatVechicleNumber(row.vehicle_number);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.warehouse.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Produk',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.product.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Harga Satuan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.unit_price);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName:
|
||||
'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div className='flex flex-row gap-3 my-3'>
|
||||
<DeliveryOrderExport
|
||||
data={initialValues}
|
||||
deliveryOrder={delivery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button color='error' onClick={deleteClickHandler}>
|
||||
<Icon icon='mdi:delete' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction === 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModalWithNotes
|
||||
ref={deliveryModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin deliver penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingDetail;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
DeliveryOrderProductFormValues,
|
||||
DeliveryOrderProductSchema,
|
||||
} from './repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
|
||||
type MarketingSchemaType = {
|
||||
customer_id: number | undefined;
|
||||
sales_person_id: number | undefined;
|
||||
customer:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
so_date: string | undefined;
|
||||
notes: string | undefined;
|
||||
};
|
||||
|
||||
type SalesOrderSchemaType = MarketingSchemaType & {
|
||||
sales_order: SalesOrderProductFormValues[];
|
||||
};
|
||||
|
||||
type DeliveryOrderSchemaType = {
|
||||
delivery_order: DeliveryOrderProductFormValues[];
|
||||
};
|
||||
|
||||
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||
Yup.object({
|
||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
|
||||
customer: Yup.object({
|
||||
value: Yup.number().required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
so_date: Yup.string().required('Tanggal wajib diisi!'),
|
||||
notes: Yup.string().required('Catatan wajib diisi!'),
|
||||
sales_order: Yup.array()
|
||||
.of(SalesOrderProductSchema)
|
||||
.min(1, 'Produk wajib diisi!')
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
|
||||
export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
||||
Yup.object({
|
||||
delivery_order: Yup.array()
|
||||
.of(DeliveryOrderProductSchema)
|
||||
.min(1, 'Pengiriman wajib diisi!')
|
||||
.required()
|
||||
.test(
|
||||
'at-least-one-delivery-date',
|
||||
'Minimal ada satu tanggal pengiriman yang harus diisi!',
|
||||
(value) => {
|
||||
if (!value || value.length == 0) {
|
||||
return false;
|
||||
}
|
||||
return value.some(
|
||||
(item) =>
|
||||
item.delivery_date !== null &&
|
||||
item.delivery_date !== undefined &&
|
||||
item.delivery_date !== ''
|
||||
);
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const UpdateSalesOrderSchema = SalesOrderSchema;
|
||||
|
||||
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
|
||||
|
||||
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
||||
@@ -0,0 +1,757 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import {
|
||||
BaseDeliveryOrder,
|
||||
BaseSalesOrder,
|
||||
CreateDeliveryOrderPayload,
|
||||
CreateSalesOrderPayload,
|
||||
CreateSalesOrderProductPayload,
|
||||
Marketing,
|
||||
UpdateDeliveryOrderPayload,
|
||||
UpdateSalesOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
DeliveryOrderFormValues,
|
||||
DeliveryOrderSchema,
|
||||
SalesOrderFormValues,
|
||||
SalesOrderSchema,
|
||||
} from './MarketingForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import {
|
||||
DeliveryOrderApi,
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import SalesOrderProductTable from './table-view/SalesOrderProductTable';
|
||||
import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm';
|
||||
import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
|
||||
import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
|
||||
import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
|
||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
|
||||
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
id: product.id,
|
||||
vehicle_number: product.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const DeliveryProductToFieldValues = (
|
||||
salesOrders: BaseSalesOrder[],
|
||||
delivery: BaseDeliveryOrder
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
const data = delivery.deliveries.map((item) => {
|
||||
const soId = salesOrders.find(
|
||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||
)?.id;
|
||||
return {
|
||||
id: soId,
|
||||
unit_price: item.unit_price,
|
||||
total_weight: item.total_weight,
|
||||
qty: item.qty,
|
||||
avg_weight: item.avg_weight,
|
||||
total_price: item.total_price,
|
||||
vehicle_number: item.vehicle_number,
|
||||
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
||||
do_number: delivery.do_number,
|
||||
marketing_product_id: soId,
|
||||
marketing_product: {
|
||||
id: soId,
|
||||
vehicle_number: item.vehicle_number,
|
||||
kandang_id: item.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: item.product_warehouse.warehouse.id,
|
||||
label: item.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: item.product_warehouse.id,
|
||||
label: item.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: item.product_warehouse.id,
|
||||
unit_price: item.unit_price,
|
||||
total_weight: item.total_weight,
|
||||
qty: item.qty,
|
||||
avg_weight: item.avg_weight,
|
||||
total_price: item.total_price,
|
||||
},
|
||||
} as DeliveryOrderProductFormValues;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const mergeSOwithDO = (
|
||||
salesOrders: SalesOrderProductFormValues[],
|
||||
deliveryOrders: DeliveryOrderProductFormValues[]
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
return salesOrders.map((so) => {
|
||||
const delivery = deliveryOrders.find(
|
||||
(d) => d?.marketing_product_id === so.id
|
||||
);
|
||||
|
||||
return {
|
||||
...so, // nilai dasar dari sales order
|
||||
marketing_product_id: so.id,
|
||||
delivery_date: delivery?.delivery_date || undefined,
|
||||
do_number: delivery?.do_number || undefined,
|
||||
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
||||
unit_price: delivery?.unit_price ?? so.unit_price,
|
||||
total_weight: delivery?.total_weight ?? so.total_weight,
|
||||
qty: delivery?.qty ?? so.qty,
|
||||
avg_weight: delivery?.avg_weight ?? so.avg_weight,
|
||||
total_price: delivery?.total_price ?? so.total_price,
|
||||
marketing_product: so, // jika ada, override
|
||||
} as DeliveryOrderProductFormValues;
|
||||
});
|
||||
};
|
||||
|
||||
const MarketingForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
afterSubmit,
|
||||
}: {
|
||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
initialValues?: Marketing;
|
||||
afterSubmit?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||
useState<SalesOrderProductFormValues | null>(null);
|
||||
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
|
||||
useState<DeliveryOrderProductFormValues | null>(null);
|
||||
|
||||
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
|
||||
DeliveryOrderProductFormValues[]
|
||||
>(
|
||||
mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
// Repeater Props
|
||||
const addSOModal = useModal();
|
||||
const addDOModal = useModal();
|
||||
const [rowSOSelection, setRowSOSelection] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const selectedRowSOIds = Object.keys(rowSOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
const [rowDOSelection, setRowDOSelection] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const selectedRowDOIds = Object.keys(rowDOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
|
||||
// End Repeater Props
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const formikInitialValues = useMemo<
|
||||
SalesOrderFormValues & DeliveryOrderFormValues
|
||||
>(() => {
|
||||
return {
|
||||
so_date: initialValues?.so_date || undefined,
|
||||
notes: initialValues?.notes || undefined,
|
||||
customer_id: initialValues?.customer?.id || undefined,
|
||||
sales_person_id: initialValues?.sales_person?.id || 1,
|
||||
customer: initialValues?.customer
|
||||
? {
|
||||
value: initialValues.customer.id,
|
||||
label: initialValues.customer.name,
|
||||
}
|
||||
: null,
|
||||
sales_order:
|
||||
initialValues?.sales_order?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
delivery_order: mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
),
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema:
|
||||
formType == 'add_deliver' || formType == 'edit_deliver'
|
||||
? DeliveryOrderSchema
|
||||
: SalesOrderSchema,
|
||||
validateOnMount: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload =
|
||||
formType != 'add_deliver' && formType != 'edit_deliver'
|
||||
? ({
|
||||
customer_id: values.customer_id as number,
|
||||
sales_person_id: values.sales_person_id as number,
|
||||
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
|
||||
notes: values.notes as string,
|
||||
marketing_products: values.sales_order.map((product) => {
|
||||
return {
|
||||
vehicle_number: product.vehicle_number as string,
|
||||
kandang_id: product.kandang_id as number,
|
||||
product_warehouse_id: product.product_warehouse_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
: ({
|
||||
marketing_id: initialValues?.id as number,
|
||||
delivery_products: values.delivery_order
|
||||
.map((product) => {
|
||||
if (Boolean(product.delivery_date)) {
|
||||
return {
|
||||
marketing_product_id:
|
||||
product.marketing_product_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
delivery_date: formatDate(
|
||||
product.delivery_date as string,
|
||||
'yyyy-MM-DD'
|
||||
),
|
||||
vehicle_number: product.vehicle_number,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => Boolean(item)),
|
||||
} as UpdateDeliveryOrderPayload);
|
||||
console.log('PAYLOAD');
|
||||
console.log(payload);
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||
break;
|
||||
case 'edit':
|
||||
await updateMarketingHandler(payload as UpdateSalesOrderPayload);
|
||||
break;
|
||||
case 'add_deliver':
|
||||
await createDeliveryHandler(payload as CreateDeliveryOrderPayload);
|
||||
break;
|
||||
case 'edit_deliver':
|
||||
await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
afterSubmit?.();
|
||||
},
|
||||
});
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return formik.values.sales_order.reduce(
|
||||
(total, product) =>
|
||||
total + parseFloat((product.total_price as string) || '0'),
|
||||
0
|
||||
);
|
||||
}, [formik.values.sales_order]);
|
||||
|
||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const createMarketingRes = await SalesOrderApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
toast.success(createMarketingRes?.message as string);
|
||||
router.push('/marketing');
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
toast.error(createMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const updateMarketingRes = await SalesOrderApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(updateMarketingRes)) {
|
||||
toast.success(updateMarketingRes?.message as string);
|
||||
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
}
|
||||
if (isResponseError(updateMarketingRes)) {
|
||||
toast.error(updateMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const createDeliveryRes = await DeliveryOrderApi.create(values);
|
||||
if (isResponseSuccess(createDeliveryRes)) {
|
||||
console.log(createDeliveryRes);
|
||||
toast.success(createDeliveryRes?.message as string);
|
||||
setDeliveryOrderValues(
|
||||
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
createDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
);
|
||||
}
|
||||
if (isResponseError(createDeliveryRes)) {
|
||||
console.log(createDeliveryRes);
|
||||
toast.error(createDeliveryRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const updateDeliveryRes = await DeliveryOrderApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
// router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
setDeliveryOrderValues(
|
||||
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
updateDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.error(updateDeliveryRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
initialValues?.id as number
|
||||
);
|
||||
if (isResponseSuccess(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
toast.success(deleteMarketingRes?.message as string);
|
||||
}
|
||||
if (isResponseError(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
toast.error(deleteMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// Repeater Handle
|
||||
const handleDeleteSO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter((p) => p.id != id)
|
||||
);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleBulkDeleteSO = useCallback(() => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter(
|
||||
(product) => !selectedRowSOIds.includes(product.id ?? -1)
|
||||
)
|
||||
);
|
||||
setRowSOSelection({});
|
||||
}, [formik, selectedRowSOIds]);
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteModal.openModal();
|
||||
}, [deleteModal]);
|
||||
const handleAddSOClick = useCallback(() => {
|
||||
setSelectedMarketingProduct(null);
|
||||
addSOModal.openModal();
|
||||
}, [addSOModal]);
|
||||
const handleAddSubmitSO = useCallback(
|
||||
async (values: SalesOrderProductFormValues) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
|
||||
|
||||
addSOModal.closeModal();
|
||||
},
|
||||
[formik, addSOModal]
|
||||
);
|
||||
|
||||
const handleDeleteDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order;
|
||||
setDeliveryOrderValues((prev) => prev.filter((p) => p.id !== id));
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleEditDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order.find(
|
||||
(product) => product.id == id
|
||||
);
|
||||
setSelectedDeliveryProduct(currentProducts ?? null);
|
||||
addDOModal.openModal();
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleBulkDeleteDO = useCallback(() => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.filter((product) => !selectedRowDOIds.includes(product.id ?? -1))
|
||||
);
|
||||
|
||||
setRowDOSelection({});
|
||||
}, [formik, selectedRowDOIds]);
|
||||
|
||||
const handleAddDOClick = useCallback(() => {
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.openModal();
|
||||
}, [addDOModal]);
|
||||
|
||||
const handleAddSubmitDO = useCallback(
|
||||
async (values: DeliveryOrderProductFormValues) => {
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
setDeliveryOrderValues((prev) => [...prev, newValues]);
|
||||
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
const handleInputDate = useCallback(
|
||||
(newData: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.marketing_product_id == newData.marketing_product_id) {
|
||||
return newData;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, ...values } : product
|
||||
)
|
||||
);
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
// End Repeater Handle
|
||||
|
||||
const memoSalesOrder = formik.values.sales_order;
|
||||
|
||||
useEffect(() => {
|
||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||
}, [deliveryOrderValues, initialValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
>
|
||||
<FormHeader
|
||||
title={`${formType == 'add' || formType == 'add_deliver' ? 'Tambah' : 'Edit'} ${formType === 'add_deliver' || formType === 'edit_deliver' ? 'Delivery' : 'Sales'} Order`}
|
||||
backUrl='/marketing'
|
||||
/>
|
||||
<Card
|
||||
title='Informasi Order'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-3 mt-3'>
|
||||
<SelectInput
|
||||
label='Pelanggan'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={formik.values.customer}
|
||||
onChange={handleChangeCustomer}
|
||||
isError={
|
||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||
}
|
||||
errorMessage={formik.errors.customer_id}
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
isDisabled={
|
||||
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||
}
|
||||
/>
|
||||
<DateInput
|
||||
name='so_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.so_date}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
|
||||
errorMessage={formik.errors.so_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
readOnly={formType == 'add_deliver' || formType == 'edit_deliver'}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{(formType == 'add' || formType == 'edit') && (
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
|
||||
<div className='text-green-500'>{JSON.stringify(formik.values)}</div>
|
||||
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={formType}
|
||||
data={memoSalesOrder}
|
||||
rowSelection={rowSOSelection}
|
||||
setRowSelection={setRowSOSelection}
|
||||
selectedRowIds={selectedRowSOIds}
|
||||
onDelete={handleDeleteSO}
|
||||
onBulkDelete={handleBulkDeleteSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{(formType == 'add_deliver' || formType == 'edit_deliver') &&
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order.length > 0 && (
|
||||
<Card
|
||||
title='Informasi Pengiriman'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* {JSON.stringify(memoSalesOrder)} */}
|
||||
{/* <small>{JSON.stringify(memoDeliveryOrder)}</small> */}
|
||||
{/* <small className='block text-error'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
formType={formType}
|
||||
data={deliveryOrderValues}
|
||||
salesOrder={memoSalesOrder}
|
||||
rowSelection={rowDOSelection}
|
||||
setRowSelection={setRowDOSelection}
|
||||
selectedRowIds={selectedRowDOIds}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onBulkDelete={handleBulkDeleteDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
onInputDate={handleInputDate}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
required
|
||||
name='notes'
|
||||
label='Catatan'
|
||||
rows={3}
|
||||
placeholder='Masukan catatan penjualan'
|
||||
value={formik.values.notes}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
|
||||
/>
|
||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
||||
<span>Total Penjualan</span>
|
||||
<span className='text-lg font-semibold'>
|
||||
{formatCurrency(grandTotal)}{' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
|
||||
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
isLoading={formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{formType == 'edit' && (
|
||||
<div className='flex flex-row justify-start'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={handleDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
ref={addSOModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addSOModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedSalesOrderProductForm
|
||||
onSubmitForm={handleAddSubmitSO}
|
||||
initialValues={selectedMarketingProduct ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
ref={addDOModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
{selectedDeliveryProduct ? 'Edit' : 'Tambah'} Pengiriman
|
||||
</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addDOModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedDeliveryOrderProductForm
|
||||
salesOrders={initialValues?.sales_order ?? []}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: deleteModal.closeModal,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
onClick: deleteMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingForm;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '../sales-order/SalesOrderProduct.schema';
|
||||
import { de } from 'react-day-picker/locale';
|
||||
|
||||
type DeliveryOrderProductSchemaType = {
|
||||
id?: number | undefined;
|
||||
marketing_product_id: number | undefined; // Sales Order ID
|
||||
marketing_product?: SalesOrderProductFormValues | undefined | null;
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
vehicle_number: string | undefined;
|
||||
delivery_date: string | undefined | null;
|
||||
do_number?: string | undefined | null; // Uncertain
|
||||
};
|
||||
|
||||
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
|
||||
Yup.object({
|
||||
id: Yup.number(),
|
||||
marketing_product_id: Yup.number()
|
||||
.min(1, 'Produk wajib diisi!')
|
||||
.required('Produk wajib diisi!'),
|
||||
marketing_product: Yup.object().nullable().optional(),
|
||||
unit_price: Yup.number()
|
||||
.min(1, 'Harga Satuan wajib diisi!')
|
||||
.required('Harga Satuan wajib diisi!'),
|
||||
total_weight: Yup.number()
|
||||
.min(0, 'Total Bobot wajib diisi!')
|
||||
.required('Total Bobot wajib diisi!'),
|
||||
qty: Yup.number()
|
||||
.min(1, 'Kuantitas wajib diisi!')
|
||||
.required('Kuantitas wajib diisi!'),
|
||||
avg_weight: Yup.number()
|
||||
.min(0, 'Avg. Bobot wajib diisi!')
|
||||
.required('Avg. Bobot wajib diisi!'),
|
||||
total_price: Yup.number()
|
||||
.min(1, 'Total Penjualan wajib diisi!')
|
||||
.required('Total Penjualan wajib diisi!'),
|
||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||
delivery_date: Yup.string()
|
||||
.required('Tanggal Pengiriman wajib diisi!')
|
||||
.nullable()
|
||||
.optional(),
|
||||
do_number: Yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type DeliveryOrderProductFormValues = Yup.InferType<
|
||||
typeof DeliveryOrderProductSchema
|
||||
>;
|
||||
|
||||
// "marketing_product_id": 3,
|
||||
// "qty": 20,
|
||||
// "unit_price": 1000,
|
||||
// "avg_weight": 1.1,
|
||||
// "total_weight": 220,
|
||||
// "total_price": 20000,
|
||||
// "delivery_date": "2025-11-09",
|
||||
// "vehicle_number": "D 4321 XXX"
|
||||
@@ -0,0 +1,354 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DeliveryOrderProductFormValues,
|
||||
DeliveryOrderProductSchema,
|
||||
} from './DeliverOrderProduct.schema';
|
||||
import { useFormik } from 'formik';
|
||||
import Alert from '@/components/Alert';
|
||||
import Button from '@/components/Button';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import { formatVechicleNumber } from '@/lib/helper';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { SalesOrderProductFormValues } from '../sales-order/SalesOrderProduct.schema';
|
||||
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
const DeliveryOrderProductForm = ({
|
||||
salesOrders,
|
||||
initialValues,
|
||||
onSubmitForm,
|
||||
onUpdateForm,
|
||||
}: {
|
||||
salesOrders: BaseSalesOrder[];
|
||||
initialValues?: DeliveryOrderProductFormValues;
|
||||
onSubmitForm?: (value: DeliveryOrderProductFormValues) => Promise<void>;
|
||||
onUpdateForm?: (
|
||||
id: number,
|
||||
value: DeliveryOrderProductFormValues
|
||||
) => Promise<void>;
|
||||
}) => {
|
||||
const [formikErrorMessage, setFormErrorMessage] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const formik = useFormik<DeliveryOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
delivery_date: initialValues?.delivery_date || undefined,
|
||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
||||
marketing_product_id: initialValues?.marketing_product_id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
marketing_product: initialValues?.marketing_product || undefined,
|
||||
},
|
||||
validationSchema: DeliveryOrderProductSchema,
|
||||
validateOnBlur: true,
|
||||
validateOnChange: false,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (initialValues?.id) {
|
||||
await onUpdateForm?.(initialValues.id, values);
|
||||
} else {
|
||||
await onSubmitForm?.(values);
|
||||
}
|
||||
handleResetForm();
|
||||
},
|
||||
});
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
values: {
|
||||
delivery_date: '',
|
||||
vehicle_number: '',
|
||||
marketing_product_id: undefined,
|
||||
unit_price: '',
|
||||
total_weight: '',
|
||||
qty: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
marketing_product: undefined,
|
||||
},
|
||||
});
|
||||
setSelectedProduct(null);
|
||||
};
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
||||
formik.values;
|
||||
|
||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
||||
formik.setFieldValue('total_price', Number(qty) * Number(unit_price));
|
||||
} else if (qty && total_price && field === 'total_price') {
|
||||
formik.setFieldValue('unit_price', Number(total_price) / Number(qty));
|
||||
}
|
||||
}
|
||||
|
||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
||||
formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight));
|
||||
} else if (qty && total_weight && field === 'total_weight') {
|
||||
formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
id: product.id,
|
||||
vehicle_number: product.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const options = salesOrders.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.product_warehouse.product.name} - ${item.product_warehouse.warehouse.name}`,
|
||||
}));
|
||||
|
||||
const { setValues: setFormikValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setFormikValues(initialValues);
|
||||
const value = salesOrders.find(
|
||||
(item) => item.id === initialValues.marketing_product_id
|
||||
);
|
||||
setSelectedProduct({
|
||||
value: value?.id,
|
||||
label: `${value?.product_warehouse.product.name} - ${value?.product_warehouse.warehouse.name}`,
|
||||
} as OptionType);
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='size-full'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{/* <small className='block text-blue-500'>
|
||||
{JSON.stringify(initialValues)}
|
||||
</small>
|
||||
<small className='block text-red-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small>
|
||||
<small className='block text-emerald-500'>
|
||||
{JSON.stringify(formik.values)}
|
||||
</small>
|
||||
<div className='hidden'>
|
||||
{JSON.stringify(formik.values.marketing_product)}
|
||||
</div> */}
|
||||
|
||||
{formikErrorMessage && (
|
||||
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
|
||||
<Alert color='error'>{formikErrorMessage}</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<SelectInput
|
||||
options={options}
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
isDisabled
|
||||
value={
|
||||
selectedProduct
|
||||
? ({
|
||||
value: selectedProduct?.value,
|
||||
label: salesOrders.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse.product.name,
|
||||
} as OptionType)
|
||||
: null
|
||||
}
|
||||
onChange={(value) => {
|
||||
const selected = value as OptionType;
|
||||
setSelectedProduct(selected);
|
||||
|
||||
const so = salesOrders.find(
|
||||
(item) => item.id === selected?.value
|
||||
);
|
||||
if (!so) {
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: undefined,
|
||||
marketing_product: null,
|
||||
qty: formik.values.qty || '',
|
||||
unit_price: '',
|
||||
total_price: '',
|
||||
avg_weight: '',
|
||||
total_weight: '',
|
||||
vehicle_number: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: selected.value as number,
|
||||
marketing_product: MarketingProductToFieldValues(so),
|
||||
qty: formik.values.qty || so.qty,
|
||||
unit_price: so.unit_price,
|
||||
total_price: so.total_price,
|
||||
avg_weight: so.avg_weight,
|
||||
total_weight: so.total_weight,
|
||||
vehicle_number: so.vehicle_number,
|
||||
});
|
||||
}}
|
||||
startAdornment={
|
||||
selectedProduct && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
className={{ badge: 'whitespace-nowrap font-semibold' }}
|
||||
>
|
||||
{
|
||||
salesOrders.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse?.warehouse?.name
|
||||
}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
isError={Boolean(formik.errors.marketing_product_id)}
|
||||
errorMessage={formik.errors.marketing_product_id}
|
||||
required
|
||||
/>
|
||||
<DateInput
|
||||
name='delivery_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.delivery_date ?? undefined}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.delivery_date &&
|
||||
Boolean(formik.errors.delivery_date)
|
||||
}
|
||||
errorMessage={formik.errors.delivery_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
required
|
||||
/>
|
||||
|
||||
<PatternInput
|
||||
name='vehicle_number'
|
||||
label='No. Polisi'
|
||||
format='AA #### AAA'
|
||||
mask='_'
|
||||
inputVehicleNumber
|
||||
required
|
||||
type='text'
|
||||
placeholder='B 1234 CDE'
|
||||
value={formatVechicleNumber(formik.values.vehicle_number ?? '')}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={Boolean(formik.errors.vehicle_number)}
|
||||
errorMessage={formik.errors.vehicle_number}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Kuantitas'
|
||||
name='qty'
|
||||
value={formik.values.qty}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('qty')}
|
||||
isError={Boolean(formik.errors.qty)}
|
||||
errorMessage={formik.errors.qty}
|
||||
placeholder='Masukan Kuantitas'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Avg. Bobot (Kg)'
|
||||
name='avg_weight'
|
||||
value={formik.values.avg_weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('avg_weight')}
|
||||
isError={Boolean(formik.errors.avg_weight)}
|
||||
errorMessage={formik.errors.avg_weight}
|
||||
placeholder='Masukan Bobot Rata-rata'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Harga Satuan (Rp)'
|
||||
name='unit_price'
|
||||
value={formik.values.unit_price}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('unit_price')}
|
||||
isError={Boolean(formik.errors.unit_price)}
|
||||
errorMessage={formik.errors.unit_price}
|
||||
placeholder='Masukan Harga Satuan'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Total Bobot (Kg)'
|
||||
name='total_weight'
|
||||
value={formik.values.total_weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('total_weight')}
|
||||
isError={Boolean(formik.errors.total_weight)}
|
||||
errorMessage={formik.errors.total_weight}
|
||||
placeholder='Masukan Total Bobot'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Total Penjualan (Rp)'
|
||||
name='total_price'
|
||||
value={formik.values.total_price}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('total_price')}
|
||||
isError={Boolean(formik.errors.total_price)}
|
||||
errorMessage={formik.errors.total_price}
|
||||
placeholder='Masukan Total Penjualan'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end gap-3 mt-4'>
|
||||
<Button type='reset' color='warning'>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryOrderProductForm;
|
||||
+10
-12
@@ -1,7 +1,7 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type MarketingProductSchemaType = {
|
||||
vehicle_number: string | undefined;
|
||||
type SalesOrderProductSchemaType = {
|
||||
id?: number | undefined;
|
||||
kandang_id?: number;
|
||||
kandang?: {
|
||||
value: number;
|
||||
@@ -15,15 +15,15 @@ type MarketingProductSchemaType = {
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
uom: string | undefined | null;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
delivery_date?: string | undefined | null;
|
||||
vehicle_number?: string | undefined;
|
||||
};
|
||||
|
||||
export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType> =
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
Yup.object({
|
||||
vehicle_number: Yup.string().required('No. Polisi wajib diisi!'),
|
||||
id: Yup.number(),
|
||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||
kandang: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
@@ -42,21 +42,19 @@ export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType
|
||||
.min(1, 'Harga Satuan wajib diisi!')
|
||||
.required('Harga Satuan wajib diisi!'),
|
||||
total_weight: Yup.number()
|
||||
.min(1, 'Total Bobot wajib diisi!')
|
||||
.min(0, 'Total Bobot wajib diisi!')
|
||||
.required('Total Bobot wajib diisi!'),
|
||||
qty: Yup.number()
|
||||
.min(1, 'Kuantitas wajib diisi!')
|
||||
.required('Kuantitas wajib diisi!'),
|
||||
uom: Yup.string().nullable(),
|
||||
avg_weight: Yup.number()
|
||||
.min(1, 'Avg. Bobot wajib diisi!')
|
||||
.min(0, 'Avg. Bobot wajib diisi!')
|
||||
.required('Avg. Bobot wajib diisi!'),
|
||||
total_price: Yup.number()
|
||||
.min(1, 'Total Penjualan wajib diisi!')
|
||||
.required('Total Penjualan wajib diisi!'),
|
||||
delivery_date: Yup.string().required().nullable(),
|
||||
});
|
||||
|
||||
export type MarketingProductFormValues = Yup.InferType<
|
||||
typeof MarketingProductSchema
|
||||
export type SalesOrderProductFormValues = Yup.InferType<
|
||||
typeof SalesOrderProductSchema
|
||||
>;
|
||||
+59
-123
@@ -1,17 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
} from './MarketingProduct.schema';
|
||||
import { RefObject, use, useEffect, useRef, useState } from 'react';
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import { RefObject, useState } from 'react';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
@@ -23,37 +17,47 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import Button from '@/components/Button';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import { formatVechicleNumber } from '@/lib/helper';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import Alert from '@/components/Alert';
|
||||
|
||||
const MarketingProductForm = ({
|
||||
const SalesOrderProductForm = ({
|
||||
initialValues,
|
||||
data,
|
||||
modalRef,
|
||||
onSubmitForm,
|
||||
}: {
|
||||
initialValues?: MarketingProduct;
|
||||
data: MarketingProduct[];
|
||||
initialValues?: SalesOrderProductFormValues;
|
||||
modalRef?: RefObject<HTMLDialogElement | null>;
|
||||
onSubmitForm?: (
|
||||
tableValues: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
) => Promise<void>;
|
||||
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
|
||||
}) => {
|
||||
// State
|
||||
const [selectedOptionsKandang, setSelectedOptionsKandang] =
|
||||
useState<OptionType | null>(null);
|
||||
const [selectedOptionsWarehouse, setSelectedOptionsWarehouse] = useState<
|
||||
OptionType | null | undefined
|
||||
>(undefined);
|
||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||
|
||||
// Options Data
|
||||
const formik = useFormik<SalesOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
||||
kandang_id: initialValues?.kandang_id || undefined,
|
||||
kandang: initialValues?.kandang || undefined,
|
||||
product_warehouse: initialValues?.product_warehouse || undefined,
|
||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
},
|
||||
validationSchema: SalesOrderProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
onSubmitForm?.(values);
|
||||
handleResetForm();
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
options: kandangSourceOptions,
|
||||
rawData: kandangSourceRawData,
|
||||
isLoadingOptions: isLoadingKandangSourceOptions,
|
||||
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: warehouseSourceOptions,
|
||||
rawData: warehouseSourceRawData,
|
||||
@@ -64,109 +68,35 @@ const MarketingProductForm = ({
|
||||
'product.name',
|
||||
'search',
|
||||
{
|
||||
warehouse_id: selectedOptionsKandang?.value?.toString() ?? '',
|
||||
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
||||
}
|
||||
);
|
||||
|
||||
// Handler
|
||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedOptionsKandang(val as OptionType);
|
||||
formik.setFieldValue('kandang', val as OptionType);
|
||||
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('product_warehouse_id', null);
|
||||
formik.setFieldValue('product_warehouse', null);
|
||||
formik.setFieldValue('qty', null);
|
||||
warehouseChangeHandler(null);
|
||||
};
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedOptionsWarehouse(val as OptionType);
|
||||
formik.setFieldValue('product_warehouse', val as OptionType);
|
||||
formik.setFieldValue('product_warehouse_id', (val as OptionType)?.value);
|
||||
if (isResponseSuccess(warehouseSourceRawData)) {
|
||||
const newId = (val as OptionType)?.value;
|
||||
formik.setFieldValue('product_warehouse_id', newId);
|
||||
|
||||
if (isResponseSuccess(warehouseSourceRawData) && newId) {
|
||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||
(item: ProductWarehouse) => item.id === (val as OptionType)?.value
|
||||
(item: ProductWarehouse) => item.id === newId
|
||||
);
|
||||
if (selectedOptionsWarehouse?.value !== null) {
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', null);
|
||||
}
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', null);
|
||||
}
|
||||
};
|
||||
|
||||
// Formik
|
||||
const formik = useFormik<MarketingProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
vehicle_number:
|
||||
initialValues?.marketing_delivery_products?.vehicle_number || undefined,
|
||||
kandang_id: initialValues?.product_warehouse.warehouse.id || undefined,
|
||||
kandang: {
|
||||
value: initialValues?.product_warehouse.warehouse.id as number,
|
||||
label: initialValues?.product_warehouse.warehouse.name as string,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: initialValues?.product_warehouse.product.id as number,
|
||||
label: initialValues?.product_warehouse.product.name as string,
|
||||
},
|
||||
product_warehouse_id:
|
||||
initialValues?.product_warehouse.product.id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
uom: initialValues?.product_warehouse?.product?.uom?.name || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
delivery_date:
|
||||
initialValues?.marketing_delivery_products?.delivery_date ||
|
||||
new Date().toDateString() ||
|
||||
undefined,
|
||||
},
|
||||
validationSchema: MarketingProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (
|
||||
isResponseSuccess(kandangSourceRawData) &&
|
||||
isResponseSuccess(warehouseSourceRawData)
|
||||
) {
|
||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||
(item: ProductWarehouse) => item.id === values.product_warehouse_id
|
||||
);
|
||||
const kandang = kandangSourceRawData?.data.find(
|
||||
(item: Kandang) => item.id === values.kandang_id
|
||||
);
|
||||
|
||||
const marketingProduct: CreateMarketingProductPayload = {
|
||||
id: initialValues?.id || undefined,
|
||||
vehicle_number: formatVechicleNumber(values.vehicle_number as string),
|
||||
kandang_id: values.kandang_id as number,
|
||||
kandang: kandang,
|
||||
product_warehouse_id: values.product_warehouse_id as number,
|
||||
product_warehouse: productWarehouse,
|
||||
unit_price: values.unit_price as number,
|
||||
total_weight: values.total_weight as number,
|
||||
qty: values.qty as number,
|
||||
uom: values.uom as string,
|
||||
avg_weight: values.avg_weight as number,
|
||||
total_price: values.total_price as number,
|
||||
delivery_date: values.delivery_date as string,
|
||||
};
|
||||
|
||||
onSubmitForm?.(marketingProduct, values);
|
||||
handleResetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formik.initialValues);
|
||||
}, [formikSetValues, formik.initialValues]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setSelectedOptionsKandang(null);
|
||||
setSelectedOptionsWarehouse(null);
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
values: {
|
||||
@@ -178,10 +108,8 @@ const MarketingProductForm = ({
|
||||
unit_price: '',
|
||||
total_weight: '',
|
||||
qty: '',
|
||||
uom: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
delivery_date: new Date().toDateString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -191,7 +119,7 @@ const MarketingProductForm = ({
|
||||
formik.values;
|
||||
|
||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
||||
if (qty && unit_price && field === 'unit_price') {
|
||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
||||
formik.setFieldValue(
|
||||
'total_price',
|
||||
(qty as number) * (unit_price as number)
|
||||
@@ -205,7 +133,7 @@ const MarketingProductForm = ({
|
||||
}
|
||||
|
||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
||||
if (qty && avg_weight && field === 'avg_weight') {
|
||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
||||
formik.setFieldValue(
|
||||
'total_weight',
|
||||
(qty as number) * (avg_weight as number)
|
||||
@@ -226,6 +154,16 @@ const MarketingProductForm = ({
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{formErrorMessage && (
|
||||
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
|
||||
<Alert color='error'>
|
||||
{formErrorMessage ? formErrorMessage : ''}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{/* <small className='block text-rose-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
<div className='grid grid-cols-2 gap-4 z-200'>
|
||||
<PatternInput
|
||||
name='vehicle_number'
|
||||
@@ -250,10 +188,9 @@ const MarketingProductForm = ({
|
||||
label='Kandang'
|
||||
options={kandangSourceOptions}
|
||||
isLoading={isLoadingKandangSourceOptions}
|
||||
value={selectedOptionsKandang}
|
||||
value={formik.values.kandang}
|
||||
onChange={kandangChangeHandler}
|
||||
isClearable
|
||||
menuPortalTarget={modalRef?.current}
|
||||
isError={
|
||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||
}
|
||||
@@ -265,12 +202,11 @@ const MarketingProductForm = ({
|
||||
label='Produk'
|
||||
options={warehouseSourceOptions}
|
||||
isLoading={isLoadingWarehouseSourceOptions}
|
||||
value={selectedOptionsWarehouse}
|
||||
value={formik.values.product_warehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
isClearable
|
||||
menuPortalTarget={modalRef?.current}
|
||||
placeholder='Pilih Kandang Terlebih Dahulu'
|
||||
isDisabled={!selectedOptionsKandang?.value}
|
||||
isDisabled={!formik.values.kandang_id}
|
||||
isError={
|
||||
formik.touched.product_warehouse_id &&
|
||||
Boolean(formik.errors.product_warehouse_id)
|
||||
@@ -358,4 +294,4 @@ const MarketingProductForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingProductForm;
|
||||
export default SalesOrderProductForm;
|
||||
@@ -0,0 +1,280 @@
|
||||
import Table from '@/components/Table';
|
||||
import { DeliveryOrderProductFormValues } from '../repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import Button from '@/components/Button';
|
||||
import { Icon } from '@iconify/react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { SalesOrderProductFormValues } from '../repeater/sales-order/SalesOrderProduct.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
|
||||
type DeliveryOrderProductTableProps = {
|
||||
data: DeliveryOrderProductFormValues[];
|
||||
salesOrder: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
selectedRowIds: number[];
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (id: number) => void;
|
||||
onBulkDelete: () => void;
|
||||
onAddProductClick: () => void;
|
||||
onInputDate: (data: DeliveryOrderProductFormValues) => void;
|
||||
};
|
||||
|
||||
const DeliveryOrderProductTable = ({
|
||||
data,
|
||||
salesOrder,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
onInputDate,
|
||||
}: DeliveryOrderProductTableProps) => {
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
const onEditRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
onEditRef.current = onEdit;
|
||||
|
||||
const canAddData = salesOrder.reduce((acc, curr) => {
|
||||
const deliveredQty = data.filter(
|
||||
(deliveryItem) => deliveryItem.marketing_product_id == curr.id
|
||||
);
|
||||
return acc && deliveredQty.length != salesOrder.length;
|
||||
}, true);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cols = [
|
||||
// {
|
||||
// id: 'select',
|
||||
// header: ({
|
||||
// table,
|
||||
// }: {
|
||||
// table: TanStack.Table<DeliveryOrderProductFormValues>;
|
||||
// }) => (
|
||||
// <div className='w-full flex flex-row justify-center'>
|
||||
// <CheckboxInput
|
||||
// name='allRow'
|
||||
// checked={table.getIsAllRowsSelected()}
|
||||
// indeterminate={table.getIsSomeRowsSelected()}
|
||||
// onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// cell: ({
|
||||
// row,
|
||||
// }: {
|
||||
// row: TanStack.Row<DeliveryOrderProductFormValues>;
|
||||
// }) => (
|
||||
// <div>
|
||||
// <CheckboxInput
|
||||
// name='row'
|
||||
// checked={row.getIsSelected()}
|
||||
// disabled={!row.getCanSelect()}
|
||||
// indeterminate={row.getIsSomeSelected()}
|
||||
// onChange={row.getToggleSelectedHandler()}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
|
||||
header: 'No. Pengiriman',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => <div>{props.row.original.do_number}</div>,
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.delivery_date
|
||||
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
|
||||
: '-',
|
||||
header: 'Tanggal Delivery',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<>
|
||||
{formType == 'add_deliver' && (
|
||||
<DateInput
|
||||
name={`delivery_date_${props.row.original.marketing_product_id}`}
|
||||
className={{
|
||||
input: 'p-0',
|
||||
inputWrapper: 'py-1 px-3 h-fit w-fit bg-white',
|
||||
wrapper: 'p-0',
|
||||
}}
|
||||
value={
|
||||
props.row.original.delivery_date
|
||||
? formatDate(props.row.original.delivery_date, 'yyyy-MM-DD')
|
||||
: undefined
|
||||
}
|
||||
onChange={(val) => {
|
||||
onInputDate({
|
||||
...props.row.original,
|
||||
delivery_date: val.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formType == 'edit_deliver' &&
|
||||
formatDate(
|
||||
props.row.original.delivery_date as string,
|
||||
'DD MMM YYYY'
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.marketing_product?.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.marketing_product?.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string)),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string)),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<>
|
||||
<Button
|
||||
color='warning'
|
||||
className='px-2 py-1 text-sm'
|
||||
onClick={() =>
|
||||
onEditRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
{/* <Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button> */}
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (formType == 'add_deliver') {
|
||||
return cols.filter(
|
||||
(col) => col.header != 'Aksi' && col.header != 'No. Pengiriman'
|
||||
);
|
||||
}
|
||||
return cols;
|
||||
}, [formType, onInputDate, onEditRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<DeliveryOrderProductFormValues>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={columns}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data pengiriman</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
{/* <Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onAddProductClick}
|
||||
// disabled={!canAddData}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Pengiriman
|
||||
</Button> */}
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onBulkDelete}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Pengiriman
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryOrderProductTable;
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Table from '@/components/Table';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
|
||||
type SalesOrderProductTableProps = {
|
||||
data: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
selectedRowIds: number[];
|
||||
onDelete: (id: number) => void;
|
||||
onBulkDelete: () => void;
|
||||
onAddProductClick: () => void;
|
||||
};
|
||||
|
||||
const SalesOrderProductTable = ({
|
||||
data,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
}: SalesOrderProductTableProps) => {
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({
|
||||
table,
|
||||
}: {
|
||||
table: TanStack.Table<SalesOrderProductFormValues>;
|
||||
}) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
row.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string)),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string)),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<SalesOrderProductFormValues>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={
|
||||
formType == 'deliver'
|
||||
? columns.filter(
|
||||
(col) => col.header != 'Aksi' && col.id != 'select'
|
||||
)
|
||||
: columns
|
||||
}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data penjualan</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{formType != 'deliver' && (
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onAddProductClick}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onBulkDelete}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Produk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesOrderProductTable;
|
||||
@@ -0,0 +1,235 @@
|
||||
import Button from '@/components/Button';
|
||||
import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import pdfStyles from './styles/MarketingPDFStyles';
|
||||
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||
import { format } from 'path';
|
||||
import { date } from 'yup';
|
||||
|
||||
interface DeliveryOrderExportProps {
|
||||
data?: Marketing;
|
||||
deliveryOrder: BaseDeliveryOrder;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DeliveryOrderExport = ({
|
||||
data,
|
||||
deliveryOrder,
|
||||
}: DeliveryOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const blob = await pdf(
|
||||
<PDFDocument data={salesData} deliveryOrder={deliveryOrder} />
|
||||
).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${deliveryOrder?.do_number || 'delivery-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!salesData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No sales order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
isLoading={isGeneratingPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{deliveryOrder.do_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default DeliveryOrderExport;
|
||||
const PDFDocument = ({
|
||||
data,
|
||||
deliveryOrder,
|
||||
}: {
|
||||
data: Marketing;
|
||||
deliveryOrder: BaseDeliveryOrder;
|
||||
}) => {
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Delivery Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>DELIVERY ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>{deliveryOrder.do_number || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Depature Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Ship To</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Depature From</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{data?.customer.email || ''} - {data?.customer.phone || ''}
|
||||
</Text>
|
||||
<Text></Text>
|
||||
<Text>{data?.customer.address || ''}</Text>
|
||||
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{deliveryOrder.warehouse?.name || '-'}
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{formatDate(deliveryOrder.delivery_date, 'DD MMM YYYY')}
|
||||
</Text>
|
||||
<Text>{deliveryOrder.warehouse?.area?.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Delivery Table */}
|
||||
<Text style={pdfStyles.sectionTitle}>Product Shipped</Text>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Vehicle Number</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{deliveryOrder.deliveries?.map((item, index) => {
|
||||
return (
|
||||
<View key={index} style={[pdfStyles.tableRow]}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{formatVechicleNumber(item.vehicle_number) || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
|
||||
>
|
||||
<Text>Rp{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>From Sales Order</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>
|
||||
{data?.so_number || '-'} -{' '}
|
||||
{formatDate(data.so_date, 'DD MMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
import Button from '@/components/Button';
|
||||
import { Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import pdfStyles from './styles/MarketingPDFStyles';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
interface SalesOrderExportProps {
|
||||
data?: Marketing;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const blob = await pdf(<PDFDocument data={salesData} />).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${salesData?.so_number || 'sales-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!salesData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No sales order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
isLoading={isGeneratingPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{salesData.so_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SalesOrderExport;
|
||||
const PDFDocument = ({ data }: { data: Marketing }) => {
|
||||
const grandTotal = useMemo(() => {
|
||||
return data?.sales_order?.reduce((a, b) => a + b.total_price, 0) ?? 0;
|
||||
}, [data?.sales_order]);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Sales Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>SALES ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>SO Number: {data?.so_number || '-'}</Text>
|
||||
<Text>
|
||||
Date:{' '}
|
||||
{data?.so_date
|
||||
? formatDate(data.so_date, 'DD MMM YYYY')
|
||||
: formatDate(new Date(), 'DD MMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Customer Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Customer</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Sales</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{data?.customer.email || ''} - {data?.customer.phone || ''}
|
||||
</Text>
|
||||
<Text></Text>
|
||||
<Text>{data?.customer.address || ''}</Text>
|
||||
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={{ fontWeight: 'bold', marginTop: '2px' }}>
|
||||
{data?.sales_person?.name || '-'}
|
||||
</Text>
|
||||
<Text>{data?.sales_person.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Product Sales Order Table */}
|
||||
<Text style={pdfStyles.sectionTitle}>Product Sold</Text>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>From</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{data?.sales_order?.map((item, index) => {
|
||||
const isLastItem = index === (data?.sales_order?.length || 0) - 1;
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
// isLastItem ? {} : pdfStyles.tableBorderBottom,
|
||||
]}
|
||||
>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
|
||||
>
|
||||
<Text>Rp{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Notes</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>{data?.notes || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import { StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
address: {
|
||||
fontSize: 8,
|
||||
color: '#666666',
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
innerCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default pdfStyles;
|
||||
@@ -1,308 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import Table from '@/components/Table';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const SalesOrderDetail = ({
|
||||
initialValues,
|
||||
refreshValues,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
refreshValues?: () => void;
|
||||
}) => {
|
||||
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>(
|
||||
'approve'
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalAction('approve');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalAction('reject');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully deleted Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.singleApproval(
|
||||
// initialValues?.id as number,
|
||||
// approvalAction
|
||||
// );
|
||||
setIsLoading(false);
|
||||
confirmationModal.closeModal();
|
||||
toast.success('Successfully approved Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.delivery(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deliveryModal.closeModal();
|
||||
toast.success('Successfully delivered Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col w-full gap-4'>
|
||||
<FormHeader
|
||||
title='Detail Sales Order'
|
||||
backUrl='/marketing/sales-orders'
|
||||
/>
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.approval?.step_number != 3 && (
|
||||
<>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
disabled={initialValues?.approval?.step_number != 1}
|
||||
>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.approval?.step_number != 2}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{initialValues?.approval?.step_number == 2 && (
|
||||
<Button color='success' onClick={deliveryClickHandler}>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Card
|
||||
title='Informasi Sales Order'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width='45%' className='font-semibold'>
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.customer?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Status</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.approval?.step_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.so_date}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Total Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
{formatCurrency(initialValues?.grand_total as number)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Catatan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.notes ?? '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
{initialValues?.marketing_products && (
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<Table<MarketingProduct>
|
||||
data={initialValues?.marketing_products}
|
||||
columns={[
|
||||
{
|
||||
header: 'No. Polisi',
|
||||
accessorFn(row) {
|
||||
return formatVechicleNumber(
|
||||
row.marketing_delivery_products?.vehicle_number as string
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.warehouse.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Produk',
|
||||
accessorFn(row) {
|
||||
return row.product_warehouse.product.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Harga Satuan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.unit_price);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.marketing_products &&
|
||||
initialValues?.marketing_products?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/sales-orders/detail/edit?salesOrderId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button color='error' onClick={deleteClickHandler}>
|
||||
<Icon icon='mdi:delete' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'approve' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction === 'approve' ? 'success' : 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={deliveryModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin deliver penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesOrderDetail;
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
} from './repeater/MarketingProduct.schema';
|
||||
|
||||
type MarketingSchema = {
|
||||
customer_id: number | undefined;
|
||||
customer:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
so_date: string | undefined;
|
||||
notes: string | undefined;
|
||||
marketing_products: MarketingProductFormValues[];
|
||||
};
|
||||
|
||||
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
|
||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||
customer: Yup.object({
|
||||
value: Yup.number().required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
so_date: Yup.string().required('Tanggal wajib diisi!'),
|
||||
notes: Yup.string().required('Catatan wajib diisi!'),
|
||||
marketing_products: Yup.array()
|
||||
.of(MarketingProductSchema)
|
||||
.min(1, 'Minimal harus ada 1 produk!')
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdateMarketingSchema = MarketingSchema;
|
||||
|
||||
export type MarketingFormValues = Yup.InferType<typeof MarketingSchema>;
|
||||
@@ -1,514 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import Table from '@/components/Table'; // Keep this import
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
Marketing,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import MarketingProductForm from './repeater/MarketingProductForm';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { useFormik } from 'formik';
|
||||
import { MarketingFormValues, MarketingSchema } from './SalesForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { MarketingProductFormValues } from './repeater/MarketingProduct.schema';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SalesForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
}: {
|
||||
formType?: 'add' | 'edit';
|
||||
initialValues?: Marketing;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const addProductModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||
useState<MarketingProduct | null>(null);
|
||||
const [rawMarketingProducts, setRawMarketingProducts] = useState<
|
||||
MarketingProduct[]
|
||||
>(initialValues?.marketing_products || []);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
|
||||
initialValues?.customer
|
||||
? { value: initialValues.customer.id, label: initialValues.customer.name }
|
||||
: null
|
||||
);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState<number>(
|
||||
initialValues?.grand_total ?? 0
|
||||
);
|
||||
const marketingProducts = useMemo(
|
||||
() => rawMarketingProducts,
|
||||
[rawMarketingProducts]
|
||||
);
|
||||
|
||||
const {
|
||||
options: customerOptions,
|
||||
rawData: customerRawData,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const handleAddProduct = useCallback(() => {
|
||||
addProductModal.openModal();
|
||||
}, [addProductModal]);
|
||||
const handleDeleteProduct = useCallback((id: number) => {
|
||||
setRawMarketingProducts((prev) => prev.filter((p) => p.id !== id));
|
||||
}, []);
|
||||
const handleBulkDeleteProduct = () => {
|
||||
setRawMarketingProducts((prev) =>
|
||||
prev.filter((product) => !selectedRowIds.includes(product.id))
|
||||
);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const handleAddSubmitProduct = useCallback(
|
||||
async (
|
||||
tableValue: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
) => {
|
||||
const newMarketingProduct: MarketingProduct = {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
product_warehouse: tableValue.product_warehouse!,
|
||||
unit_price: tableValue.unit_price as number,
|
||||
total_weight: tableValue.total_weight as number,
|
||||
qty: tableValue.qty as number,
|
||||
avg_weight: tableValue.avg_weight as number,
|
||||
total_price: tableValue.total_price as number,
|
||||
marketing_delivery_products: {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
vehicle_number: tableValue.vehicle_number as string,
|
||||
delivery_date: tableValue.delivery_date as string,
|
||||
unit_price: tableValue.unit_price as number,
|
||||
total_weight: tableValue.total_weight as number,
|
||||
qty: tableValue.qty as number,
|
||||
avg_weight: tableValue.avg_weight as number,
|
||||
total_price: tableValue.total_price as number,
|
||||
},
|
||||
};
|
||||
|
||||
setRawMarketingProducts((prev) => [...prev, newMarketingProduct]);
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_products: [...formik.values.marketing_products, fieldValues],
|
||||
});
|
||||
setGrandTotal((prev) => prev + (tableValue.total_price as number));
|
||||
addProductModal.closeModal();
|
||||
},
|
||||
[rawMarketingProducts.length, addProductModal]
|
||||
);
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
setSelectedCustomer(val as OptionType);
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[selectedCustomer, setSelectedCustomer]
|
||||
);
|
||||
|
||||
const createMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
toast.success('Successfully created Sales Order!');
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
const updateMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
toast.success('Successfully updated Sales Order!');
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
initialValues?.id as number
|
||||
);
|
||||
if (isResponseSuccess(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
}
|
||||
if (isResponseError(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
}
|
||||
toast.success('Successfully deleted Sales Order!');
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: MarketingProduct
|
||||
): MarketingProductFormValues => {
|
||||
return {
|
||||
vehicle_number: product.marketing_delivery_products?.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.product.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.product.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
uom: product.product_warehouse?.product?.uom?.name,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
delivery_date: product.marketing_delivery_products?.delivery_date,
|
||||
};
|
||||
};
|
||||
|
||||
const formik = useFormik<MarketingFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
so_date: initialValues?.so_date || undefined,
|
||||
notes: initialValues?.notes || undefined,
|
||||
customer_id: initialValues?.customer?.id || undefined,
|
||||
customer: {
|
||||
value: initialValues?.customer?.id as number,
|
||||
label: initialValues?.customer?.name as string,
|
||||
},
|
||||
marketing_products:
|
||||
initialValues?.marketing_products?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
},
|
||||
validationSchema: MarketingSchema,
|
||||
onSubmit: async (values) => {
|
||||
const payload = {
|
||||
customer_id: values.customer_id as number,
|
||||
date: values.so_date as string,
|
||||
notes: values.notes as string,
|
||||
marketing_products: values.marketing_products,
|
||||
} as CreateMarketingPayload;
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
createMarketingHandler(payload);
|
||||
break;
|
||||
case 'edit':
|
||||
updateMarketingHandler(payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formik.initialValues);
|
||||
}, [formikSetValues, formik.initialValues]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }: { table: TanStack.Table<MarketingProduct> }) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<MarketingProduct> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.marketing_delivery_products?.vehicle_number,
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.product_warehouse.warehouse.name,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.product_warehouse.product.name,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatCurrency(row.unit_price),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.total_weight),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.qty),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.avg_weight),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatCurrency(row.total_price),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props: TanStack.CellContext<MarketingProduct, unknown>) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() => handleDeleteProduct(props.row.original.id)}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleDeleteProduct] // dependensi tunggal
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
>
|
||||
<FormHeader
|
||||
title={`${formType === 'add' ? 'Tambah' : 'Edit'} Sales Order`}
|
||||
backUrl='/marketing/sales-orders'
|
||||
/>
|
||||
<Card
|
||||
title='Informasi Order'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-3 mt-3'>
|
||||
<SelectInput
|
||||
label='Pelanggan'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={selectedCustomer}
|
||||
onChange={handleChangeCustomer}
|
||||
isError={
|
||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||
}
|
||||
errorMessage={formik.errors.customer_id}
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
/>
|
||||
<DateInput
|
||||
name='so_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.so_date}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
|
||||
errorMessage={formik.errors.so_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<Table<MarketingProduct>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={marketingProducts}
|
||||
columns={columns}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data penjualan</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={handleAddProduct}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={handleBulkDeleteProduct}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Produk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
required
|
||||
name='notes'
|
||||
label='Catatan'
|
||||
rows={3}
|
||||
placeholder='Masukan catatan penjualan'
|
||||
value={formik.values.notes}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
/>
|
||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
||||
<span>Total Penjualan</span>
|
||||
<span className='text-lg font-semibold'>
|
||||
{formatCurrency(grandTotal)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
|
||||
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{formType == 'edit' && (
|
||||
<div className='flex flex-row justify-start'>
|
||||
<Button type='button' color='error' onClick={handleDelete}>
|
||||
<Icon icon='mdi:trash' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
ref={addProductModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addProductModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MarketingProductForm
|
||||
onSubmitForm={handleAddSubmitProduct}
|
||||
modalRef={addProductModal.ref}
|
||||
data={rawMarketingProducts}
|
||||
initialValues={selectedMarketingProduct ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
onClick: deleteMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesForm;
|
||||
@@ -1,26 +1,48 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const KandangFormSchema = Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
type KandangFormSchemaType = {
|
||||
name: string;
|
||||
locationId: number | undefined;
|
||||
location:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
capacity: number | undefined;
|
||||
picId: number | undefined;
|
||||
pic:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
};
|
||||
|
||||
locationId: Yup.number()
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||
Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
|
||||
capacity: Yup.number()
|
||||
.min(1, 'Kapasitas wajib diisi!')
|
||||
.required('Kapasitas wajib diisi!'),
|
||||
locationId: Yup.number()
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
|
||||
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
|
||||
pic: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
});
|
||||
capacity: Yup.number()
|
||||
.min(1, 'Kapasitas wajib diisi!')
|
||||
.required('Kapasitas wajib diisi!'),
|
||||
|
||||
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
|
||||
pic: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
});
|
||||
|
||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
capacity: initialValues?.capacity ?? 0,
|
||||
capacity: initialValues?.capacity,
|
||||
picId: initialValues?.pic?.id ?? 0,
|
||||
pic: initialValues?.pic
|
||||
? {
|
||||
@@ -102,9 +102,9 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
|
||||
const kandangPayload: CreateKandangPayload = {
|
||||
name: values.name,
|
||||
location_id: values.locationId,
|
||||
capacity: values.capacity,
|
||||
pic_id: values.picId,
|
||||
location_id: values.locationId!,
|
||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||
pic_id: values.picId!,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
@@ -256,7 +256,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
required
|
||||
name='capacity'
|
||||
label='Kapasitas'
|
||||
value={formik.values.capacity ?? undefined}
|
||||
placeholder='Masukan kapasitas kandang'
|
||||
value={formik.values.capacity}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
|
||||
@@ -20,7 +20,6 @@ import { Icon } from '@iconify/react';
|
||||
import { CellContext, SortingState } from '@tanstack/react-table';
|
||||
import { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import ChickinForm from './form/ChickinForm';
|
||||
|
||||
const ChickinTable = () => {
|
||||
const {
|
||||
|
||||
@@ -45,7 +45,7 @@ const ChickinFormKandang = ({
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<FormHeader
|
||||
title='Chick In DOC'
|
||||
title={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
|
||||
backUrl={`/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import PillBadge from '@/components/PillBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -31,12 +32,13 @@ const ChickinLogsView = ({
|
||||
confirmModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
const confirmationModalApproveClickHandler = async (notes?: string) => {
|
||||
setChickinErrorMessage('');
|
||||
setIsApproveLoading(true);
|
||||
const approveChickinRes = await ChickinApi.singleApproval(
|
||||
initialValues?.id as number,
|
||||
'APPROVED'
|
||||
'APPROVED',
|
||||
notes
|
||||
);
|
||||
if (isResponseSuccess(approveChickinRes)) {
|
||||
toast.success(approveChickinRes?.message as string);
|
||||
@@ -151,7 +153,7 @@ const ChickinLogsView = ({
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type='success'
|
||||
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
|
||||
@@ -161,7 +163,9 @@ const ChickinLogsView = ({
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
onClick: (notes) => {
|
||||
confirmationModalApproveClickHandler(notes);
|
||||
},
|
||||
isLoading: isApproveLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
ChickinSchema,
|
||||
} from '../ChickinForm.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -24,7 +22,6 @@ import Alert from '@/components/Alert';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
const ChickinFormView = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
afterSubmit,
|
||||
}: {
|
||||
@@ -122,7 +119,7 @@ const ChickinFormView = ({
|
||||
return (
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onReset={(e) => {
|
||||
onReset={() => {
|
||||
handleReset();
|
||||
}}
|
||||
onSubmit={formik.handleSubmit}
|
||||
|
||||
@@ -6,6 +6,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import Table from '@/components/Table';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
@@ -144,6 +145,9 @@ const ProjectFlockTable = () => {
|
||||
useState<ProjectFlock>();
|
||||
const deleteModal = useModal();
|
||||
const confirmModal = useModal();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
|
||||
@@ -226,18 +230,21 @@ const ProjectFlockTable = () => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'APPROVED' | 'REJECTED'
|
||||
) => {
|
||||
setIsApproveLoading(true);
|
||||
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
|
||||
BaseApiResponse<ProjectFlock>,
|
||||
ProjectFlockApprovalPayload
|
||||
>(`/approvals`, {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
action: 'APPROVED',
|
||||
approvable_ids: selectedRowIds.map((id) => id),
|
||||
},
|
||||
});
|
||||
const approveProjectFlockRes =
|
||||
approvalAction === 'APPROVED'
|
||||
? await ProjectFlockApi.bulkApprove(
|
||||
selectedRowIds.map((id) => id),
|
||||
notes
|
||||
)
|
||||
: await ProjectFlockApi.bulkReject(
|
||||
selectedRowIds.map((id) => id),
|
||||
notes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(approveProjectFlockRes)) {
|
||||
toast.success('Project Flock berhasil di-approve!');
|
||||
@@ -271,6 +278,7 @@ const ProjectFlockTable = () => {
|
||||
variant='outline'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
setApprovalAction('APPROVED');
|
||||
confirmModal.openModal();
|
||||
}}
|
||||
disabled={selectedRowIds.length === 0}
|
||||
@@ -279,6 +287,19 @@ const ProjectFlockTable = () => {
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setApprovalAction('REJECTED');
|
||||
confirmModal.openModal();
|
||||
}}
|
||||
disabled={selectedRowIds.length === 0}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='mdi:times' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
<div className='ms-auto w-full sm:w-auto'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
@@ -558,7 +579,7 @@ const ProjectFlockTable = () => {
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock?.name})?`}
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
@@ -570,17 +591,19 @@ const ProjectFlockTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type='success'
|
||||
text={`Apakah anda yakin ingin approve data Project Flock ini (${selectedRowIds.length} data)?`}
|
||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
||||
onClick: (notes) => {
|
||||
confirmApprovalHandler(notes, approvalAction);
|
||||
},
|
||||
isLoading: isApproveLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
+12
-11
@@ -14,13 +14,13 @@ import { cn } from '@/lib/helper';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
|
||||
const ProjectFlockChickinDetail = ({
|
||||
projectFlockId,
|
||||
@@ -42,10 +42,7 @@ const ProjectFlockChickinDetail = ({
|
||||
const [projectFlock, setProjectFlock] = useState<ProjectFlock>();
|
||||
|
||||
// Fetch Data
|
||||
const {
|
||||
data: listProjectFlockKandang,
|
||||
isLoading: isLoadingListProjectFlockKandang,
|
||||
} = useSWR(
|
||||
const { data: listProjectFlockKandang } = useSWR(
|
||||
`${ProjectFlockKandangApi.basePath}?${new URLSearchParams({
|
||||
search: searchProjectFlock,
|
||||
project_flock_id:
|
||||
@@ -104,6 +101,10 @@ const ProjectFlockChickinDetail = ({
|
||||
}, [projectFlockId, listProjectFlock]);
|
||||
return (
|
||||
<>
|
||||
<FormHeader
|
||||
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
|
||||
backUrl='/production/project-flock'
|
||||
/>
|
||||
<div className='flex flex-col gap-4 w-full my-4'>
|
||||
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
|
||||
<SelectInput
|
||||
@@ -118,7 +119,7 @@ const ProjectFlockChickinDetail = ({
|
||||
value={
|
||||
projectFlock
|
||||
? {
|
||||
label: `${projectFlock?.flock?.name}`,
|
||||
label: `${projectFlock?.flock_name}`,
|
||||
value: projectFlock?.id,
|
||||
}
|
||||
: null
|
||||
@@ -175,7 +176,7 @@ const ProjectFlockChickinDetail = ({
|
||||
},
|
||||
{
|
||||
header: 'Nama Flock',
|
||||
accessorKey: 'flock.name',
|
||||
accessorKey: 'flock_name',
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
@@ -209,10 +210,6 @@ const ProjectFlockChickinDetail = ({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Periode',
|
||||
accessorKey: 'period',
|
||||
},
|
||||
{
|
||||
header: 'FCR Layer',
|
||||
accessorKey: 'fcr.name',
|
||||
@@ -278,6 +275,10 @@ const ProjectFlockChickinDetail = ({
|
||||
accessorKey: 'kandang.capacity',
|
||||
header: 'Kapasitas',
|
||||
},
|
||||
{
|
||||
accessorFn: () => projectFlock?.period,
|
||||
header: 'Periode',
|
||||
},
|
||||
{
|
||||
accessorKey: 'approval.step_name',
|
||||
header: 'Status',
|
||||
|
||||
@@ -42,11 +42,6 @@ export const ProjectFlockFormSchema = Yup.object({
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
|
||||
period: Yup.number()
|
||||
.required('Periode wajib diisi!')
|
||||
.typeError('Periode harus berupa angka')
|
||||
.min(1, 'Minimal periode adalah 1'),
|
||||
|
||||
kandang_ids: Yup.array()
|
||||
.of(Yup.number().typeError('Kandang tidak valid!'))
|
||||
.min(1, 'Minimal harus ada 1 kandang!')
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
ProjectFlock,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import toast from 'react-hot-toast';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
@@ -42,6 +41,7 @@ import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -72,8 +72,11 @@ const ProjectFlockForm = ({
|
||||
const [optionsKandang, setOptionsKandang] = useState<Kandang[]>(
|
||||
initialValues?.kandangs ?? []
|
||||
);
|
||||
const [selectedFlock, setSelectedFlock] = useState<number | undefined>(
|
||||
initialValues?.flock?.id ?? 0
|
||||
const [selectedFlock, setSelectedFlock] = useState<string | undefined>(
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
) ?? ''
|
||||
);
|
||||
|
||||
const deleteModal = useModal();
|
||||
@@ -102,9 +105,13 @@ const ProjectFlockForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues?.approval?.step_name) {
|
||||
const approvedDisabled = initialValues.approval.step_name !== 'Pengajuan';
|
||||
const pengajuanRejected =
|
||||
initialValues.approval.step_number == 1 &&
|
||||
initialValues.approval.action == 'REJECTED';
|
||||
const approvedDisabled =
|
||||
initialValues.approval.step_number !== 1 || pengajuanRejected;
|
||||
setIsApprovedDisabled(approvedDisabled);
|
||||
setIsRejectedDisabled(!approvedDisabled);
|
||||
setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
|
||||
setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
|
||||
}
|
||||
}, [initialValues]);
|
||||
@@ -143,15 +150,14 @@ const ProjectFlockForm = ({
|
||||
mutate: refreshKandang,
|
||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||
|
||||
const { data: periodFlocks, isLoading: isLoadingPeriodFlocks } = useSWR(
|
||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||
`${selectedFlock?.toString()}/periods`,
|
||||
(id: string) => ProjectFlockApi.getNextPeriod(id)
|
||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
rawDataApprovals: rawDataApprovals,
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.approval,
|
||||
@@ -182,6 +188,7 @@ const ProjectFlockForm = ({
|
||||
formik.setFieldValue('kandang_ids', selectedRowIds);
|
||||
}
|
||||
}
|
||||
refreshPeriodFlocks();
|
||||
}
|
||||
}, [kandang, selectedLocation]);
|
||||
useEffect(() => {
|
||||
@@ -278,13 +285,24 @@ const ProjectFlockForm = ({
|
||||
|
||||
// Formik InitialValue
|
||||
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
|
||||
const trimFlock =
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
) ?? '';
|
||||
return {
|
||||
name: initialValues?.flock_name,
|
||||
flock: initialValues?.flock
|
||||
flock: initialValues?.flock_name
|
||||
? {
|
||||
value: initialValues?.flock?.id ?? 0,
|
||||
value:
|
||||
optionsFlock.find((flock) => {
|
||||
return flock.label == trimFlock;
|
||||
})?.value ?? 0,
|
||||
label:
|
||||
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
|
||||
formType != 'detail'
|
||||
? (optionsFlock.find((flock) => {
|
||||
return flock.label == trimFlock;
|
||||
})?.label ?? '')
|
||||
: initialValues?.flock_name,
|
||||
}
|
||||
: null,
|
||||
area: initialValues?.area
|
||||
@@ -311,31 +329,56 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
flock_id: initialValues?.flock?.id ?? 0,
|
||||
flock_name: initialValues?.flock_name ?? '',
|
||||
flock_name:
|
||||
optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label ?? '',
|
||||
area_id: initialValues?.area?.id ?? 0,
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
period: initialValues?.period ?? 1,
|
||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
||||
| number
|
||||
| undefined
|
||||
)[],
|
||||
};
|
||||
}, [initialValues]);
|
||||
}, [initialValues, optionsFlock]);
|
||||
|
||||
// Formik
|
||||
const formik = useFormik<ProjectFlockFormValues>({
|
||||
initialValues: {
|
||||
name: initialValues?.flock_name,
|
||||
flock: initialValues?.flock
|
||||
flock: initialValues?.flock_name
|
||||
? {
|
||||
value: initialValues?.flock?.id ?? 0,
|
||||
value:
|
||||
optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.value ?? 0,
|
||||
label:
|
||||
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
|
||||
formType != 'detail'
|
||||
? (optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label ?? '')
|
||||
: initialValues?.flock_name,
|
||||
}
|
||||
: null,
|
||||
area: initialValues?.area
|
||||
@@ -362,15 +405,24 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
flock_id: initialValues?.flock?.id ?? 0,
|
||||
flock_name: initialValues?.flock_name ?? '',
|
||||
flock_name:
|
||||
formType != 'detail'
|
||||
? optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label
|
||||
: (initialValues?.flock_name ?? ''),
|
||||
area_id: initialValues?.area?.id ?? 0,
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
period: initialValues?.period ?? 1,
|
||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
||||
| number
|
||||
| undefined
|
||||
@@ -385,12 +437,11 @@ const ProjectFlockForm = ({
|
||||
onSubmit: async (values) => {
|
||||
setProjectFlockFormErrorMessage('');
|
||||
const payload: CreateProjectFlockPayload = {
|
||||
flock_name: values.flock?.label as string,
|
||||
flock_name: values.flock_name as string,
|
||||
area_id: values.area_id as number,
|
||||
category: values.category as string,
|
||||
fcr_id: values.fcr_id as number,
|
||||
location_id: values.location_id as number,
|
||||
period: values.period as number,
|
||||
kandang_ids: values.kandang_ids as number[],
|
||||
};
|
||||
|
||||
@@ -419,8 +470,6 @@ const ProjectFlockForm = ({
|
||||
if (initialValues?.area_id) {
|
||||
setSelectedArea(initialValues?.area_id.toString() as string);
|
||||
}
|
||||
|
||||
formik.setFieldValue('period', initialValues?.period);
|
||||
}
|
||||
}, [initialValues, setSelectedArea, formType]);
|
||||
|
||||
@@ -449,15 +498,6 @@ const ProjectFlockForm = ({
|
||||
formik.validateForm();
|
||||
}, [formik.values]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResponseSuccess(periodFlocks)) {
|
||||
formik.setFieldValue('period', periodFlocks.data.next_period);
|
||||
}
|
||||
if (isResponseError(periodFlocks)) {
|
||||
console.log(periodFlocks?.message as string);
|
||||
}
|
||||
}, [periodFlocks]);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedRowIds = Object.keys(rowSelection)
|
||||
.filter((id) => rowSelection[id])
|
||||
@@ -485,32 +525,23 @@ const ProjectFlockForm = ({
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const confirmationModalClickHandler = async ({
|
||||
action = 'APPROVED',
|
||||
}: {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
}) => {
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'REJECTED' | 'APPROVED'
|
||||
) => {
|
||||
if (initialValues?.id === undefined) return;
|
||||
setIsApproveLoading(true);
|
||||
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
|
||||
BaseApiResponse<ProjectFlock>,
|
||||
ProjectFlockApprovalPayload
|
||||
>(`/approvals`, {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
action: action,
|
||||
approvable_ids: [initialValues?.id],
|
||||
},
|
||||
});
|
||||
|
||||
if (isResponseSuccess(approveProjectFlockRes)) {
|
||||
if (refreshProjectFlocks) {
|
||||
await refreshProjectFlocks();
|
||||
}
|
||||
toast.success(approveProjectFlockRes.message as string);
|
||||
const approvalRes =
|
||||
approvalAction == 'APPROVED'
|
||||
? await ProjectFlockApi.approve(initialValues?.id, notes)
|
||||
: await ProjectFlockApi.reject(initialValues?.id, notes);
|
||||
if (isResponseSuccess(approvalRes)) {
|
||||
refreshProjectFlocks?.();
|
||||
toast.success(approvalRes.message as string);
|
||||
}
|
||||
if (isResponseError(approveProjectFlockRes)) {
|
||||
toast.error(approveProjectFlockRes?.message as string);
|
||||
if (isResponseError(approvalRes)) {
|
||||
toast.error(approvalRes?.message as string);
|
||||
}
|
||||
refreshApprovals();
|
||||
confirmModal.closeModal();
|
||||
@@ -555,7 +586,7 @@ const ProjectFlockForm = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{approvals && !approvalsLoading && (
|
||||
{approvals && !approvalsLoading && formType == 'detail' && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
{formType == 'detail' && (
|
||||
@@ -615,7 +646,6 @@ const ProjectFlockForm = ({
|
||||
<div className='card bg-base-100 shadow w-full mb-6'>
|
||||
<div className='card-body'>
|
||||
<div className='card-title mb-4'>Informasi Umum</div>
|
||||
|
||||
<div className='grid sm:grid-cols-2 gap-4'>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -634,10 +664,19 @@ const ProjectFlockForm = ({
|
||||
<SelectInput
|
||||
required
|
||||
label='Flock'
|
||||
value={formik.values.flock as OptionType}
|
||||
value={
|
||||
formik.values.flock_name
|
||||
? ({
|
||||
label: formik.values.flock_name,
|
||||
value: optionsFlock.find((flock) => {
|
||||
return flock.label === formik.values.flock_name;
|
||||
})?.value,
|
||||
} as OptionType)
|
||||
: undefined
|
||||
}
|
||||
onChange={(val) => {
|
||||
optionChangeHandler(val, 'flock');
|
||||
setSelectedFlock((val as OptionType)?.value as number);
|
||||
setSelectedFlock((val as OptionType)?.label);
|
||||
formik.setFieldValue(
|
||||
'flock_name',
|
||||
(val as OptionType)?.label
|
||||
@@ -701,22 +740,6 @@ const ProjectFlockForm = ({
|
||||
isClearable
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name='period'
|
||||
label='Periode'
|
||||
placeholder='Masukkan periode yang project'
|
||||
value={formik.values.period ?? (1 as number)}
|
||||
onChange={formik.handleChange}
|
||||
isError={
|
||||
formik.touched.period && Boolean(formik.errors.period)
|
||||
}
|
||||
errorMessage={formik.errors.period as string}
|
||||
readOnly={formType === 'detail'}
|
||||
disabled={true}
|
||||
isLoading={isLoadingPeriodFlocks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -750,6 +773,9 @@ const ProjectFlockForm = ({
|
||||
<span className='loading loading-dots loading-xl'></span>
|
||||
)}
|
||||
<ProjectFlockKandangTable
|
||||
listPeriods={
|
||||
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
|
||||
}
|
||||
listKandang={optionsKandang}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
@@ -832,7 +858,7 @@ const ProjectFlockForm = ({
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock?.name} - ${initialValues?.area?.name})?`}
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock_name} - ${initialValues?.area?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
@@ -844,12 +870,12 @@ const ProjectFlockForm = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${
|
||||
approvalAction == 'APPROVED' ? 'approve' : 'reject'
|
||||
} Project Flock berikut? (${initialValues?.flock?.name} - ${
|
||||
} Project Flock berikut? (${initialValues?.flock_name} - ${
|
||||
initialValues?.area?.name
|
||||
})?`}
|
||||
secondaryButton={{
|
||||
@@ -859,10 +885,8 @@ const ProjectFlockForm = ({
|
||||
text: 'Ya',
|
||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isApproveLoading,
|
||||
onClick: () => {
|
||||
confirmationModalClickHandler({
|
||||
action: approvalAction,
|
||||
});
|
||||
onClick: (notes) => {
|
||||
confirmApprovalHandler(notes, approvalAction);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,10 +5,12 @@ import PillBadge from '@/components/PillBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlockPeriods } from '@/types/api/production/project-flock';
|
||||
import { OnChangeFn, Row } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const ProjectFlockKandangTable = ({
|
||||
listPeriods,
|
||||
listKandang,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
@@ -16,6 +18,7 @@ const ProjectFlockKandangTable = ({
|
||||
initialValues,
|
||||
formType = 'add',
|
||||
}: {
|
||||
listPeriods: ProjectFlockPeriods;
|
||||
listKandang: Kandang[];
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: OnChangeFn<Record<string, boolean>>;
|
||||
@@ -134,6 +137,19 @@ const ProjectFlockKandangTable = ({
|
||||
accessorFn: (row) => row.capacity,
|
||||
header: 'Kapasitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.location?.name,
|
||||
header: 'Periode',
|
||||
cell: (props) => {
|
||||
console.log('listPeriods');
|
||||
console.log(listPeriods);
|
||||
const period =
|
||||
listPeriods.length > 0
|
||||
? listPeriods.find((p) => p.id == props.row.original.id)
|
||||
: undefined;
|
||||
return period?.period ?? '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.pic?.name,
|
||||
header: 'Penanggung Jawab',
|
||||
|
||||
@@ -33,6 +33,21 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const MARKETING_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
},
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Sales Order',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Delivery Order',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const RECORDING_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
|
||||
|
||||
{
|
||||
title: 'Penjualan',
|
||||
link: '/marketing/sales-orders',
|
||||
link: '/marketing',
|
||||
icon: 'mdi:attach-money',
|
||||
},
|
||||
|
||||
|
||||
+189
-165
@@ -4,12 +4,34 @@ import { Location } from '@/types/api/master-data/location';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Marketing } from '@/types/api/marketing/marketing';
|
||||
import { CreatedUser } from '@/types/api/api-general';
|
||||
import {
|
||||
BaseMarketing,
|
||||
Marketing,
|
||||
BaseSalesOrder,
|
||||
BaseDeliveryOrder,
|
||||
BaseDelivery,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import {
|
||||
CreatedUser,
|
||||
BaseApproval,
|
||||
BaseMetadata,
|
||||
} from '@/types/api/api-general';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { Uom } from '@/types/api/master-data/uom';
|
||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
|
||||
// Waktu saat ini untuk created_at/updated_at
|
||||
const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
const tomorrow = format(
|
||||
new Date().setDate(new Date().getDate() + 1),
|
||||
'yyyy-MM-dd'
|
||||
);
|
||||
|
||||
// ======================
|
||||
// 👤 Created User
|
||||
// 👤 Created User & Helper Data
|
||||
// ======================
|
||||
export const createdUser: CreatedUser = {
|
||||
id: 1,
|
||||
@@ -18,6 +40,24 @@ export const createdUser: CreatedUser = {
|
||||
name: 'Admin Utama',
|
||||
};
|
||||
|
||||
const dummyProductBase: Product = {
|
||||
id: 101,
|
||||
name: 'Pakan Ayam Premium',
|
||||
brand: 'Brand Hebat',
|
||||
sku: 'PAK-001',
|
||||
product_price: 15000,
|
||||
selling_price: 18000,
|
||||
tax: 0.1,
|
||||
expiry_period: 365,
|
||||
uom: { id: 1, name: 'Sak' } as Uom,
|
||||
product_category: { id: 1, name: 'Pakan' } as ProductCategory,
|
||||
suppliers: [{ id: 1, name: 'Supplier A' } as Supplier],
|
||||
flags: ['PAKAN'],
|
||||
created_user: createdUser,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// ======================
|
||||
// 📍 Area Dummy
|
||||
// ======================
|
||||
@@ -26,15 +66,15 @@ export const dummyAreas: Area[] = [
|
||||
id: 1,
|
||||
name: 'Bandung Barat',
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Cimahi Utara',
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -48,8 +88,8 @@ export const dummyLocations: Location[] = [
|
||||
address: 'Jl. Sukajadi No. 12',
|
||||
area: dummyAreas[0],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -57,8 +97,8 @@ export const dummyLocations: Location[] = [
|
||||
address: 'Jl. Setiabudi No. 45',
|
||||
area: dummyAreas[1],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -74,8 +114,8 @@ export const dummyKandangs: Kandang[] = [
|
||||
location: dummyLocations[0],
|
||||
pic: createdUser,
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -85,8 +125,8 @@ export const dummyKandangs: Kandang[] = [
|
||||
location: dummyLocations[1],
|
||||
pic: createdUser,
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -100,9 +140,9 @@ export const dummyWarehouses: Warehouse[] = [
|
||||
name: 'Gudang Wilayah Bandung Barat',
|
||||
area: dummyAreas[0],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Warehouse,
|
||||
{
|
||||
id: 2,
|
||||
type: 'LOKASI',
|
||||
@@ -110,9 +150,9 @@ export const dummyWarehouses: Warehouse[] = [
|
||||
area: dummyAreas[0],
|
||||
location: { ...dummyLocations[0], area: dummyAreas[0] },
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Warehouse,
|
||||
{
|
||||
id: 3,
|
||||
type: 'KANDANG',
|
||||
@@ -125,9 +165,9 @@ export const dummyWarehouses: Warehouse[] = [
|
||||
pic: createdUser,
|
||||
},
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Warehouse,
|
||||
];
|
||||
|
||||
// ======================
|
||||
@@ -139,16 +179,11 @@ export const dummyProductWarehouses: ProductWarehouse[] = [
|
||||
product_id: 101,
|
||||
warehouse_id: 1,
|
||||
quantity: 1000,
|
||||
product: {
|
||||
id: 101,
|
||||
name: 'Pakan Ayam Premium',
|
||||
sku: 'PAK-001',
|
||||
category: 'PAKAN',
|
||||
} as unknown as Product,
|
||||
product: dummyProductBase,
|
||||
warehouse: dummyWarehouses[0],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -156,29 +191,95 @@ export const dummyProductWarehouses: ProductWarehouse[] = [
|
||||
warehouse_id: 2,
|
||||
quantity: 500,
|
||||
product: {
|
||||
...dummyProductBase,
|
||||
id: 102,
|
||||
name: 'Vitamin Ayam Super',
|
||||
sku: 'VIT-002',
|
||||
category: 'VITAMIN',
|
||||
} as unknown as Product,
|
||||
flags: ['VITAMIN'],
|
||||
selling_price: 25000,
|
||||
},
|
||||
warehouse: dummyWarehouses[1],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
];
|
||||
|
||||
// ======================
|
||||
// 💼 Marketing Dummy
|
||||
// ======================
|
||||
export const dummyMarketings: Marketing[] = [
|
||||
// Step 1: Pengajuan Order
|
||||
|
||||
// Helper untuk Sales Order (SO) Item
|
||||
const soItem1: BaseSalesOrder = {
|
||||
vehicle_number: 'B 1234 ABC',
|
||||
id: 101,
|
||||
marketing_id: 1,
|
||||
product_warehouse_id: 1,
|
||||
qty: 100,
|
||||
unit_price: 18000, // Harga jual
|
||||
avg_weight: 1.0,
|
||||
total_weight: 100 * 1.0,
|
||||
total_price: 100 * 18000,
|
||||
product_warehouse: dummyProductWarehouses[0] as ProductWarehouse,
|
||||
};
|
||||
const soItem2: BaseSalesOrder = {
|
||||
vehicle_number: 'D 5678 EFG',
|
||||
id: 102,
|
||||
marketing_id: 2,
|
||||
product_warehouse_id: 2,
|
||||
qty: 50,
|
||||
unit_price: 25000,
|
||||
avg_weight: 0.5,
|
||||
total_weight: 50 * 0.5,
|
||||
total_price: 50 * 25000,
|
||||
product_warehouse: dummyProductWarehouses[1] as ProductWarehouse,
|
||||
};
|
||||
|
||||
// Helper untuk Delivery Item (DO) Detail
|
||||
const doDelivery1: BaseDelivery[] = [
|
||||
{
|
||||
product_warehouse: dummyProductWarehouses[0] as ProductWarehouse,
|
||||
qty: soItem1.qty,
|
||||
unit_price: soItem1.unit_price,
|
||||
total_weight: soItem1.total_weight,
|
||||
avg_weight: soItem1.avg_weight,
|
||||
total_price: soItem1.total_price,
|
||||
vehicle_number: 'B 1234 ABC',
|
||||
},
|
||||
];
|
||||
|
||||
const doDelivery2: BaseDelivery[] = [
|
||||
{
|
||||
product_warehouse: dummyProductWarehouses[1] as ProductWarehouse,
|
||||
qty: soItem2.qty,
|
||||
unit_price: soItem2.unit_price,
|
||||
total_weight: soItem2.total_weight,
|
||||
avg_weight: soItem2.avg_weight,
|
||||
total_price: soItem2.total_price,
|
||||
vehicle_number: 'D 5678 EFG',
|
||||
},
|
||||
];
|
||||
|
||||
// Helper untuk Delivery Order (DO) Header
|
||||
const deliveryOrder1: BaseDeliveryOrder[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'APPROVED',
|
||||
marketing_id: 3,
|
||||
do_number: 'DO-003-2025',
|
||||
delivery_date: tomorrow,
|
||||
warehouse: dummyWarehouses[0],
|
||||
deliveries: doDelivery1,
|
||||
},
|
||||
];
|
||||
|
||||
export const dummyMarketings: Marketing[] = [
|
||||
// 1. Pengajuan Order (Langkah Pertama/Awal)
|
||||
{
|
||||
id: 1,
|
||||
status: 'DRAFT',
|
||||
// name: 'SO-001-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-001-2025',
|
||||
so_docs: 'https://example.com/docs/so001.pdf',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 1,
|
||||
name: 'PT Maju Jaya',
|
||||
@@ -190,52 +291,32 @@ export const dummyMarketings: Marketing[] = [
|
||||
email: 'contact@majujaya.com',
|
||||
account_number: '1234567890',
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Customer,
|
||||
sales_person: createdUser,
|
||||
notes: 'Pengiriman awal bulan.',
|
||||
grand_total: 7500000,
|
||||
approval: {
|
||||
notes: 'Pengajuan Order Awal, menunggu persetujuan harga.',
|
||||
latest_approval: {
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan Order',
|
||||
action: 'APPROVED',
|
||||
action: 'CREATED',
|
||||
action_by: createdUser,
|
||||
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
marketing_products: [
|
||||
{
|
||||
id: 1,
|
||||
qty: 100,
|
||||
unit_price: 75000,
|
||||
avg_weight: 2.5,
|
||||
total_weight: 250,
|
||||
total_price: 7500000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
id: 1,
|
||||
qty: 100,
|
||||
unit_price: 75000,
|
||||
avg_weight: 2.5,
|
||||
total_weight: 250,
|
||||
total_price: 7500000,
|
||||
delivery_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
vehicle_number: 'B 1234 XY',
|
||||
},
|
||||
},
|
||||
],
|
||||
action_at: now,
|
||||
} as BaseApproval,
|
||||
sales_order: [soItem1],
|
||||
delivery_order: [],
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Marketing,
|
||||
|
||||
// Step 2: Sales Order
|
||||
// 2. Sales Order (Disetujui dan Siap DO)
|
||||
{
|
||||
id: 2,
|
||||
status: 'APPROVED',
|
||||
// name: 'SO-002-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-002-2025',
|
||||
so_docs: 'https://example.com/docs/so002.pdf',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 2,
|
||||
name: 'CV Sumber Sehat',
|
||||
@@ -247,52 +328,33 @@ export const dummyMarketings: Marketing[] = [
|
||||
email: 'info@sumbersehat.com',
|
||||
account_number: '9876543210',
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Customer,
|
||||
sales_person: createdUser,
|
||||
notes: 'Pesanan kedua untuk stok akhir tahun.',
|
||||
grand_total: 3750000,
|
||||
approval: {
|
||||
notes: 'Sales Order telah disetujui oleh Supervisor.',
|
||||
latest_approval: {
|
||||
id: 2,
|
||||
step_number: 2,
|
||||
step_name: 'Sales Order',
|
||||
action: 'APPROVED',
|
||||
action_by: createdUser,
|
||||
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
marketing_products: [
|
||||
{
|
||||
id: 2,
|
||||
qty: 50,
|
||||
unit_price: 75000,
|
||||
avg_weight: 2.5,
|
||||
total_weight: 125,
|
||||
total_price: 3750000,
|
||||
product_warehouse: dummyProductWarehouses[1],
|
||||
marketing_delivery_products: {
|
||||
id: 2,
|
||||
qty: 50,
|
||||
unit_price: 75000,
|
||||
avg_weight: 2.5,
|
||||
total_weight: 125,
|
||||
total_price: 3750000,
|
||||
delivery_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
vehicle_number: 'B 5678 YZ',
|
||||
},
|
||||
},
|
||||
],
|
||||
action_at: now,
|
||||
} as BaseApproval,
|
||||
sales_order: [soItem2],
|
||||
delivery_order: [], // Belum ada pengiriman (DO) yang dibuat
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Marketing,
|
||||
|
||||
// Step 3: Delivery Order
|
||||
// 3. Delivery Order (Proses Pengiriman telah dibuat)
|
||||
{
|
||||
id: 3,
|
||||
status: 'APPROVED',
|
||||
status: 'DELIVERED', // Asumsi status DELIVERED berarti DO sudah selesai/terbuat
|
||||
// name: 'SO-003-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-003-2025',
|
||||
so_docs: 'https://example.com/docs/so003.pdf',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 3,
|
||||
name: 'UD Ternak Sejahtera',
|
||||
@@ -304,61 +366,23 @@ export const dummyMarketings: Marketing[] = [
|
||||
email: 'halo@ternaksejahtera.com',
|
||||
account_number: '1122334455',
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Customer,
|
||||
sales_person: createdUser,
|
||||
notes: 'Order untuk pengiriman ke luar kota.',
|
||||
grand_total: 5600000,
|
||||
approval: {
|
||||
notes: 'Pengiriman barang telah berhasil dilakukan.',
|
||||
latest_approval: {
|
||||
id: 3,
|
||||
step_number: 3,
|
||||
step_name: 'Delivery Order',
|
||||
action: 'APPROVED',
|
||||
action: 'COMPLETED',
|
||||
action_by: createdUser,
|
||||
action_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
marketing_products: [
|
||||
{
|
||||
id: 3,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
avg_weight: 2.4,
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
id: 3,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
avg_weight: 2.4,
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
delivery_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
vehicle_number: 'D 9090 ZZ',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
avg_weight: 2.4,
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
id: 3,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
avg_weight: 2.4,
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
delivery_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
vehicle_number: 'D 9090 ZZ',
|
||||
},
|
||||
},
|
||||
],
|
||||
action_at: now,
|
||||
} as BaseApproval,
|
||||
sales_order: [soItem1, soItem2],
|
||||
delivery_order: deliveryOrder1, // DO sudah terbuat
|
||||
created_user: createdUser,
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
} as Marketing,
|
||||
];
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ import moment from 'moment';
|
||||
import 'moment/locale/id';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import clsx, { ClassValue } from 'clsx';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
// set locale globally
|
||||
moment.locale('id');
|
||||
@@ -32,7 +31,7 @@ export const formatNumber = (
|
||||
|
||||
export function formatVechicleNumber(value: string): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
for (let i = 0; i < (value?.length ?? 0); i++) {
|
||||
const curr = value[i];
|
||||
const prev = value[i - 1];
|
||||
|
||||
|
||||
@@ -5,82 +5,74 @@ import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
Marketing,
|
||||
CreateMarketingPayload,
|
||||
UpdateMarketingPayload,
|
||||
CreateSalesOrderPayload,
|
||||
UpdateSalesOrderPayload,
|
||||
CreateDeliveryOrderPayload,
|
||||
UpdateDeliveryOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
|
||||
export class MarketingService extends BaseApiService<
|
||||
/**
|
||||
* 💡 Helper untuk membuat respons dummy
|
||||
* @param data Data yang akan dimasukkan ke dalam body respons
|
||||
*/
|
||||
const createDummyResponse = <T>(data: T): BaseApiResponse<T> => ({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Data retrieved successfully (MOCK)',
|
||||
data: data,
|
||||
});
|
||||
|
||||
export class SalesOrderService extends BaseApiService<
|
||||
Marketing,
|
||||
CreateMarketingPayload,
|
||||
UpdateMarketingPayload
|
||||
CreateSalesOrderPayload,
|
||||
UpdateSalesOrderPayload
|
||||
> {
|
||||
constructor(basePath: string = '/marketing') {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override: Get all marketing data (dummy mode)
|
||||
*/
|
||||
override async getAllFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<Marketing[]>> {
|
||||
// simulasi loading
|
||||
await sleep(750);
|
||||
// /**
|
||||
// * Override: Mengambil semua data Marketing dari dummyMarketings
|
||||
// */
|
||||
// async getAllFetcher(endpoint: string): Promise<BaseApiResponse<Marketing[]>> {
|
||||
// // Simulasi delay jaringan
|
||||
// await sleep(500);
|
||||
|
||||
// data dummy sementara
|
||||
const DUMMY_MARKETING_DATA: BaseApiResponse<Marketing[]> = {
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Berhasil mengambil data marketing (dummy)',
|
||||
data: dummyMarketings,
|
||||
};
|
||||
// // Filter data marketing yang valid (jika menggunakan BaseMarketing[])
|
||||
// const data = dummyMarketings as Marketing[];
|
||||
|
||||
return DUMMY_MARKETING_DATA;
|
||||
}
|
||||
// return createDummyResponse<Marketing[]>(data);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Override: Get single marketing data (dummy mode)
|
||||
*/
|
||||
override async getSingle(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<Marketing> | undefined> {
|
||||
// simulasi delay
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
// /**
|
||||
// * Override: Mengambil satu data Marketing berdasarkan ID dari dummyMarketings
|
||||
// */
|
||||
// async getSingle(id: number): Promise<BaseApiResponse<Marketing> | undefined> {
|
||||
// // Simulasi delay jaringan
|
||||
// await sleep(300);
|
||||
|
||||
const marketing = dummyMarketings.find((marketing) => {
|
||||
console.log('marketing', marketing);
|
||||
console.log('id-m', marketing.id);
|
||||
console.log('id-p', id);
|
||||
console.log('id', marketing.id == id);
|
||||
return marketing.id == id;
|
||||
});
|
||||
console.log('marketings', dummyMarketings);
|
||||
console.log('marketing', marketing);
|
||||
// const foundData = dummyMarketings.find((m) => m.id == id);
|
||||
|
||||
if (marketing) {
|
||||
// misalnya fetch dari dummy
|
||||
return {
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Data marketing berhasil diambil.',
|
||||
data: marketing,
|
||||
};
|
||||
} else {
|
||||
// jika tidak ditemukan
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: 'Data marketing tidak ditemukan.',
|
||||
};
|
||||
}
|
||||
}
|
||||
// if (foundData) {
|
||||
// // Data ditemukan, kembalikan respons sukses
|
||||
// return createDummyResponse<Marketing>(foundData as Marketing);
|
||||
// } else {
|
||||
// // Data tidak ditemukan, simulasi respons error
|
||||
// return {
|
||||
// code: 404,
|
||||
// status: 'error',
|
||||
// message: 'Marketing data not found (MOCK)',
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Approve single marketing data
|
||||
*/
|
||||
async singleApproval(
|
||||
id: number,
|
||||
action: 'approve' | 'reject'
|
||||
action: 'APPROVED' | 'REJECTED',
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -88,8 +80,8 @@ export class MarketingService extends BaseApiService<
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: action,
|
||||
approval_ids: [id],
|
||||
notes: `${action} marketing ${id}`,
|
||||
approvable_ids: [id],
|
||||
notes: notes || `${action} marketing ${id}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -103,7 +95,8 @@ export class MarketingService extends BaseApiService<
|
||||
*/
|
||||
async bulkApprovals(
|
||||
ids: number[],
|
||||
action: 'approve' | 'reject'
|
||||
action: 'APPROVED' | 'REJECTED',
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -111,8 +104,8 @@ export class MarketingService extends BaseApiService<
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: action,
|
||||
approval_ids: ids,
|
||||
notes: `${action} marketing ${ids.join(', ')}`,
|
||||
approvable_ids: ids,
|
||||
notes: notes || `${action} marketing ${ids.join(', ')}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -120,6 +113,37 @@ export class MarketingService extends BaseApiService<
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery
|
||||
*/
|
||||
async delivery(
|
||||
id: number,
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
return await httpClient<BaseApiResponse<{ message: string }>>(path, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: 'APPROVED',
|
||||
approvable_ids: [id],
|
||||
notes: notes || `Delivery marketing ${id}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error delivery marketing:', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MarketingApi = new MarketingService('/marketing');
|
||||
export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders');
|
||||
export const DeliveryOrderApi = new BaseApiService<
|
||||
Marketing,
|
||||
CreateDeliveryOrderPayload,
|
||||
UpdateDeliveryOrderPayload
|
||||
>('/marketing/delivery-orders');
|
||||
export const MarketingApi = new BaseApiService<Marketing, unknown, unknown>(
|
||||
'/marketing'
|
||||
);
|
||||
|
||||
@@ -21,7 +21,8 @@ export class ChickinService extends BaseApiService<
|
||||
*/
|
||||
async singleApproval(
|
||||
id: number,
|
||||
action: 'APPROVED' | 'REJECTED'
|
||||
action: 'APPROVED' | 'REJECTED',
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -30,7 +31,7 @@ export class ChickinService extends BaseApiService<
|
||||
body: {
|
||||
action: action,
|
||||
approvable_ids: [id],
|
||||
notes: `${action} chickin ${id}`,
|
||||
notes: notes ?? `${action} chickin ${id}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,13 +8,10 @@ import {
|
||||
BaseApiResponse,
|
||||
BaseGroupedApproval,
|
||||
ErrorApiResponse,
|
||||
GroupedApprovals,
|
||||
SuccessApiResponse,
|
||||
} from '@/types/api/api-general';
|
||||
import { sleep } from '@/lib/helper';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import axios from 'axios';
|
||||
import { Flock } from '@/types/api/master-data/flock';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { RequestOptions } from '@/services/http/base';
|
||||
|
||||
@@ -104,25 +101,34 @@ export class ProjectFlockService extends BaseApiService<
|
||||
/**
|
||||
* Get Next Period of Project Flock
|
||||
*/
|
||||
async getNextPeriod(id: string): Promise<
|
||||
| BaseApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
async getNextPeriod(locationId: number): Promise<
|
||||
| BaseApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
| ErrorApiResponse
|
||||
| SuccessApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
| SuccessApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const path = `${this.basePath}/kandangs/${id}`;
|
||||
const path = `${this.basePath}/location/${locationId.toString()}/periods`;
|
||||
return await httpClient<
|
||||
SuccessApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
SuccessApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
>(path, {
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -139,36 +145,40 @@ export class ProjectFlockService extends BaseApiService<
|
||||
* Approve single Project Flock
|
||||
*/
|
||||
async approve(
|
||||
id: number
|
||||
id: number,
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
return await this.bulkApprovalAction([id], 'APPROVED');
|
||||
return await this.bulkApprovalAction([id], 'APPROVED', notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject single Project Flock
|
||||
*/
|
||||
async reject(
|
||||
id: number
|
||||
id: number,
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
return await this.bulkApprovalAction([id], 'REJECTED');
|
||||
return await this.bulkApprovalAction([id], 'REJECTED', notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve Bulk Project Flock
|
||||
*/
|
||||
async bulkApprove(
|
||||
ids: number[]
|
||||
ids: number[],
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
return await this.bulkApprovalAction(ids, 'APPROVED');
|
||||
return await this.bulkApprovalAction(ids, 'APPROVED', notes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject Bulk Project Flock
|
||||
*/
|
||||
async bulkReject(
|
||||
ids: number[]
|
||||
ids: number[],
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
return await this.bulkApprovalAction(ids, 'REJECTED');
|
||||
return await this.bulkApprovalAction(ids, 'REJECTED', notes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +186,8 @@ export class ProjectFlockService extends BaseApiService<
|
||||
*/
|
||||
async bulkApprovalAction(
|
||||
ids: number[],
|
||||
action: 'APPROVED' | 'REJECTED'
|
||||
action: 'APPROVED' | 'REJECTED',
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -185,7 +196,7 @@ export class ProjectFlockService extends BaseApiService<
|
||||
body: {
|
||||
action: action,
|
||||
approvable_ids: ids,
|
||||
notes: `Bulk ${action} Project Flock ${ids.join(', ')}`,
|
||||
notes: notes ?? `Bulk ${action} Project Flock ${ids.join(', ')}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
+86
-18
@@ -6,19 +6,55 @@ import {
|
||||
} from '@/types/api/api-general';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { id } from 'react-day-picker/locale';
|
||||
import { Warehouse } from '../master-data/warehouse';
|
||||
|
||||
/**
|
||||
* Base Data Response
|
||||
*/
|
||||
export type BaseMarketing = {
|
||||
id: number;
|
||||
status?: string;
|
||||
so_number: string;
|
||||
customer: Customer;
|
||||
so_docs: string;
|
||||
so_date: string;
|
||||
customer: Customer;
|
||||
sales_person: CreatedUser;
|
||||
notes: string;
|
||||
grand_total: number;
|
||||
approval: BaseApproval;
|
||||
marketing_products?: MarketingProduct[];
|
||||
latest_approval: BaseApproval;
|
||||
sales_order: BaseSalesOrder[];
|
||||
delivery_order: BaseDeliveryOrder[];
|
||||
};
|
||||
|
||||
export type BaseSalesOrder = {
|
||||
id: number;
|
||||
marketing_id: number;
|
||||
product_warehouse_id: number;
|
||||
qty: number;
|
||||
unit_price: number;
|
||||
avg_weight: number;
|
||||
total_weight: number;
|
||||
total_price: number;
|
||||
product_warehouse: ProductWarehouse;
|
||||
vehicle_number: string;
|
||||
};
|
||||
|
||||
export type BaseDeliveryOrder = {
|
||||
id: number;
|
||||
marketing_id: number;
|
||||
do_number: string;
|
||||
delivery_date: string;
|
||||
warehouse: Warehouse;
|
||||
deliveries: BaseDelivery[];
|
||||
};
|
||||
|
||||
export type BaseDelivery = {
|
||||
product_warehouse: ProductWarehouse;
|
||||
qty: number;
|
||||
unit_price: number;
|
||||
total_weight: number;
|
||||
avg_weight: number;
|
||||
total_price: number;
|
||||
vehicle_number: string;
|
||||
};
|
||||
|
||||
export type MarketingProduct = {
|
||||
@@ -29,7 +65,7 @@ export type MarketingProduct = {
|
||||
total_weight: number;
|
||||
total_price: number;
|
||||
product_warehouse: ProductWarehouse;
|
||||
marketing_delivery_products?: MarketingDeliveryProducts;
|
||||
delivery_product?: MarketingDeliveryProducts;
|
||||
};
|
||||
|
||||
export type MarketingDeliveryProducts = {
|
||||
@@ -39,34 +75,66 @@ export type MarketingDeliveryProducts = {
|
||||
avg_weight: number;
|
||||
total_weight: number;
|
||||
total_price: number;
|
||||
delivery_date: string;
|
||||
delivery_date: string | null;
|
||||
vehicle_number: string;
|
||||
do_number?: string | undefined;
|
||||
do_number?: string | undefined; // Uncertain
|
||||
};
|
||||
|
||||
export type Marketing = BaseMetadata & BaseMarketing;
|
||||
|
||||
export type CreateMarketingPayload = {
|
||||
/**
|
||||
* Base Data Payload
|
||||
*/
|
||||
export type BaseCreateMarketingPayload = {
|
||||
customer_id: number;
|
||||
sales_person_id: number;
|
||||
date: string;
|
||||
notes: string;
|
||||
marketing_products: CreateMarketingProductPayload[];
|
||||
};
|
||||
export type UpdateMarketingPayload = CreateMarketingPayload;
|
||||
|
||||
export type CreateMarketingProductPayload = {
|
||||
id?: number;
|
||||
export type BaseCreateMarketingProductPayload = {
|
||||
vehicle_number: string;
|
||||
kandang_id: string | number | undefined;
|
||||
kandang: Kandang | undefined;
|
||||
product_warehouse_id: string | number | undefined;
|
||||
product_warehouse: ProductWarehouse | undefined;
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
uom: string | undefined;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
delivery_date?: string | null;
|
||||
};
|
||||
export type UpdateMarketingProductPayload = CreateMarketingProductPayload;
|
||||
|
||||
/**
|
||||
* Payload Data Types Sales Order
|
||||
*/
|
||||
|
||||
export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
|
||||
marketing_products: CreateSalesOrderProductPayload[];
|
||||
};
|
||||
|
||||
export type CreateSalesOrderProductPayload =
|
||||
BaseCreateMarketingProductPayload & {
|
||||
id?: number;
|
||||
kandang?: Kandang | undefined;
|
||||
product_warehouse?: ProductWarehouse | undefined;
|
||||
};
|
||||
|
||||
export type CreateDeliveryOrderPayload = {
|
||||
marketing_id?: number;
|
||||
delivery_products: CreateDeliveryOrderProductPayload[];
|
||||
};
|
||||
|
||||
export type CreateDeliveryOrderProductPayload =
|
||||
BaseCreateMarketingProductPayload & {
|
||||
id?: number;
|
||||
marketing_product_id: number;
|
||||
delivery_date: string;
|
||||
};
|
||||
|
||||
export type UpdateSalesOrderProductPayload = CreateSalesOrderProductPayload;
|
||||
|
||||
export type UpdateDeliveryOrderProductPayload =
|
||||
CreateDeliveryOrderProductPayload;
|
||||
|
||||
export type UpdateSalesOrderPayload = CreateSalesOrderPayload;
|
||||
|
||||
export type UpdateDeliveryOrderPayload = CreateDeliveryOrderPayload;
|
||||
|
||||
+6
-4
@@ -10,9 +10,6 @@ export type BaseProjectFlock = {
|
||||
name?: string;
|
||||
flock_name?: string;
|
||||
status: string;
|
||||
flock?: Flock;
|
||||
flock_i?: number;
|
||||
flock_name: string;
|
||||
area: Area;
|
||||
area_id: number;
|
||||
category: string;
|
||||
@@ -41,7 +38,6 @@ export type CreateProjectFlockPayload = {
|
||||
category: string;
|
||||
fcr_id: number;
|
||||
location_id: number;
|
||||
period: number;
|
||||
kandang_ids: number[];
|
||||
};
|
||||
|
||||
@@ -71,3 +67,9 @@ export type ProjectFlockAvailableQuantity = {
|
||||
available_qty: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ProjectFlockPeriods = {
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[];
|
||||
|
||||
Reference in New Issue
Block a user