Merge branch 'feat/FE/US-79/egg-grading' into 'development'

[FEAT/FE][US#78|US#79] Add Feature Daily Recording Laying, Grading and Adjusting Recording Growing

See merge request mbugroup/lti-web-client!58
This commit is contained in:
Adnan Zahir
2025-11-21 09:15:02 +07:00
35 changed files with 7105 additions and 3465 deletions
-3
View File
@@ -40,8 +40,5 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# prettier
.prettierrc
# idea # idea
.idea .idea
+14 -19
View File
@@ -13,7 +13,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
@@ -33,7 +32,6 @@
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -1647,13 +1645,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/inputmask": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz",
"integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1689,6 +1680,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -1758,6 +1750,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@@ -2275,6 +2268,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2817,7 +2811,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.3.10", "version": "5.3.10",
@@ -3261,6 +3256,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3434,6 +3430,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -4248,12 +4245,6 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inputmask": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz",
"integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==",
"license": "MIT"
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4725,9 +4716,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5790,6 +5781,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5820,6 +5812,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -6635,6 +6628,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -6802,6 +6796,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
-2
View File
@@ -16,7 +16,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
@@ -36,7 +35,6 @@
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const AddGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recording_id');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId && recordingId !== 'new' ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (
recordingId &&
recordingId !== 'new' &&
!isLoadingRecording &&
(!recording || !isResponseSuccess(recording))
) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{recordingId && recordingId !== 'new' && isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{(!recordingId ||
recordingId === 'new' ||
(!isLoadingRecording && recording && isResponseSuccess(recording))) && (
<GradingForm
type='add'
initialValues={
isResponseSuccess(recording) ? recording.data?.eggs?.[0] : undefined
}
/>
)}
</div>
);
};
export default AddGrading;
@@ -0,0 +1,53 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const EditGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const recordingId = searchParams.get('recordingId');
const gradingId = searchParams.get('gradingId');
const { data: recording, isLoading: isLoadingRecording } = useSWR(
recordingId ? [recordingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!recordingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingRecording && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingRecording && recording && isResponseSuccess(recording) && (
<GradingForm
type='edit'
initialValues={recording.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId || '0')
)}
/>
)}
</div>
);
};
export default EditGrading;
@@ -0,0 +1,52 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm';
import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper';
const DetailGrading = () => {
const router = useRouter();
const searchParams = useSearchParams();
const gradingId = searchParams.get('gradingId');
const { data: grading, isLoading: isLoadingGrading } = useSWR(
gradingId ? [gradingId] : null,
([id]) => RecordingApi.getSingle(parseInt(id))
);
if (!gradingId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingGrading && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingGrading && grading && isResponseSuccess(grading) && (
<GradingForm
type='detail'
initialValues={grading.data.eggs?.find(
(egg) => egg.id === parseInt(gradingId)
)}
/>
)}
</div>
);
};
export default DetailGrading;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
+3 -2
View File
@@ -3,6 +3,7 @@
import { HTMLAttributes, ReactNode } from 'react'; import { HTMLAttributes, ReactNode } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import Image from 'next/image';
export interface CardProps export interface CardProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> { extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
@@ -106,7 +107,7 @@ const Card = ({
return ( return (
<div className={getCardClasses()} {...props}> <div className={getCardClasses()} {...props}>
<figure> <figure>
<img <Image
src={image} src={image}
alt={imageAlt || title || 'Card image'} alt={imageAlt || title || 'Card image'}
className={getImageClasses()} className={getImageClasses()}
@@ -127,7 +128,7 @@ const Card = ({
<div className={getCardClasses()} {...props}> <div className={getCardClasses()} {...props}>
{image && ( {image && (
<figure> <figure>
<img <Image
src={image} src={image}
alt={imageAlt || title || 'Card image'} alt={imageAlt || title || 'Card image'}
className={getImageClasses()} className={getImageClasses()}
+67 -15
View File
@@ -9,6 +9,11 @@ interface FormActionsProps<T> {
editUrl?: string; editUrl?: string;
onDelete?: () => void; onDelete?: () => void;
disableSubmit?: boolean; disableSubmit?: boolean;
onApprove?: () => void;
onReject?: () => void;
isApproveLoading?: boolean;
isRejectLoading?: boolean;
showApproveReject?: boolean;
} }
export const FormActions = <T,>({ export const FormActions = <T,>({
@@ -17,25 +22,32 @@ export const FormActions = <T,>({
editUrl, editUrl,
onDelete, onDelete,
disableSubmit = false, disableSubmit = false,
onApprove,
onReject,
isApproveLoading = false,
isRejectLoading = false,
showApproveReject = false,
}: FormActionsProps<T>) => { }: FormActionsProps<T>) => {
return ( return (
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && onDelete && ( {type !== 'add' && (
<div className='flex flex-row justify-start gap-2'> <div className='flex flex-row justify-start gap-2'>
<Button {onDelete && (
type='button' <Button
color='error' type='button'
onClick={onDelete} color='error'
className='px-4' onClick={onDelete}
> className='px-4'
<Icon >
icon='material-symbols:delete-outline-rounded' <Icon
width={24} icon='material-symbols:delete-outline-rounded'
height={24} width={24}
className='justify-start text-sm' height={24}
/> className='justify-start text-sm'
Delete />
</Button> Delete
</Button>
)}
{type !== 'edit' && editUrl && ( {type !== 'edit' && editUrl && (
<Button <Button
type='button' type='button'
@@ -52,6 +64,46 @@ export const FormActions = <T,>({
Edit Edit
</Button> </Button>
)} )}
{type === 'detail' &&
showApproveReject &&
(onApprove || onReject) && (
<>
{onApprove && (
<Button
type='button'
color='success'
onClick={onApprove}
className='px-4'
isLoading={isApproveLoading}
>
<Icon
icon='material-symbols:check-circle-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Approve
</Button>
)}
{onReject && (
<Button
type='button'
color='error'
onClick={onReject}
className='px-4'
isLoading={isRejectLoading}
>
<Icon
icon='material-symbols:cancel-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Reject
</Button>
)}
</>
)}
</div> </div>
)} )}
{type !== 'detail' && ( {type !== 'detail' && (
+2 -2
View File
@@ -49,8 +49,8 @@ const NumberInput = ({
onValueChange={valueChangeHandler} onValueChange={valueChangeHandler}
decimalScale={decimalScale} decimalScale={decimalScale}
allowNegative={allowNegative} allowNegative={allowNegative}
startAdornment={inputPrefix} inputPrefix={inputPrefix}
endAdornment={inputSuffix} inputSuffix={inputSuffix}
{...restProps} {...restProps}
/> />
); );
+111 -29
View File
@@ -31,6 +31,8 @@ export interface TextInputProps {
errorMessage?: string; errorMessage?: string;
startAdornment?: ReactNode; startAdornment?: ReactNode;
endAdornment?: ReactNode; endAdornment?: ReactNode;
inputPrefix?: ReactNode;
inputSuffix?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>; onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>; onBlur?: FocusEventHandler<HTMLInputElement>;
} }
@@ -48,6 +50,8 @@ const TextInput = ({
errorMessage, errorMessage,
startAdornment, startAdornment,
endAdornment, endAdornment,
inputPrefix,
inputSuffix,
disabled = false, disabled = false,
required = false, required = false,
onChange, onChange,
@@ -85,39 +89,117 @@ const TextInput = ({
</label> </label>
)} )}
<div {inputPrefix || inputSuffix ? (
className={cn( <div className='relative flex'>
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200', {inputPrefix && (
{ <div
'border-error': isError, className={cn(
'border-success!': isValid, 'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
}, {
className?.inputWrapper 'bg-gray-100 border-gray-300': !disabled,
)} 'bg-gray-50 border-gray-200': disabled,
> }
{startAdornment && startAdornment} )}
>
{inputPrefix}
</div>
)}
<input <div
type={type} className={cn(
id={name} 'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
name={name} {
placeholder={placeholder} 'border-error': isError,
value={value} 'border-success!': isValid,
onChange={onChange} 'rounded-l-none!': inputPrefix,
onBlur={onBlur} 'rounded-r-none!': inputSuffix,
disabled={disabled} 'input-disabled': disabled,
className={cn('grow', className?.input)} 'cursor-not-allowed': disabled,
readOnly={readOnly} 'bg-gray-50': disabled,
/> },
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
{(isLoading || endAdornment) && ( <input
<div className='flex flex-row gap-2'> type={type}
{isLoading && <span className='loading loading-spinner' />} id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn(
'grow bg-transparent outline-none',
{
'cursor-not-allowed': disabled,
'text-gray-500': disabled,
},
className?.input
)}
readOnly={readOnly}
/>
{endAdornment && endAdornment} {(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div> </div>
)}
</div> {inputSuffix && (
<div
className={cn(
'inline-flex items-center px-4 py-2 border border-l-0 rounded-r-md transition-all duration-200',
{
'bg-gray-100 border-gray-300': !disabled,
'bg-gray-50 border-gray-200': disabled,
}
)}
>
{inputSuffix}
</div>
)}
</div>
) : (
<div
className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white',
{
'border-error': isError,
'border-success!': isValid,
},
className?.inputWrapper
)}
>
{startAdornment && startAdornment}
<input
type={type}
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('grow', className?.input)}
readOnly={readOnly}
/>
{(isLoading || endAdornment) && (
<div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />}
{endAdornment && endAdornment}
</div>
)}
</div>
)}
{!isError && bottomLabel && ( {!isError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p> <p className='w-full text-sm opacity-60'>{bottomLabel}</p>
+2 -1
View File
@@ -158,6 +158,7 @@ export const formatGroupedApprovalsToApprovalSteps = (
if (approvalGroup.approvals) { if (approvalGroup.approvals) {
switch (approvalGroup?.approvals[0]?.action) { switch (approvalGroup?.approvals[0]?.action) {
case 'CREATED': case 'CREATED':
case 'UPDATED':
case 'APPROVED': case 'APPROVED':
approvalStatus = 'APPROVED'; approvalStatus = 'APPROVED';
break; break;
@@ -256,7 +257,7 @@ const useApprovalSteps = ({
moduleName: string; moduleName: string;
moduleId: string; moduleId: string;
params?: { params?: {
page: number; page?: number;
limit: number; limit: number;
search?: string; search?: string;
group_step_number?: boolean; group_step_number?: boolean;
@@ -1,24 +1,46 @@
'use client'; 'use client';
import { useState } from 'react'; import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { SortingState } from '@tanstack/react-table'; import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { useModal } from '@/components/Modal'; import { Icon } from '@iconify/react';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { Movement } from '@/types/api/inventory/movement'; import { Movement } from '@/types/api/inventory/movement';
import { MovementApi } from '@/services/api/inventory'; import { MovementApi } from '@/services/api/inventory';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Product } from '@/types/api/master-data/product';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import { TableToolbar } from '@/components/table/TableToolbar'; import { OptionType, useSelect } from '@/components/input/SelectInput';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import Button from '@/components/Button';
import { OptionType } from '@/components/input/SelectInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { TableRowOptions } from '@/components/table/TableRowOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Movement, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RowOptionsMenuWrapper>
);
const MovementTable = () => { const MovementTable = () => {
const { const {
@@ -28,30 +50,47 @@ const MovementTable = () => {
setPageSize, setPageSize,
toQueryString: getTableFilterQueryString, toQueryString: getTableFilterQueryString,
} = useTableFilter({ } = useTableFilter({
initial: { search: '' }, initial: {
paramMap: { page: 'page', pageSize: 'limit' }, search: '',
product: '',
warehouse: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
product: 'product_id',
warehouse: 'warehouse_id',
},
}); });
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
const { const {
data: movements, setInputValue: setProductInputValue,
isLoading, options: productOptions,
mutate: refreshMovements, isLoadingOptions: isLoadingProductOptions,
} = useSWR( } = useSelect<Product>('/products', 'id', 'name');
const {
setInputValue: setWarehouseInputValue,
options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions,
} = useSelect<Warehouse>('/warehouses', 'id', 'name');
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
null
);
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
null
);
const { data: movements, isLoading } = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`, `${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher MovementApi.getAllFetcher
); );
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => { const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value); updateFilter('search', e.target.value);
setPage(1);
}; };
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -60,167 +99,179 @@ const MovementTable = () => {
setPage(1); setPage(1);
}; };
const confirmationModalDeleteClickHandler = async () => { const productChangeHandler = (val: OptionType | OptionType[] | null) => {
setIsDeleteLoading(true); setSelectedProduct(val as OptionType);
try { updateFilter('product', val ? ((val as OptionType).value as string) : '');
await MovementApi.delete(selectedMovement?.id as number);
refreshMovements();
deleteModal.closeModal();
} finally {
setIsDeleteLoading(false);
}
}; };
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedWarehouse(val as OptionType);
updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
};
const movementColumns: ColumnDef<Movement>[] = [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.source_warehouse?.name,
header: 'Gudang Asal',
},
{
accessorFn: (row) => row.destination_warehouse?.name,
header: 'Gudang Tujuan',
},
{
accessorKey: 'transfer_reason',
header: 'Catatan',
},
{
accessorKey: 'transfer_date',
header: 'Tanggal',
cell: (props) =>
new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
},
{
accessorFn: (row) => {
const totalCost = row.deliveries?.reduce(
(sum, d) => sum + (d.shipping_cost_total || 0),
0
);
return totalCost?.toLocaleString('id-ID');
},
header: 'Biaya Pengiriman',
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize = props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu type='dropdown' props={props} />
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu type='collapse' props={props} />
</RowCollapseOptions>
)}
</>
);
},
},
];
return ( return (
<div className='flex flex-col gap-4'> <>
<div className='flex flex-col gap-2 mb-4'> <div className='w-full p-0 sm:p-4'>
<TableToolbar <div className='flex flex-col gap-2 mb-4'>
addButton={{ <div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
href: '/inventory/movement/add', <div className='w-full flex flex-row gap-2'>
label: 'Tambah', <Button
href='/inventory/movement/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</div>
<DebouncedTextInput
name='search'
placeholder='Cari Movement'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<div className='grid grid-cols-12 justify-end gap-4'>
<SelectInput
label='Produk'
options={productOptions}
isLoading={isLoadingProductOptions}
value={selectedProduct}
onChange={productChangeHandler}
onInputChange={setProductInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-4',
}}
/>
<SelectInput
label='Gudang'
options={warehouseOptions}
isLoading={isLoadingWarehouseOptions}
value={selectedWarehouse}
onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue}
isClearable
className={{
wrapper: 'col-span-12 sm:col-span-4',
}}
/>
<SelectInput
label='Baris'
options={ROWS_OPTIONS}
value={{
label: String(tableFilterState.pageSize),
value: tableFilterState.pageSize,
}}
onChange={pageSizeChangeHandler}
className={{
wrapper:
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
}}
/>
</div>
</div>
<Table<Movement>
data={isResponseSuccess(movements) ? movements?.data : []}
columns={movementColumns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
totalItems={
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(movements) && movements?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}} }}
search={{
value: tableFilterState.search,
onChange: searchChangeHandler,
placeholder: 'Cari Movement',
}}
/>
<TableRowSizeSelector
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/> />
</div> </div>
</>
<Table<Movement>
data={isResponseSuccess(movements) ? movements?.data : []}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.source_warehouse?.name,
header: 'Gudang Asal',
},
{
accessorFn: (row) => row.destination_warehouse?.name,
header: 'Gudang Tujuan',
},
{
accessorKey: 'transfer_reason',
header: 'Catatan',
},
{
accessorKey: 'transfer_date',
header: 'Tanggal',
cell: (props) =>
new Date(props.row.original.transfer_date).toLocaleDateString(
'id-ID'
),
},
{
accessorFn: (row) => {
const totalCost = row.deliveries?.reduce(
(sum, d) => sum + (d.shipping_cost_total || 0),
0
);
return totalCost?.toLocaleString('id-ID');
},
header: 'Biaya Pengiriman',
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows =
props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedMovement(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<TableRowOptions
type='dropdown'
recordId={props.row.original.id}
basePath='/inventory/movement'
queryParam='movementId'
showEdit={false}
showDelete={false}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<TableRowOptions
type='collapse'
recordId={props.row.original.id}
basePath='/inventory/movement'
queryParam='movementId'
showEdit={false}
showDelete={false}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
totalItems={
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(movements) && movements?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</div>
); );
}; };
@@ -1,34 +1,82 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement'; import { Movement } from '@/types/api/inventory/movement';
type MovementFormSchemaType = {
transfer_reason: string;
transfer_date: string;
source_warehouse?: {
value: number;
label: string;
area?: string;
location?: string;
} | null;
source_warehouse_id: number;
destination_warehouse?: {
value: number;
label: string;
area?: string;
location?: string;
} | null;
destination_warehouse_id: number;
products: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number | string;
}[];
deliveries: {
delivery_cost?: number | string;
delivery_cost_per_item?: number | string;
document?: File | string | null;
document_path?: string | null;
driver_name: string;
vehicle_plate: string;
supplier?: {
value: number;
label: string;
} | null;
supplier_id: number;
products: {
product?: {
value: number;
label: string;
} | null;
product_id: number;
product_qty: number | string;
}[];
}[];
};
export type ProductSchema = { export type ProductSchema = {
product: { product?: {
value: number; value: number;
label: string; label: string;
} | null; } | null;
product_id: number; product_id: number;
product_qty: number; product_qty: number | string;
}; };
export type DeliverySchema = { export type DeliverySchema = {
delivery_cost?: number | undefined; delivery_cost?: number | string;
delivery_cost_per_item?: number | undefined; delivery_cost_per_item?: number | string;
document?: File | string | null; document?: File | string | null;
document_path?: string | null; document_path?: string | null;
driver_name: string; driver_name: string;
vehicle_plate: string; vehicle_plate: string;
supplier: { supplier?: {
value: number; value: number;
label: string; label: string;
} | null; } | null;
supplier_id: number; supplier_id: number;
products: { products: {
product: { product?: {
value: number; value: number;
label: string; label: string;
} | null; } | null;
product_id: number; product_id: number;
product_qty: number; product_qty: number | string;
}[]; }[];
}; };
@@ -102,38 +150,37 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
.required('Produk wajib diisi!'), .required('Produk wajib diisi!'),
}); });
export const MovementFormSchema = Yup.object({ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'), Yup.object({
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'), transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
source_warehouse: Yup.object({ transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
value: Yup.number().min(1).required(), source_warehouse: Yup.object({
label: Yup.string().required(), value: Yup.number().min(1).required(),
area: Yup.string().optional(), label: Yup.string().required(),
location: Yup.string().optional(), area: Yup.string().optional(),
}).nullable(), location: Yup.string().optional(),
source_warehouse_id: Yup.number() }).nullable(),
.required('Gudang asal wajib diisi!') source_warehouse_id: Yup.number()
.typeError('Gudang asal wajib diisi!'), .required('Gudang asal wajib diisi!')
destination_warehouse: Yup.object({ .typeError('Gudang asal wajib diisi!'),
value: Yup.number().min(1).required(), destination_warehouse: Yup.object({
label: Yup.string().required(), value: Yup.number().min(1).required(),
area: Yup.string().optional(), label: Yup.string().required(),
location: Yup.string().optional(), area: Yup.string().optional(),
}).nullable(), location: Yup.string().optional(),
destination_warehouse_id: Yup.number() }).nullable(),
.required('Gudang tujuan wajib diisi!') destination_warehouse_id: Yup.number()
.typeError('Gudang tujuan wajib diisi!'), .required('Gudang tujuan wajib diisi!')
products: Yup.array() .typeError('Gudang tujuan wajib diisi!'),
.of(ProductObjectSchema) products: Yup.array()
.min(1, 'Minimal harus ada 1 produk!') .of(ProductObjectSchema)
.required('Produk wajib diisi!'), .min(1, 'Minimal harus ada 1 produk!')
deliveries: Yup.array() .required('Produk wajib diisi!'),
.of(DeliveryObjectSchema) deliveries: Yup.array()
.min(1, 'Minimal harus ada 1 pengiriman!') .of(DeliveryObjectSchema)
.required('Pengiriman wajib diisi!'), .min(1, 'Minimal harus ada 1 pengiriman!')
}); .required('Pengiriman wajib diisi!'),
});
export const UpdateMovementFormSchema = MovementFormSchema;
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>; export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
File diff suppressed because it is too large Load Diff
@@ -1,95 +0,0 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { MovementApi } from '@/services/api/inventory';
import {
CreateMovementPayload,
UpdateMovementPayload,
} from '@/types/api/inventory/movement';
import { isResponseError } from '@/lib/api-helper';
export const useMovementFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createMovementHandler = useCallback(
async (payload: CreateMovementPayload, documents: File[] = []) => {
const formData = new FormData();
formData.append('data', JSON.stringify(payload));
documents.forEach((file, index) => {
formData.append(`documents[${index}]`, file);
});
const res = await MovementApi.create(
formData as unknown as CreateMovementPayload
);
if (isResponseError(res)) {
setMovementFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/inventory/movement');
},
[router]
);
const updateMovementHandler = useCallback(
async (
movementId: number,
payload: UpdateMovementPayload,
documents: File[] = []
) => {
let finalPayload: UpdateMovementPayload | FormData;
if (documents.length > 0) {
const formData = new FormData();
formData.append('data', JSON.stringify(payload));
documents.forEach((file, index) => {
formData.append(`documents[${index}]`, file);
});
finalPayload = formData as unknown as UpdateMovementPayload;
} else {
finalPayload = payload;
}
const res = await MovementApi.update(movementId, finalPayload);
if (res?.status === 'error') {
setMovementFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/inventory/movement');
},
[router]
);
const deleteMovementClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await MovementApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Movement!');
setIsDeleteLoading(false);
router.push('/inventory/movement');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
movementFormErrorMessage,
isDeleteLoading,
createMovementHandler,
updateMovementHandler,
deleteMovementClickHandler,
confirmationModalDeleteClickHandler,
};
};
@@ -1,11 +1,17 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
export const ProductCategoryFormSchema = Yup.object({ type ProductCategoryFormSchemaType = {
code: Yup.string() code: string;
.required('Kode wajib diisi!') name: string;
.max(3, 'Kode kategori produk melebihi 3 karakter!'), };
name: Yup.string().required('Nama wajib diisi!'),
}); export const ProductCategoryFormSchema: Yup.ObjectSchema<ProductCategoryFormSchemaType> =
Yup.object({
code: Yup.string()
.required('Kode wajib diisi!')
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
name: Yup.string().required('Nama wajib diisi!'),
});
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema; export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
@@ -71,12 +71,13 @@ const ProductCategoryForm = ({
[router] [router]
); );
const formikInitialValues = useMemo<ProductCategoryFormValues>(() => { const formikInitialValues = useMemo<ProductCategoryFormValues>(
return { () => ({
code: initialValues?.code ?? '', code: initialValues?.code ?? '',
name: initialValues?.name ?? '', name: initialValues?.name ?? '',
}; }),
}, [initialValues]); [initialValues]
);
const formik = useFormik<ProductCategoryFormValues>({ const formik = useFormik<ProductCategoryFormValues>({
initialValues: formikInitialValues, initialValues: formikInitialValues,
@@ -118,7 +119,7 @@ const ProductCategoryForm = ({
await ProductCategoryApi.delete(initialValues?.id as number); await ProductCategoryApi.delete(initialValues?.id as number);
deleteModal.closeModal(); deleteModal.closeModal();
toast.success('Successfully delete Product Category!'); toast.success('Berhasil menghapus data Kategori Produk!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
router.push('/master-data/product-category'); router.push('/master-data/product-category');
}; };
@@ -129,7 +130,7 @@ const ProductCategoryForm = ({
return ( return (
<> <>
<section className='w-full max-w-xl'> <section className='w-full max-w-2xl'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href='/master-data/product-category' href='/master-data/product-category'
@@ -141,9 +142,9 @@ const ProductCategoryForm = ({
</Button> </Button>
<h1 className='text-2xl font-bold text-center'> <h1 className='text-2xl font-bold text-center'>
{type === 'add' && 'Tambah Product Category'} {type === 'add' && 'Tambah Kategori Produk'}
{type === 'edit' && 'Edit Product Category'} {type === 'edit' && 'Edit Kategori Produk'}
{type === 'detail' && 'Detail Product Category'} {type === 'detail' && 'Detail Kategori Produk'}
</h1> </h1>
</header> </header>
@@ -157,7 +158,7 @@ const ProductCategoryForm = ({
required required
label='Kode' label='Kode'
name='code' name='code'
placeholder='Masukkan kode kategori produk' placeholder='Masukkan kode...'
value={formik.values.code} value={formik.values.code}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -169,7 +170,7 @@ const ProductCategoryForm = ({
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama kategori produk' placeholder='Masukkan nama...'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -256,7 +257,7 @@ const ProductCategoryForm = ({
<ConfirmationModal <ConfirmationModal
ref={deleteModal.ref} ref={deleteModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin menghapus data Product Category ini (${initialValues?.name})?`} text={`Apakah anda yakin ingin menghapus data Kategori Produk ini (${initialValues?.name})?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
@@ -1,53 +1,86 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
export const ProductFormSchema = Yup.object({ type ProductFormSchemaType = {
name: Yup.string().required('Nama wajib diisi!'), name: string;
brand: Yup.string().required('Merek wajib diisi!'), brand: string;
sku: Yup.string().required('SKU wajib diisi!'), sku: string;
uom: Yup.object({ uom?: {
value: Yup.number().min(1).required(), value: number;
label: Yup.string().required(), label: string;
}).nullable(), } | null;
uom_id: Yup.number() uom_id: number;
.required('Satuan wajib diisi!') product_category?: {
.typeError('Satuan wajib diisi!'), value: number;
product_category: Yup.object({ label: string;
value: Yup.number().min(1).required(), } | null;
label: Yup.string().required(), product_category_id: number;
}).nullable(), product_price: number | string;
product_category_id: Yup.number() selling_price: number | string;
.required('Kategori produk wajib diisi!') tax: number | string;
.typeError('Kategori produk wajib diisi!'), expiry_period: number | string;
product_price: Yup.number() supplier_ids: number[];
.required('Harga produk wajib diisi!') flags: string[];
.typeError('Harga produk wajib diisi!') };
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number() export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
.required('Harga jual wajib diisi!') Yup.object({
.typeError('Harga jual wajib diisi!') name: Yup.string().required('Nama wajib diisi!'),
.min(0, 'Harga jual tidak boleh kurang dari 0!'), brand: Yup.string().required('Merek wajib diisi!'),
tax: Yup.number() sku: Yup.string().required('SKU wajib diisi!'),
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!') uom: Yup.object({
.min(0, 'Pajak tidak boleh kurang dari 0!') value: Yup.number().min(1).required(),
.max(100, 'Pajak tidak boleh lebih dari 100%!'), label: Yup.string().required(),
expiry_period: Yup.number() })
.required('Periode kadaluarsa wajib diisi!') .nullable()
.typeError('Periode kadaluarsa wajib diisi!') .required('Satuan wajib diisi!'),
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier: Yup.object({ uom_id: Yup.number()
value: Yup.number().min(1).required(), .required('Satuan wajib diisi!')
label: Yup.string().required(), .typeError('Satuan wajib diisi!'),
}).nullable(),
supplier_ids: Yup.array() product_category: Yup.object({
.of(Yup.number().typeError('Supplier tidak valid!')) value: Yup.number().min(1).required(),
.min(1, 'Minimal harus ada 1 supplier!') label: Yup.string().required(),
.required('Supplier wajib diisi!'), })
flags: Yup.array() .nullable()
.of(Yup.string()) .required('Kategori produk wajib diisi!'),
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'), product_category_id: Yup.number()
}); .required('Kategori produk wajib diisi!')
.typeError('Kategori produk wajib diisi!'),
product_price: Yup.number()
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
flags: Yup.array()
.of(Yup.string().required())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
});
export const UpdateProductFormSchema = ProductFormSchema; export const UpdateProductFormSchema = ProductFormSchema;
@@ -9,7 +9,11 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
@@ -79,20 +83,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: initialValues?.sku ?? '', sku: initialValues?.sku ?? '',
uom: initialValues?.uom uom: initialValues?.uom
? { value: initialValues.uom.id, label: initialValues.uom.name } ? { value: initialValues.uom.id, label: initialValues.uom.name }
: null, : undefined,
uom_id: initialValues?.uom?.id ?? 0, uom_id: initialValues?.uom?.id ?? 0,
product_category: initialValues?.product_category product_category: initialValues?.product_category
? { ? {
value: initialValues.product_category.id, value: initialValues.product_category.id,
label: initialValues.product_category.name, label: initialValues.product_category.name,
} }
: null, : undefined,
product_category_id: initialValues?.product_category?.id ?? 0, product_category_id: initialValues?.product_category?.id ?? 0,
product_price: initialValues?.product_price ?? 0, product_price: initialValues?.product_price ?? '',
selling_price: initialValues?.selling_price ?? 0, selling_price: initialValues?.selling_price ?? '',
tax: initialValues?.tax ?? 0, tax: initialValues?.tax ?? '',
expiry_period: initialValues?.expiry_period ?? 0, expiry_period: initialValues?.expiry_period ?? '',
supplier: null, // not used for payload, just for UI
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [], supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
flags: initialValues?.flags ?? [], flags: initialValues?.flags ?? [],
}), }),
@@ -111,16 +114,14 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: values.sku, sku: values.sku,
uom_id: values.uom_id, uom_id: values.uom_id,
product_category_id: values.product_category_id, product_category_id: values.product_category_id,
product_price: values.product_price, product_price: parseInt(values.product_price.toString()) || 0,
selling_price: values.selling_price, selling_price: parseInt(values.selling_price.toString()) || 0,
tax: values.tax, tax: parseInt(values.tax.toString()) || 0,
expiry_period: values.expiry_period, expiry_period: parseInt(values.expiry_period.toString()) || 0,
supplier_ids: (values.supplier_ids ?? []).filter( supplier_ids: values.supplier_ids.filter(
(id): id is number => typeof id === 'number' (id): id is number => typeof id === 'number'
), ),
flags: (values.flags ?? []).filter( flags: values.flags.filter((f): f is string => typeof f === 'string'),
(f): f is string => typeof f === 'string'
),
}; };
switch (type) { switch (type) {
case 'add': case 'add':
@@ -136,15 +137,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// UOM // UOM
const [uomSelectInputValue, setUomSelectInputValue] = useState(''); const {
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`; setInputValue: setUomSelectInputValue,
const { data: uoms, isLoading: isLoadingUoms } = useSWR( options: uomOptions,
uomsUrl, isLoadingOptions: isLoadingUoms,
UomApi.getAllFetcher } = useSelect(UomApi.basePath, 'id', 'name');
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name }))
: [];
const uomChangeHandler = (val: OptionType | OptionType[] | null) => { const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true); formik.setFieldTouched('uom', true);
formik.setFieldValue('uom', val); formik.setFieldValue('uom', val);
@@ -153,15 +150,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
}; };
// Product Category // Product Category
const [categorySelectInputValue, setCategorySelectInputValue] = useState(''); const {
const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`; setInputValue: setCategorySelectInputValue,
const { data: categories, isLoading: isLoadingCategories } = useSWR( options: categoryOptions,
categoriesUrl, isLoadingOptions: isLoadingCategories,
ProductCategoryApi.getAllFetcher } = useSelect(ProductCategoryApi.basePath, 'id', 'name');
);
const categoryOptions = isResponseSuccess(categories)
? categories?.data.map((cat) => ({ value: cat.id, label: cat.name }))
: [];
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('product_category', true); formik.setFieldTouched('product_category', true);
formik.setFieldValue('product_category', val); formik.setFieldValue('product_category', val);
@@ -169,7 +162,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
formik.setFieldValue('product_category_id', (val as OptionType)?.value); formik.setFieldValue('product_category_id', (val as OptionType)?.value);
}; };
// Supplier (multi select) // Supplier (multi select) - using SWR to filter by category
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`; const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
@@ -209,7 +202,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
return ( return (
<> <>
<section className='w-full max-w-xl'> <section className='w-full max-w-2xl'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href='/master-data/product' href='/master-data/product'
@@ -235,7 +228,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama produk' placeholder='Masukkan nama...'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -247,7 +240,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required required
label='Merek' label='Merek'
name='brand' name='brand'
placeholder='Masukkan merek produk' placeholder='Masukkan merek...'
value={formik.values.brand} value={formik.values.brand}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -259,7 +252,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
required required
label='SKU' label='SKU'
name='sku' name='sku'
placeholder='Masukkan SKU produk' placeholder='Masukkan SKU...'
value={formik.values.sku} value={formik.values.sku}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -270,6 +263,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput <SelectInput
required required
label='Satuan' label='Satuan'
placeholder='Pilih satuan...'
value={formik.values.uom ?? undefined} value={formik.values.uom ?? undefined}
onChange={uomChangeHandler} onChange={uomChangeHandler}
options={uomOptions} options={uomOptions}
@@ -283,6 +277,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput <SelectInput
required required
label='Kategori Produk' label='Kategori Produk'
placeholder='Pilih kategori produk...'
value={formik.values.product_category ?? undefined} value={formik.values.product_category ?? undefined}
onChange={categoryChangeHandler} onChange={categoryChangeHandler}
options={categoryOptions} options={categoryOptions}
@@ -296,15 +291,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
/> />
<TextInput <NumberInput
required required
label='Harga Produk' label='Harga Produk'
name='product_price' name='product_price'
type='number' placeholder='Masukkan harga produk...'
placeholder='Masukkan harga produk'
value={formik.values.product_price} value={formik.values.product_price}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={ isError={
formik.touched.product_price && formik.touched.product_price &&
Boolean(formik.errors.product_price) Boolean(formik.errors.product_price)
@@ -312,15 +311,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.product_price as string} errorMessage={formik.errors.product_price as string}
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <NumberInput
required required
label='Harga Jual' label='Harga Jual'
name='selling_price' name='selling_price'
type='number' placeholder='Masukkan harga jual...'
placeholder='Masukkan harga jual'
value={formik.values.selling_price} value={formik.values.selling_price}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={ isError={
formik.touched.selling_price && formik.touched.selling_price &&
Boolean(formik.errors.selling_price) Boolean(formik.errors.selling_price)
@@ -328,28 +331,36 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
errorMessage={formik.errors.selling_price as string} errorMessage={formik.errors.selling_price as string}
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <NumberInput
required required
label='Pajak (%)' label='Pajak (%)'
name='tax' name='tax'
type='number' placeholder='Masukkan pajak...'
placeholder='Masukkan pajak'
value={formik.values.tax} value={formik.values.tax}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='%'
isError={formik.touched.tax && Boolean(formik.errors.tax)} isError={formik.touched.tax && Boolean(formik.errors.tax)}
errorMessage={formik.errors.tax as string} errorMessage={formik.errors.tax as string}
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <NumberInput
required required
label='Periode Kadaluarsa (hari)' label='Periode Kadaluarsa (hari)'
name='expiry_period' name='expiry_period'
type='number' placeholder='Masukkan periode kadaluarsa...'
placeholder='Masukkan periode kadaluarsa'
value={formik.values.expiry_period} value={formik.values.expiry_period}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='hari'
isError={ isError={
formik.touched.expiry_period && formik.touched.expiry_period &&
Boolean(formik.errors.expiry_period) Boolean(formik.errors.expiry_period)
@@ -360,9 +371,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput <SelectInput
required required
label='Supplier' label='Supplier'
placeholder='Pilih supplier...'
isMulti isMulti
value={supplierOptions.filter((opt) => value={supplierOptions.filter((opt) =>
formik.values.supplier_ids.includes(opt.value) (formik.values.supplier_ids || []).includes(opt.value)
)} )}
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions} options={supplierOptions}
@@ -379,9 +391,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput <SelectInput
required required
label='Flags' label='Flags'
placeholder='Pilih flags...'
isMulti isMulti
value={PRODUCT_FLAG_OPTIONS.filter((opt) => value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
formik.values.flags.includes(opt.value) (formik.values.flags || []).includes(opt.value)
)} )}
onChange={(val) => { onChange={(val) => {
const arr = Array.isArray(val) ? val : val ? [val] : []; const arr = Array.isArray(val) ? val : val ? [val] : [];
File diff suppressed because it is too large Load Diff
@@ -1,212 +1,320 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; import {
import { Recording } from '@/types/api/production/recording'; Recording,
CreateGrowingRecordingPayload,
CreateLayingRecordingPayload,
CreateEggPayload,
CreateGradingPayload,
} from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({ type RecordingGrowingFormSchemaType = {
flock: Yup.object({ project_flock_kandang: {
value: Yup.number().min(1).required(), value: number;
label: Yup.string().required(), label: string;
}).nullable(), } | null;
flock_id: Yup.number() project_flock_kandang_id: number;
.default(0) body_weights: {
.typeError('Flock wajib diisi!') weight: number | string;
.test( avg_weight: number | string;
'is-valid-flock', qty: number | string;
'Flock wajib diisi!', }[];
(value) => value !== undefined && value !== null && value > 0 stocks: {
) product_warehouse_id: number;
.required('Flock wajib diisi!'), qty: number | string;
location: Yup.object({ }[];
value: Yup.number().min(1).required(), depletions: {
label: Yup.string().required(), product_warehouse_id: number;
}).nullable(), qty: number | string;
location_id: Yup.number() }[];
.default(0) };
.typeError('Lokasi wajib diisi!')
.test( type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
'is-valid-location', eggs: {
'Lokasi wajib diisi!', product_warehouse_id: number;
(value) => value !== undefined && value !== null && value > 0 qty: number | string;
) }[];
.required('Lokasi wajib diisi!'), };
coop: Yup.object({
value: Yup.number().min(1).required(), type RecordingGradingFormSchemaType = {
label: Yup.string().required(), eggs_grading: {
}).nullable(), recording_egg_id: number;
coop_id: Yup.number() grade: string;
.default(0) qty: number | string;
.typeError('Kandang wajib diisi!') }[];
.test( };
'is-valid-coop',
'Kandang wajib diisi!', export type BodyWeightSchema = {
(value) => value !== undefined && value !== null && value > 0 weight: number | string;
) avg_weight: number | string;
.required('Kandang wajib diisi!'), qty: number | string;
recording_date: Yup.date() };
.required('Tanggal recording wajib diisi')
.typeError('Format tanggal tidak valid'), export type StockSchema = {
feed_data: Yup.array() product_warehouse_id: number;
.of( qty: number | string;
Yup.object({ };
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.mixed<number | ''>().notRequired(), export type DepletionSchema = {
feed_stock: Yup.number() product_warehouse_id: number;
.required('Jumlah pakan yang digunakan wajib diisi!') qty: number | string;
.min(1, 'Jumlah pakan minimal 1!') };
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
.test( export type EggSchema = {
'is-not-exceed-qty', product_warehouse_id: number;
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!', qty: number | string;
function (value) { };
const { feed_qty } = this.parent;
if (value === undefined) return true; const BodyWeightObjectSchema: Yup.ObjectSchema<BodyWeightSchema> = Yup.object({
if ( weight: Yup.number()
feed_qty === undefined || .required('Berat ayam total wajib diisi!')
feed_qty === '' || .min(1, 'Berat ayam total minimal 1 gram!')
typeof feed_qty !== 'number' .typeError('Berat ayam total harus berupa angka!'),
) avg_weight: Yup.number()
return true; .required('Berat ayam rata-rata wajib diisi!')
return value <= feed_qty; .typeError('Berat ayam rata-rata harus berupa angka!'),
} qty: Yup.number()
), .required('Jumlah ayam wajib diisi!')
}) .min(1, 'Jumlah ayam minimal 1 ekor!')
) .typeError('Jumlah ayam harus berupa angka!'),
.min(1, 'Minimal harus ada 1 data pakan!')
.required('Data pakan wajib diisi!'),
body_weight: Yup.array()
.of(
Yup.object({
chicken_weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
chicken_count: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!'),
average_chicken_weight: Yup.number()
.required('Rata-rata berat ayam wajib diisi!')
.min(1, 'Rata-rata berat ayam minimal 1 gram!')
.typeError('Rata-rata berat ayam harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
vaccination: Yup.array()
.of(
Yup.object({
vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
total_stock: Yup.mixed<number | ''>().notRequired(),
used_stock: Yup.number()
.required('Jumlah vaksin yang digunakan wajib diisi!')
.min(1, 'Jumlah vaksin minimal 1!')
.typeError('Jumlah vaksin yang digunakan harus berupa angka!')
.test(
'is-not-exceed-total',
'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { total_stock } = this.parent;
if (value === undefined) return true;
if (
total_stock === undefined ||
total_stock === '' ||
typeof total_stock !== 'number'
)
return true;
return value <= total_stock;
}
),
})
)
.min(1, 'Minimal harus ada 1 data vaksinasi!')
.required('Data vaksinasi wajib diisi!'),
mortality: Yup.array()
.of(
Yup.object({
condition: Yup.mixed<string>()
.oneOf(
RECORDING_FLAG_OPTIONS.map((opt) => opt.value),
'Kondisi tidak valid!'
)
.required('Kondisi wajib diisi!'),
count: Yup.number()
.required('Jumlah mortalitas wajib diisi!')
.min(1, 'Jumlah mortalitas minimal 1 ekor!')
.typeError('Jumlah mortalitas harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data mortalitas!')
.required('Data mortalitas wajib diisi!'),
}); });
export const UpdateRecordingFormSchema = RecordingFormSchema; const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk wajib diisi!')
.min(1, 'Produk wajib diisi!')
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.required('Jumlah penggunaan wajib diisi!')
.min(1, 'Jumlah penggunaan tidak boleh 0!')
.typeError('Jumlah penggunaan harus berupa angka!'),
});
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>; const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.typeError('Jumlah depletions harus berupa angka!'),
});
export const getRecordingFormInitialValues = ( const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
initialValues?: Recording product_warehouse_id: Yup.number()
): RecordingFormValues => ({ .required('Kondisi telur wajib diisi!')
flock: initialValues?.flock .min(1, 'Kondisi telur wajib diisi!')
.typeError('Kondisi telur harus berupa angka!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
Yup.object({
project_flock_kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!')
.test(
'not-already-recorded',
'Project Flock ini sudah direcord hari ini!',
function (value) {
const recordedProjectFlockIds = this.options.context
?.recordedProjectFlockIds as Set<number>;
const formType = this.options.context?.type as
| 'add'
| 'edit'
| 'detail';
if (formType !== 'add') return true;
if (value && recordedProjectFlockIds?.has(value)) {
return false;
}
return true;
}
),
body_weights: Yup.array()
.of(BodyWeightObjectSchema)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
stocks: Yup.array()
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(DepletionObjectSchema)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
});
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({
eggs: Yup.array()
.of(EggObjectSchema)
.min(1, 'Minimal harus ada 1 data telur!')
.required('Data telur wajib diisi!'),
});
export const UpdateRecordingGrowingFormSchema =
RecordingGrowingFormSchema.shape({
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
});
export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
project_flock_kandang_id: Yup.number()
.default(0)
.typeError('Project Flock Kandang wajib diisi!')
.test(
'is-valid-project-flock-kandang',
'Project Flock Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Project Flock Kandang wajib diisi!'),
});
export const RecordingGradingFormSchema: Yup.ObjectSchema<RecordingGradingFormSchemaType> =
Yup.object({
eggs_grading: Yup.array()
.of(
Yup.object({
recording_egg_id: Yup.number()
.required('Recording Egg ID wajib diisi!')
.min(1, 'Recording Egg ID minimal 1!')
.typeError('Recording Egg ID harus berupa angka!'),
grade: Yup.string()
.required('Grade telur wajib diisi!')
.typeError('Grade telur harus berupa string!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur minimal 1!')
.typeError('Jumlah telur harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data grading telur!')
.required('Data grading telur wajib diisi!'),
});
export const UpdateRecordingGradingFormSchema = RecordingGradingFormSchema;
export type RecordingGrowingFormValues = Yup.InferType<
typeof RecordingGrowingFormSchema
>;
export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema
>;
export type RecordingGradingFormValues = Yup.InferType<
typeof RecordingGradingFormSchema
>;
type RecordingFormData = Partial<Recording> & {
body_weights?: CreateGrowingRecordingPayload['body_weights'];
stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks'];
depletions?:
| CreateGrowingRecordingPayload['depletions']
| Recording['depletions'];
eggs?: CreateLayingRecordingPayload['eggs'] | Recording['eggs'];
project_flock_kandang_id?: number;
project_flock_category?: string;
};
export const getRecordingGrowingFormInitialValues = (
initialValues?: RecordingFormData
): RecordingGrowingFormValues => ({
project_flock_kandang: initialValues?.project_flock_kandang_id
? { ? {
value: initialValues.flock.id, value: initialValues.project_flock_kandang_id,
label: initialValues.flock.name, label: `Project Flock #${initialValues.project_flock_kandang_id}`,
} }
: null, : null,
flock_id: initialValues?.flock?.id ?? 0, project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0,
location: initialValues?.location body_weights: initialValues?.body_weights?.map(
? { (bw: NonNullable<CreateGrowingRecordingPayload['body_weights']>[0]) => ({
value: initialValues.location.id, weight: bw.avg_weight * bw.qty,
label: initialValues.location.name, avg_weight: bw.avg_weight,
} qty: bw.qty,
: null, })
location_id: initialValues?.location?.id ?? 0, ) ?? [
coop: initialValues?.coop
? {
value: initialValues.coop.id,
label: initialValues.coop.name,
}
: null,
coop_id: initialValues?.coop?.id ?? 0,
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{
feed_id: '',
feed_qty: '',
feed_stock: 0,
},
],
body_weight: initialValues?.body_weight ?? [
{ {
chicken_weight: 0, weight: '',
chicken_count: 0, avg_weight: '',
average_chicken_weight: 0, qty: '',
}, },
], ],
vaccination: initialValues?.vaccination stocks: initialValues?.stocks?.map((stock) => ({
? initialValues.vaccination.map((vaccine) => ({ product_warehouse_id: stock.product_warehouse_id,
vaccine_id: vaccine.vaccine_name, qty:
total_stock: vaccine.total_stock, (stock as { qty?: number; usage_amount?: number }).qty ||
used_stock: vaccine.used_stock, (stock as { qty?: number; usage_amount?: number }).usage_amount ||
})) '',
: [ })) ?? [
{
vaccine_id: '',
total_stock: '',
used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
{ {
condition: '', product_warehouse_id: 0,
count: 0, qty: '',
},
],
depletions: initialValues?.depletions?.map(
(
depletion: NonNullable<CreateGrowingRecordingPayload['depletions']>[0]
) => ({
product_warehouse_id: depletion.product_warehouse_id,
qty: depletion.qty,
})
) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
});
export const getRecordingLayingFormInitialValues = (
initialValues?: RecordingFormData
): RecordingLayingFormValues => ({
...getRecordingGrowingFormInitialValues(initialValues),
eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({
product_warehouse_id: egg.product_warehouse_id,
qty: egg.qty,
})) ?? [
{
product_warehouse_id: 0,
qty: '',
},
],
});
export const getRecordingGradingFormInitialValues = (
initialValues?: Partial<CreateGradingPayload> & { recording_egg_id?: number }
): RecordingGradingFormValues => ({
eggs_grading: initialValues?.eggs_grading?.map((grading) => ({
recording_egg_id: grading.recording_egg_id,
grade: grading.grade,
qty: grading.qty,
})) ?? [
{
recording_egg_id: initialValues?.recording_egg_id ?? 0,
grade: '',
qty: '',
}, },
], ],
}); });
File diff suppressed because it is too large Load Diff
@@ -1,70 +0,0 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { RecordingApi } from '@/services/api/production';
import {
CreateRecordingPayload,
UpdateRecordingPayload,
} from '@/types/api/production/recording';
import { isResponseError } from '@/lib/api-helper';
export const useRecordingFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createRecordingHandler = useCallback(
async (payload: CreateRecordingPayload) => {
const res = await RecordingApi.create(payload);
if (isResponseError(res)) {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/flock/recording');
},
[router]
);
const updateRecordingHandler = useCallback(
async (recordingId: number, payload: UpdateRecordingPayload) => {
const res = await RecordingApi.update(recordingId, payload);
if (res?.status === 'error') {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/flock/recording');
},
[router]
);
const deleteRecordingClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await RecordingApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
router.push('/flock/recording');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
recordingFormErrorMessage,
isDeleteLoading,
createRecordingHandler,
updateRecordingHandler,
deleteRecordingClickHandler,
confirmationModalDeleteClickHandler,
};
};
File diff suppressed because it is too large Load Diff
+45
View File
@@ -33,6 +33,51 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
}, },
] as const; ] as const;
export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
] as const;
export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [ export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
+36 -3
View File
@@ -233,9 +233,42 @@ export const SUPPLIER_FLAG_OPTIONS = [
]; ];
export const RECORDING_FLAG_OPTIONS = [ export const RECORDING_FLAG_OPTIONS = [
{ label: 'Ayam Afkir', value: 'Ayam Afkir' }, { label: 'Ayam Afkir', value: 'Afkir' },
{ label: 'Ayam Culling', value: 'Ayam Culling' }, { label: 'Ayam Culling', value: 'Culling' },
{ label: 'Ayam Mati', value: 'Ayam Mati' }, { label: 'Ayam Mati', value: 'Mati' },
];
export const APPROVAL_WORKFLOWS = [
{
key: 'PROJECT_FLOCKS',
steps: [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Aktif',
},
],
},
{
key: 'RECORDINGS',
steps: [
{
step_number: 1,
step_name: 'Grading-Telur',
},
{
step_number: 2,
step_name: 'Pengajuan',
},
{
step_number: 3,
step_name: 'Disetujui',
},
],
},
]; ];
export const ACCEPTED_FILE_TYPE = { export const ACCEPTED_FILE_TYPE = {
+6
View File
@@ -0,0 +1,6 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApproval } from '@/types/api/api-general';
export const ApprovalApi = new BaseApiService<BaseApproval, unknown, unknown>(
'/approvals'
);
+1 -2
View File
@@ -7,7 +7,6 @@ import {
import { import {
CreateMovementPayload, CreateMovementPayload,
Movement, Movement,
UpdateMovementPayload,
} from '@/types/api/inventory/movement'; } from '@/types/api/inventory/movement';
import { import {
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
@@ -23,7 +22,7 @@ export const ProductWarehouseApi = new BaseApiService<
export const MovementApi = new BaseApiService< export const MovementApi = new BaseApiService<
Movement, Movement,
CreateMovementPayload, CreateMovementPayload,
UpdateMovementPayload unknown
>('/inventory/transfers'); >('/inventory/transfers');
export const inventoryAdjustmentApi = new BaseApiService< export const inventoryAdjustmentApi = new BaseApiService<
+100 -3
View File
@@ -1,8 +1,17 @@
import { BaseApiService } from '@/services/api/base'; import { BaseApiService } from './base';
import { BaseApiResponse } from '@/types/api/api-general';
import {
CreateProjectFlockPayload,
ProjectFlock,
UpdateProjectFlockPayload,
} from '@/types/api/production/project-flock';
import { import {
CreateRecordingPayload, CreateRecordingPayload,
Recording, Recording,
UpdateRecordingPayload, UpdateRecordingPayload,
CreateGradingPayload,
UpdateGradingPayload,
NextDayRecording,
} from '@/types/api/production/recording'; } from '@/types/api/production/recording';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
@@ -11,8 +20,96 @@ export const ProjectFlockKandangApi = new BaseApiService<
unknown, unknown,
unknown unknown
>('/production/project-flock-kandangs'); >('/production/project-flock-kandangs');
export const RecordingApi = new BaseApiService< export const ProjectFlockApi = new BaseApiService<
ProjectFlock,
CreateProjectFlockPayload,
UpdateProjectFlockPayload
>('/production/project-flocks');
export class RecordingService extends BaseApiService<
Recording, Recording,
CreateRecordingPayload, CreateRecordingPayload,
UpdateRecordingPayload UpdateRecordingPayload
>('/flock/recordings'); > {
constructor(basePath: string = '') {
super(basePath);
}
async approve(
idOrIds: number | number[],
notes?: string
): Promise<BaseApiResponse<Recording[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Recording[]>>('approvals', {
method: 'POST',
payload: {
action: 'APPROVED',
approvable_ids,
notes,
},
});
}
async reject(
idOrIds: number | number[],
notes: string = ''
): Promise<BaseApiResponse<Recording[]> | undefined> {
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
return await this.customRequest<BaseApiResponse<Recording[]>>('approvals', {
method: 'POST',
payload: {
action: 'REJECTED',
approvable_ids,
notes,
},
});
}
async createGrading(
payload: CreateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>('gradings', {
method: 'POST',
payload,
});
}
async updateGrading(
gradingId: number,
payload: UpdateGradingPayload
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'PUT',
payload,
}
);
}
async deleteGrading(
gradingId: number
): Promise<BaseApiResponse<unknown> | undefined> {
return await this.customRequest<BaseApiResponse<unknown>>(
`gradings/${gradingId}`,
{
method: 'DELETE',
}
);
}
async nextDayRecording(
projectFlockId: number
): Promise<BaseApiResponse<NextDayRecording> | undefined> {
return await this.customRequest<BaseApiResponse<NextDayRecording>>(
`next-day`,
{
method: 'GET',
params: {
project_flock_kandang_id: projectFlockId,
},
}
);
}
}
export const RecordingApi = new RecordingService('/production/recordings');
@@ -0,0 +1,11 @@
import { BaseApiService } from '@/services/api/base';
import {
BaseProjectFlockKandang,
ProjectFlockKandang,
} from '@/types/api/production/project-flock-kandang';
export const ProjectFlockKandangApi = new BaseApiService<
BaseProjectFlockKandang,
ProjectFlockKandang,
unknown
>('project-flock-kandang');
-2
View File
@@ -71,5 +71,3 @@ export type CreateMovementPayload = {
}[]; }[];
}[]; }[];
}; };
export type UpdateMovementPayload = CreateMovementPayload;
+12 -2
View File
@@ -7,8 +7,8 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = { export type BaseProjectFlock = {
id: number; id: number;
name: string; name?: string;
flock_name: string; flock_name?: string;
status: string; status: string;
flock?: Flock; flock?: Flock;
flock_i?: number; flock_i?: number;
@@ -52,6 +52,16 @@ export type ProjectFlockApprovalPayload = {
approvable_ids: number[]; approvable_ids: number[];
}; };
export type ProjectFlockKandangLookup = {
id: number;
project_flock_kandang_id: number;
project_flock_id: number;
kandang_id: number;
kandang: Kandang;
project_flock: ProjectFlock;
quantity: number;
};
export type ProjectFlockAvailableQuantity = { export type ProjectFlockAvailableQuantity = {
project_flock_id: number; project_flock_id: number;
flock_name: string; flock_name: string;
+134 -48
View File
@@ -1,61 +1,147 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { Location } from '@/types/api/master-data/location'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { Flock } from '@/types/api/master-data/flock'; export type ProductionMetrics = {
total_depletion_qty: number;
cum_depletion_rate: number;
daily_gain: number;
avg_daily_gain: number;
cum_intake: number;
fcr_value: number;
total_chick_qty: number;
daily_depletion_rate?: number;
cum_depletion?: number;
};
export type BaseRecording = { export type BaseRecording = {
id: number; id: number;
flock: Flock; project_flock_kandang_id: number;
recording_date: string; record_datetime: string;
location: Location; day: number;
coop: Kandang; created_by: User;
feed_data: { } & ProductionMetrics;
feed_name: string;
feed_qty: number; export type RecordingBW = {
feed_stock: number; id: number;
}[]; recording_id: number;
body_weight: { avg_weight: number;
chicken_weight: number; qty: number;
chicken_count: number; total_weight: number;
average_chicken_weight: number; };
}[];
vaccination: { export type RecordingDepletion = {
vaccine_name: string; id: number;
total_stock: number; recording_id: number;
used_stock: number; product_warehouse_id: number;
}[]; qty: number;
mortality: { product_warehouse: ProductWarehouse;
condition: string; };
count: number;
export type RecordingStock = {
id: number;
recording_id: number;
product_warehouse_id: number;
usage_amount?: number;
usage_qty: number;
qty: number;
pending_qty: number;
product_warehouse: ProductWarehouse;
};
export type RecordingEgg = {
id: number;
recording_id: number;
product_warehouse_id: number;
qty: number;
created_by: User;
product_warehouse: ProductWarehouse;
gradings?: {
grade: string;
qty: number;
}[]; }[];
}; };
export type Recording = BaseMetadata & BaseRecording; export type GradingEgg = {
id: number;
recording_egg_id: number;
qty: number;
grade: string;
created_by: User;
};
export type CreateRecordingPayload = { export type Recording = BaseMetadata &
flock_id: number; BaseRecording & {
recording_date: string; project_flock_category?: 'GROWING' | 'LAYING';
location_id: number; approval?: BaseApproval;
coop_id: number; egg_grading_status?: string | null;
feed_data: { egg_grading_pending_qty?: number | null;
feed_id: string; egg_grading_completed_qty?: number | null;
feed_qty: number; body_weights?: RecordingBW[];
feed_stock: number; depletions?: RecordingDepletion[];
stocks?: RecordingStock[];
eggs?: RecordingEgg[];
recording_bws?: RecordingBW[];
recording_depletions?: RecordingDepletion[];
recording_stocks?: RecordingStock[];
recording_eggs?: RecordingEgg[];
grading_eggs?: GradingEgg[];
};
export type NextDayRecording = {
project_flock_kandang_id: number;
next_day: number;
};
export type CreateGrowingRecordingPayload = {
project_flock_kandang_id: number;
body_weights: {
avg_weight: number;
qty: number;
}[]; }[];
body_weight: { stocks?: {
chicken_weight: number; product_warehouse_id: number;
chicken_count: number; qty: number;
average_chicken_weight: number;
}[]; }[];
vaccination: { depletions?: {
vaccine_id: string; product_warehouse_id: number;
total_stock: number; qty: number;
used_stock: number;
}[];
mortality: {
condition: string;
count: number;
}[]; }[];
}; };
export type CreateGradingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type UpdateGradingPayload = CreateGradingPayload;
export type CreateGradingRecordingPayload = {
eggs_grading: {
recording_egg_id: number;
grade: string;
qty: number;
}[];
};
export type CreateEggPayload = {
product_warehouse_id: number;
qty: number;
};
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
eggs?: CreateEggPayload[];
};
export type CreateRecordingPayload =
| CreateGrowingRecordingPayload
| CreateLayingRecordingPayload
| CreateGradingRecordingPayload;
export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload;
export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload;
export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload;
export type UpdateRecordingPayload = CreateRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload;