mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-76/daily-recording-growing' into 'development'
[FEAT/FE][US#76] Daily Recording Growing See merge request mbugroup/lti-web-client!31
This commit is contained in:
Generated
+26
-1
@@ -13,6 +13,7 @@
|
|||||||
"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",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"@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",
|
||||||
@@ -1640,6 +1642,13 @@
|
|||||||
"@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",
|
||||||
@@ -1675,6 +1684,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
|
||||||
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
|
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1744,6 +1754,7 @@
|
|||||||
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
|
"integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.43.0",
|
"@typescript-eslint/scope-manager": "8.43.0",
|
||||||
"@typescript-eslint/types": "8.43.0",
|
"@typescript-eslint/types": "8.43.0",
|
||||||
@@ -2261,6 +2272,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"
|
||||||
},
|
},
|
||||||
@@ -2829,7 +2841,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.1.12",
|
"version": "5.1.12",
|
||||||
@@ -3263,6 +3276,7 @@
|
|||||||
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
|
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
|
||||||
"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",
|
||||||
@@ -3437,6 +3451,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",
|
||||||
@@ -4229,6 +4244,12 @@
|
|||||||
"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",
|
||||||
@@ -5766,6 +5787,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"
|
||||||
}
|
}
|
||||||
@@ -5775,6 +5797,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"
|
||||||
},
|
},
|
||||||
@@ -6591,6 +6614,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"
|
||||||
},
|
},
|
||||||
@@ -6758,6 +6782,7 @@
|
|||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"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"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"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",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"@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 RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
|
||||||
|
const AddRecording = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<RecordingForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddRecording;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const RecordingEdit = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId,
|
||||||
|
(id: number) => RecordingApi.getSingle(id) // Gunakan RecordingApi
|
||||||
|
);
|
||||||
|
|
||||||
|
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 || isResponseError(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 && isResponseSuccess(recording) && (
|
||||||
|
<RecordingForm type='edit' initialValues={recording.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingEdit;
|
||||||
@@ -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,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
|
||||||
|
import { RecordingApi } from '@/services/api/production';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const RecordingDetail = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const recordingId = searchParams.get('recordingId');
|
||||||
|
|
||||||
|
const { data: recording, isLoading: isLoadingRecording } = useSWR(
|
||||||
|
recordingId,
|
||||||
|
(id: number) => RecordingApi.getSingle(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 || isResponseError(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 && isResponseSuccess(recording) && (
|
||||||
|
<RecordingForm type='detail' initialValues={recording.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingDetail;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import RecordingTable from '@/components/pages/production/recording/RecordingTable';
|
||||||
|
|
||||||
|
const Recording = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<RecordingTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recording;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: {
|
||||||
|
badge?: string;
|
||||||
|
};
|
||||||
|
variant?: 'default' | 'outline' | 'ghost' | 'soft' | 'dash';
|
||||||
|
color?:
|
||||||
|
| 'neutral'
|
||||||
|
| 'primary'
|
||||||
|
| 'secondary'
|
||||||
|
| 'accent'
|
||||||
|
| 'info'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'error';
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Badge = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
color,
|
||||||
|
size = 'md',
|
||||||
|
...props
|
||||||
|
}: BadgeProps) => {
|
||||||
|
const getBadgeClasses = () => {
|
||||||
|
const baseClasses = 'badge';
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
default: '',
|
||||||
|
outline: 'badge-outline',
|
||||||
|
ghost: 'badge-ghost',
|
||||||
|
soft: 'badge-soft',
|
||||||
|
dash: 'badge-dash',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorClasses = {
|
||||||
|
neutral: 'badge-neutral',
|
||||||
|
primary: 'badge-primary',
|
||||||
|
secondary: 'badge-secondary',
|
||||||
|
accent: 'badge-accent',
|
||||||
|
info: 'badge-info',
|
||||||
|
success: 'badge-success',
|
||||||
|
warning: 'badge-warning',
|
||||||
|
error: 'badge-error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
xs: 'badge-xs',
|
||||||
|
sm: 'badge-sm',
|
||||||
|
md: 'badge-md',
|
||||||
|
lg: 'badge-lg',
|
||||||
|
xl: 'badge-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
color && colorClasses[color],
|
||||||
|
sizeClasses[size],
|
||||||
|
className?.badge
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={getBadgeClasses()} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
actions?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
image?: string;
|
||||||
|
body?: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
actions?: string;
|
||||||
|
footer?: string;
|
||||||
|
};
|
||||||
|
variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
image,
|
||||||
|
imageAlt,
|
||||||
|
actions,
|
||||||
|
footer,
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'md',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CardProps) => {
|
||||||
|
const getCardClasses = () => {
|
||||||
|
const baseClasses = 'card bg-base-100';
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
'default': '',
|
||||||
|
'compact': 'card-compact',
|
||||||
|
'bordered': 'border border-base-300',
|
||||||
|
'shadow': 'shadow-xl',
|
||||||
|
'image-full': 'card-side card-compact shadow-xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
'sm': 'w-64',
|
||||||
|
'md': 'w-96',
|
||||||
|
'lg': 'w-[28rem]',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
baseClasses,
|
||||||
|
variantClasses[variant],
|
||||||
|
variant !== 'image-full' ? sizeClasses[size] : '',
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageClasses = () => {
|
||||||
|
if (variant === 'image-full') {
|
||||||
|
return cn('w-32 h-32 object-cover', className?.image);
|
||||||
|
}
|
||||||
|
return cn('h-48 object-cover', className?.image);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBodyClasses = () => {
|
||||||
|
const baseClasses = 'card-body';
|
||||||
|
|
||||||
|
if (variant === 'compact' || variant === 'image-full') {
|
||||||
|
return cn(baseClasses, 'p-4', className?.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cn(baseClasses, 'p-6', className?.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitleClasses = () => {
|
||||||
|
const sizeClasses = {
|
||||||
|
'sm': 'text-lg',
|
||||||
|
'md': 'text-xl',
|
||||||
|
'lg': 'text-2xl',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn('card-title font-bold', sizeClasses[size], className?.title);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubtitleClasses = () => {
|
||||||
|
return cn('text-base-content/70 text-sm mt-1', className?.subtitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionsClasses = () => {
|
||||||
|
return cn('card-actions justify-end mt-4', className?.actions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFooterClasses = () => {
|
||||||
|
return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (variant === 'image-full' && image) {
|
||||||
|
return (
|
||||||
|
<div className={getCardClasses()} {...props}>
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt || title || 'Card image'}
|
||||||
|
className={getImageClasses()}
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<div className={getBodyClasses()}>
|
||||||
|
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||||
|
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||||
|
{children}
|
||||||
|
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||||
|
</div>
|
||||||
|
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={getCardClasses()} {...props}>
|
||||||
|
{image && (
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt || title || 'Card image'}
|
||||||
|
className={getImageClasses()}
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
)}
|
||||||
|
<div className={getBodyClasses()}>
|
||||||
|
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||||
|
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||||
|
{children}
|
||||||
|
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||||
|
</div>
|
||||||
|
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
||||||
@@ -68,7 +68,7 @@ export const Collapse = ({
|
|||||||
'collapse',
|
'collapse',
|
||||||
variant === 'arrow' && 'collapse-arrow',
|
variant === 'arrow' && 'collapse-arrow',
|
||||||
variant === 'plus' && 'collapse-plus',
|
variant === 'plus' && 'collapse-plus',
|
||||||
bordered && 'border base-content/20 border-opacity-20 rounded-box',
|
bordered && 'border base-content/20 border-opacity-20 rounded',
|
||||||
disabled && 'opacity-60 pointer-events-none',
|
disabled && 'opacity-60 pointer-events-none',
|
||||||
!open && 'w-fit',
|
!open && 'w-fit',
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { HTMLProps, useEffect, useRef } from 'react';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface CheckboxInputProps extends HTMLProps<HTMLInputElement> {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
indeterminate?: boolean;
|
||||||
|
classNames?: {
|
||||||
|
wrapper?: string;
|
||||||
|
inputWrapper?: string;
|
||||||
|
checkbox?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
isError?: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckboxInput = ({
|
||||||
|
indeterminate,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
isValid,
|
||||||
|
isError,
|
||||||
|
errorMessage,
|
||||||
|
...rest
|
||||||
|
}: CheckboxInputProps) => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null!);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof indeterminate === 'boolean') {
|
||||||
|
ref.current.indeterminate = !rest.checked && indeterminate;
|
||||||
|
}
|
||||||
|
}, [ref, indeterminate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-col items-center gap-1', classNames?.wrapper)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row justify-center items-center gap-2',
|
||||||
|
classNames?.inputWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
ref={ref}
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
className={cn(
|
||||||
|
'checkbox cursor-pointer',
|
||||||
|
{
|
||||||
|
'border-error': isError,
|
||||||
|
'border-success': isValid,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
classNames?.checkbox
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className={cn(
|
||||||
|
'text-inherit',
|
||||||
|
{
|
||||||
|
'text-error': isError,
|
||||||
|
'text-success': isValid,
|
||||||
|
},
|
||||||
|
classNames?.label
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMessage && <span className='text-error'>{errorMessage}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckboxInput;
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
ChangeEventHandler,
|
||||||
|
FocusEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import Inputmask from 'inputmask';
|
||||||
|
|
||||||
|
const createInputMask = (
|
||||||
|
maskType: MaskType,
|
||||||
|
decimals: number,
|
||||||
|
thousandSeparator: string,
|
||||||
|
decimalSeparator: string,
|
||||||
|
allowNegative: boolean,
|
||||||
|
oncomplete?: () => void,
|
||||||
|
onincomplete?: () => void,
|
||||||
|
oncleared?: () => void
|
||||||
|
): Inputmask.Instance => {
|
||||||
|
const options: Inputmask.Options = {
|
||||||
|
alias: 'numeric',
|
||||||
|
groupSeparator: thousandSeparator,
|
||||||
|
radixPoint: decimalSeparator,
|
||||||
|
digits: decimals,
|
||||||
|
allowMinus: allowNegative,
|
||||||
|
rightAlign: false,
|
||||||
|
insertMode: true,
|
||||||
|
autoUnmask: false,
|
||||||
|
clearMaskOnLostFocus: false,
|
||||||
|
digitsOptional: decimals > 0,
|
||||||
|
placeholder: '0',
|
||||||
|
numericInput: false,
|
||||||
|
positionCaretOnClick: 'radixFocus',
|
||||||
|
greedy: true,
|
||||||
|
oncomplete,
|
||||||
|
onincomplete,
|
||||||
|
oncleared
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Inputmask(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MaskType = 'currency' | 'weight' | 'decimal' | 'number' | 'text';
|
||||||
|
|
||||||
|
export interface NumberInputProps {
|
||||||
|
label?: string;
|
||||||
|
bottomLabel?: string;
|
||||||
|
name: string;
|
||||||
|
value?: number | string;
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
className?: {
|
||||||
|
wrapper?: string;
|
||||||
|
label?: string;
|
||||||
|
inputWrapper?: string;
|
||||||
|
input?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
isError?: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
|
||||||
|
startAdornment?: ReactNode;
|
||||||
|
endAdornment?: ReactNode;
|
||||||
|
|
||||||
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
|
||||||
|
maskType?: MaskType;
|
||||||
|
decimals?: number;
|
||||||
|
thousandSeparator?: string;
|
||||||
|
decimalSeparator?: string;
|
||||||
|
currencyPrefix?: string;
|
||||||
|
weightUnit?: string;
|
||||||
|
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
allowNegative?: boolean;
|
||||||
|
|
||||||
|
oncomplete?: () => void;
|
||||||
|
onincomplete?: () => void;
|
||||||
|
oncleared?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberInput = ({
|
||||||
|
label,
|
||||||
|
bottomLabel,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
isError,
|
||||||
|
isValid,
|
||||||
|
errorMessage,
|
||||||
|
startAdornment,
|
||||||
|
endAdornment,
|
||||||
|
disabled = false,
|
||||||
|
required = false,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
readOnly = false,
|
||||||
|
isLoading = false,
|
||||||
|
maskType = 'number',
|
||||||
|
decimals = 0,
|
||||||
|
thousandSeparator = ',',
|
||||||
|
decimalSeparator = '.',
|
||||||
|
currencyPrefix = 'Rp ',
|
||||||
|
weightUnit = 'kg',
|
||||||
|
allowNegative = false,
|
||||||
|
oncomplete,
|
||||||
|
onincomplete,
|
||||||
|
oncleared,
|
||||||
|
}: NumberInputProps) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const inputmaskRef = useRef<Inputmask.Instance | null>(null);
|
||||||
|
const [maskComplete, setMaskComplete] = useState<boolean>(false);
|
||||||
|
const [maskIncomplete, setMaskIncomplete] = useState<boolean>(false);
|
||||||
|
const [maskCleared, setMaskCleared] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const getInputPrefix = (): string => {
|
||||||
|
switch (maskType) {
|
||||||
|
case 'currency':
|
||||||
|
return currencyPrefix;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInputSuffix = (): string => {
|
||||||
|
switch (maskType) {
|
||||||
|
case 'weight':
|
||||||
|
return weightUnit;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && !readOnly && !disabled) {
|
||||||
|
if (inputmaskRef.current) {
|
||||||
|
try {
|
||||||
|
inputmaskRef.current.remove();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error removing Inputmask:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
setMaskComplete(true);
|
||||||
|
setMaskIncomplete(false);
|
||||||
|
setMaskCleared(false);
|
||||||
|
if (oncomplete) oncomplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIncomplete = () => {
|
||||||
|
setMaskIncomplete(true);
|
||||||
|
setMaskComplete(false);
|
||||||
|
setMaskCleared(false);
|
||||||
|
if (onincomplete) onincomplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCleared = () => {
|
||||||
|
setMaskCleared(true);
|
||||||
|
setMaskComplete(false);
|
||||||
|
setMaskIncomplete(false);
|
||||||
|
if (oncleared) oncleared();
|
||||||
|
};
|
||||||
|
|
||||||
|
const im = createInputMask(
|
||||||
|
maskType,
|
||||||
|
decimals,
|
||||||
|
',',
|
||||||
|
'.',
|
||||||
|
allowNegative,
|
||||||
|
handleComplete,
|
||||||
|
handleIncomplete,
|
||||||
|
handleCleared
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
im.mask(inputRef.current);
|
||||||
|
inputmaskRef.current = im;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error applying Inputmask:', error);
|
||||||
|
inputmaskRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (inputmaskRef.current) {
|
||||||
|
try {
|
||||||
|
inputmaskRef.current.remove();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error removing Inputmask on cleanup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [maskType, decimals, thousandSeparator, decimalSeparator, allowNegative, readOnly, disabled, oncomplete, onincomplete, oncleared]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && value !== undefined) {
|
||||||
|
if (value === null || value === '') {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
} else {
|
||||||
|
inputRef.current.value = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const currentValue = (e.currentTarget as HTMLInputElement).value;
|
||||||
|
console.log('✅ After format:', currentValue);
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: {
|
||||||
|
name,
|
||||||
|
value: currentValue,
|
||||||
|
},
|
||||||
|
} as ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputPrefix = getInputPrefix();
|
||||||
|
const inputSuffix = getInputSuffix();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full flex flex-col gap-2 text-start',
|
||||||
|
className?.wrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className={cn(
|
||||||
|
'w-full text-sm font-normal leading-5',
|
||||||
|
{
|
||||||
|
'text-error': isError,
|
||||||
|
},
|
||||||
|
className?.label
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='relative flex'>
|
||||||
|
{inputPrefix && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center px-4 py-2 border border-r-0 rounded-l-md transition-all duration-200',
|
||||||
|
{
|
||||||
|
'bg-gray-100 border-gray-300': !disabled,
|
||||||
|
'bg-gray-50 border-gray-200': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium select-none whitespace-nowrap',
|
||||||
|
{
|
||||||
|
'text-gray-600': !disabled,
|
||||||
|
'text-gray-400': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputPrefix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'input h-12 text-base font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white',
|
||||||
|
{
|
||||||
|
'border-error': isError,
|
||||||
|
'border-success!': isValid,
|
||||||
|
'rounded-l-none!': inputPrefix,
|
||||||
|
'rounded-r-none!': inputSuffix,
|
||||||
|
'input-disabled': disabled,
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'bg-gray-50': disabled,
|
||||||
|
},
|
||||||
|
className?.inputWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id={name}
|
||||||
|
name={name}
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={placeholder || '0'}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'grow bg-transparent outline-none',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'text-gray-500': disabled,
|
||||||
|
},
|
||||||
|
className?.input
|
||||||
|
)}
|
||||||
|
readOnly={readOnly}
|
||||||
|
inputMode='text'
|
||||||
|
autoComplete='off'
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(isLoading || endAdornment) && (
|
||||||
|
<div className='flex flex-row gap-2'>
|
||||||
|
{isLoading && <span className='loading loading-spinner' />}
|
||||||
|
{endAdornment && endAdornment}
|
||||||
|
</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,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium select-none whitespace-nowrap',
|
||||||
|
{
|
||||||
|
'text-gray-600': !disabled,
|
||||||
|
'text-gray-400': disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{inputSuffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(maskType === 'text' || (oncomplete || onincomplete || oncleared)) && (
|
||||||
|
<div className='flex gap-2 text-xs'>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskComplete
|
||||||
|
? 'bg-green-100 text-green-700 border border-green-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Complete
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskIncomplete
|
||||||
|
? 'bg-yellow-100 text-yellow-700 border border-yellow-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Incomplete
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 rounded transition-all duration-200',
|
||||||
|
maskCleared
|
||||||
|
? 'bg-blue-100 text-blue-700 border border-blue-200'
|
||||||
|
: 'bg-gray-50 text-gray-400 border border-gray-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Cleared
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isError && bottomLabel && (
|
||||||
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
|
)}
|
||||||
|
{isError && errorMessage && (
|
||||||
|
<p className='w-full text-sm text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NumberInput;
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
const SelectComponent = createables ? CreatableSelect : Select;
|
const SelectComponent = createables ? CreatableSelect : Select;
|
||||||
|
|
||||||
/** 🎯 handleChange tanpa any */
|
/** 🎯 handleChange tanpa any */
|
||||||
const handleChange = (val: MultiValue<T> | SingleValue<T> | null): void => {
|
const handleChange = (val: MultiValue<T> | SingleValue<T>): void => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
onChange?.(null);
|
onChange?.(null);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {
|
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
|
||||||
ChangeEventHandler,
|
|
||||||
FocusEventHandler,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
@@ -52,7 +48,7 @@ const TextArea = ({
|
|||||||
onBlur,
|
onBlur,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows = 3
|
rows = 3,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -83,35 +79,35 @@ const TextArea = ({
|
|||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{startAdornment && startAdornment}
|
{startAdornment && startAdornment}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all',
|
'input h-auto px-4 py-2 text-base font-normal leading-6 w-full rounded-lg! outline-none! transition-all bg-white',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
|
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 && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const TextInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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',
|
'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-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
|
|||||||
@@ -61,32 +61,24 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||||
.min(1, 'Biaya minimal 1!')
|
.min(1, 'Biaya minimal 1!')
|
||||||
.typeError('Biaya harus berupa angka!')
|
.typeError('Biaya harus berupa angka!')
|
||||||
.test(
|
.test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
|
||||||
'one-of-cost-fields',
|
const { delivery_cost_per_item } = this.parent;
|
||||||
'Biaya pengiriman atau biaya per item wajib diisi!',
|
return (
|
||||||
function (value) {
|
(value !== undefined && value > 0) ||
|
||||||
const { delivery_cost_per_item } = this.parent;
|
(delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
|
||||||
return (
|
);
|
||||||
(value !== undefined && value > 0) ||
|
}),
|
||||||
(delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
delivery_cost_per_item: Yup.number()
|
delivery_cost_per_item: Yup.number()
|
||||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||||
.min(1, 'Biaya per item minimal 1!')
|
.min(1, 'Biaya per item minimal 1!')
|
||||||
.typeError('Biaya per item harus berupa angka!')
|
.typeError('Biaya per item harus berupa angka!')
|
||||||
.test(
|
.test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
|
||||||
'one-of-cost-fields',
|
const { delivery_cost } = this.parent;
|
||||||
'Biaya pengiriman atau biaya per item wajib diisi!',
|
return (
|
||||||
function (value) {
|
(value !== undefined && value > 0) ||
|
||||||
const { delivery_cost } = this.parent;
|
(delivery_cost !== undefined && delivery_cost > 0)
|
||||||
return (
|
);
|
||||||
(value !== undefined && value > 0) ||
|
}),
|
||||||
(delivery_cost !== undefined && delivery_cost > 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
document_path: Yup.string().optional(),
|
document_path: Yup.string().optional(),
|
||||||
document_index: Yup.number().optional(),
|
document_index: Yup.number().optional(),
|
||||||
document: Yup.mixed<File | string>()
|
document: Yup.mixed<File | string>()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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 NumberInput from '@/components/input/NumberInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
import { FormActions } from '@/components/helper/form/FormActions';
|
import { FormActions } from '@/components/helper/form/FormActions';
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +29,7 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
|
|||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import FileInput from '@/components/input/FileInput';
|
import FileInput from '@/components/input/FileInput';
|
||||||
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
|
|
||||||
interface MovementFormProps {
|
interface MovementFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -217,7 +218,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
isError: false,
|
isError: false,
|
||||||
errorMessage: undefined,
|
errorMessage: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +230,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isError: touchedField && Boolean(errorField?.[column as string]),
|
isError: touchedField && Boolean(errorField?.[column as string]),
|
||||||
errorMessage: touchedField ? errorField?.[column as string] : undefined,
|
errorMessage:
|
||||||
|
touchedField && errorField?.[column as string]
|
||||||
|
? errorField[column as string]
|
||||||
|
: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,7 +250,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!touchedDelivery?.products || !errorDelivery?.products) {
|
if (!touchedDelivery?.products || !errorDelivery?.products) {
|
||||||
return {
|
return {
|
||||||
isError: false,
|
isError: false,
|
||||||
errorMessage: undefined,
|
errorMessage: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +259,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isError: Boolean(touchedField && errorField),
|
isError: Boolean(touchedField && errorField),
|
||||||
errorMessage: touchedField ? errorField : undefined,
|
errorMessage: touchedField && errorField ? errorField : '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -706,7 +710,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
value={formik.values.source_warehouse}
|
value={formik.values.source_warehouse}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
formik.setFieldValue('source_warehouse', val);
|
formik.setFieldValue('source_warehouse', val);
|
||||||
|
formik.setFieldTouched('source_warehouse_id', true);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'source_warehouse_id',
|
'source_warehouse_id',
|
||||||
(val as WarehouseOptionType)?.value
|
(val as WarehouseOptionType)?.value
|
||||||
@@ -764,7 +770,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
value={formik.values.destination_warehouse}
|
value={formik.values.destination_warehouse}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
formik.setFieldValue('destination_warehouse', val);
|
formik.setFieldValue('destination_warehouse', val);
|
||||||
|
formik.setFieldTouched('destination_warehouse_id', true);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'destination_warehouse_id',
|
'destination_warehouse_id',
|
||||||
(val as WarehouseOptionType)?.value
|
(val as WarehouseOptionType)?.value
|
||||||
@@ -831,30 +839,53 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<tr>
|
<tr>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<th>
|
<th>
|
||||||
<input
|
<div className='flex justify-center'>
|
||||||
type='checkbox'
|
<CheckboxInput
|
||||||
className='checkbox'
|
name='select-all-products'
|
||||||
checked={
|
checked={
|
||||||
formik.values.products?.length ===
|
formik.values.products?.length ===
|
||||||
selectedProducts.length &&
|
selectedProducts.length &&
|
||||||
formik.values.products?.length > 0
|
formik.values.products?.length > 0
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts(
|
|
||||||
formik.values.products?.map(
|
|
||||||
(_, idx) => idx
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts([]);
|
|
||||||
}
|
}
|
||||||
}}
|
onChange={(
|
||||||
/>
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts(
|
||||||
|
formik.values.products?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>Produk</th>
|
<th>
|
||||||
<th>Qty</th>
|
Produk
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Qty
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
{type !== 'detail' && <th>Aksi</th>}
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -863,23 +894,30 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<tr key={`product-row-${idx}-${product.product_id}`}>
|
<tr key={`product-row-${idx}-${product.product_id}`}>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<input
|
<div className='flex justify-center'>
|
||||||
type='checkbox'
|
<CheckboxInput
|
||||||
className='checkbox'
|
name={`product-${idx}`}
|
||||||
checked={selectedProducts.includes(idx)}
|
checked={selectedProducts.includes(idx)}
|
||||||
onChange={(e) => {
|
onChange={(
|
||||||
if (e.target.checked) {
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
setSelectedProducts([
|
) => {
|
||||||
...selectedProducts,
|
if (e.target.checked) {
|
||||||
idx,
|
setSelectedProducts([
|
||||||
]);
|
...selectedProducts,
|
||||||
} else {
|
idx,
|
||||||
setSelectedProducts(
|
]);
|
||||||
selectedProducts.filter((i) => i !== idx)
|
} else {
|
||||||
);
|
setSelectedProducts(
|
||||||
}
|
selectedProducts.filter((i) => i !== idx)
|
||||||
}}
|
);
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
@@ -887,10 +925,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
value={product.product ?? undefined}
|
value={product.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`products.${idx}.product`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`products.${idx}.product`,
|
`products.${idx}.product`,
|
||||||
val
|
val
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`products.${idx}.product_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`products.${idx}.product_id`,
|
`products.${idx}.product_id`,
|
||||||
(val as ProductWarehouseOptionType)?.value
|
(val as ProductWarehouseOptionType)?.value
|
||||||
@@ -911,7 +957,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
isClearable
|
isClearable
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'products',
|
'products',
|
||||||
'product',
|
'product_id',
|
||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
className={{
|
className={{
|
||||||
@@ -954,17 +1000,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<Button
|
<div className='flex flex-col items-start gap-2'>
|
||||||
type='button'
|
<Button
|
||||||
color='error'
|
type='button'
|
||||||
onClick={() => removeProduct(idx)}
|
color='error'
|
||||||
>
|
onClick={() => removeProduct(idx)}
|
||||||
<Icon
|
>
|
||||||
icon='material-symbols:delete-outline-rounded'
|
<Icon
|
||||||
width={24}
|
icon='material-symbols:delete-outline-rounded'
|
||||||
height={24}
|
width={24}
|
||||||
/>
|
height={24}
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1006,43 +1054,106 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
{/* Deliveries table */}
|
{/* Deliveries table */}
|
||||||
<div className='card bg-base-100 shadow mb-4'>
|
<div className='card bg-base-100 shadow mb-4'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<h2 className='card-title mb-4'>Pengiriman</h2>
|
<h2 className='card-title mb-8'>Pengiriman</h2>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
<table className='table'>
|
<table className='table'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<th>
|
<th>
|
||||||
<input
|
<div className='flex justify-center'>
|
||||||
type='checkbox'
|
<CheckboxInput
|
||||||
className='checkbox'
|
name='select-all-deliveries'
|
||||||
checked={
|
checked={
|
||||||
formik.values.deliveries?.length ===
|
formik.values.deliveries?.length ===
|
||||||
selectedDeliveries.length &&
|
selectedDeliveries.length &&
|
||||||
formik.values.deliveries?.length > 0
|
formik.values.deliveries?.length > 0
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
formik.values.deliveries?.map(
|
|
||||||
(_, idx) => idx
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries([]);
|
|
||||||
}
|
}
|
||||||
}}
|
onChange={(
|
||||||
/>
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedDeliveries(
|
||||||
|
formik.values.deliveries?.map(
|
||||||
|
(_, idx) => idx
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedDeliveries([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>Produk</th>
|
<th>
|
||||||
<th>Qty</th>
|
Produk
|
||||||
<th>Supplier</th>
|
<span
|
||||||
<th>Plat Nomor</th>
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Qty
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Supplier
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Plat Nomor
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
<th>Dokumen</th>
|
<th>Dokumen</th>
|
||||||
<th>Biaya Pengiriman (Rp.)</th>
|
<th>
|
||||||
<th>Biaya Per Item (Rp.)</th>
|
Biaya Pengiriman (Rp.)
|
||||||
<th>Nama Sopir</th>
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Biaya Per Item (Rp.)
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Nama Sopir
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-[9999]'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
{type !== 'detail' && <th>Aksi</th>}
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1051,23 +1162,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<tr key={`delivery-row-${idx}`}>
|
<tr key={`delivery-row-${idx}`}>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<input
|
<div className='flex justify-center'>
|
||||||
type='checkbox'
|
<CheckboxInput
|
||||||
className='checkbox'
|
name={`delivery-${idx}`}
|
||||||
checked={selectedDeliveries.includes(idx)}
|
checked={selectedDeliveries.includes(idx)}
|
||||||
onChange={(e) => {
|
onChange={(
|
||||||
if (e.target.checked) {
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
setSelectedDeliveries([
|
) => {
|
||||||
...selectedDeliveries,
|
if (e.target.checked) {
|
||||||
idx,
|
setSelectedDeliveries([
|
||||||
]);
|
...selectedDeliveries,
|
||||||
} else {
|
idx,
|
||||||
setSelectedDeliveries(
|
]);
|
||||||
selectedDeliveries.filter((i) => i !== idx)
|
} else {
|
||||||
);
|
setSelectedDeliveries(
|
||||||
}
|
selectedDeliveries.filter(
|
||||||
}}
|
(i) => i !== idx
|
||||||
/>
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'flex justify-center',
|
||||||
|
checkbox: 'checkbox checkbox-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
@@ -1075,10 +1195,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
value={delivery.products[0]?.product ?? undefined}
|
value={delivery.products[0]?.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${idx}.products.0.product`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.products.0.product`,
|
`deliveries.${idx}.products.0.product`,
|
||||||
val
|
val
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${idx}.products.0.product_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
`deliveries.${idx}.products.0.product_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
@@ -1087,6 +1215,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
options={getFilteredProductWarehouseOptions()}
|
options={getFilteredProductWarehouseOptions()}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
|
isError={
|
||||||
|
isDeliveryProductInputError(idx, 0, 'product_id')
|
||||||
|
.isError
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
isDeliveryProductInputError(idx, 0, 'product_id')
|
||||||
|
.errorMessage
|
||||||
|
}
|
||||||
className={{
|
className={{
|
||||||
wrapper:
|
wrapper:
|
||||||
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
@@ -1122,10 +1258,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
value={delivery.supplier}
|
value={delivery.supplier}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${idx}.supplier`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.supplier`,
|
`deliveries.${idx}.supplier`,
|
||||||
val
|
val
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${idx}.supplier_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.supplier_id`,
|
`deliveries.${idx}.supplier_id`,
|
||||||
(val as OptionType)?.value
|
(val as OptionType)?.value
|
||||||
@@ -1136,6 +1280,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
|
{...isRepeaterInputError(
|
||||||
|
'deliveries',
|
||||||
|
'supplier_id',
|
||||||
|
idx
|
||||||
|
)}
|
||||||
className={{
|
className={{
|
||||||
wrapper:
|
wrapper:
|
||||||
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
@@ -1163,27 +1312,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{type === 'detail' ? (
|
{type === 'detail' ? (
|
||||||
<Button
|
<>
|
||||||
color='primary'
|
<div className='flex flex-col items-start gap-2'>
|
||||||
className='w-full min-w-52 flex items-center justify-center gap-2'
|
<Button
|
||||||
disabled={!delivery.document_path}
|
color='primary'
|
||||||
href={delivery.document_path ?? undefined}
|
className='w-full min-w-52 flex items-center justify-center gap-2'
|
||||||
target='_blank'
|
disabled={!delivery.document_path}
|
||||||
rel='noopener noreferrer'
|
href={delivery.document_path ?? undefined}
|
||||||
>
|
target='_blank'
|
||||||
{delivery.document_path ? (
|
rel='noopener noreferrer'
|
||||||
<>
|
>
|
||||||
<Icon
|
{delivery.document_path ? (
|
||||||
icon='material-symbols:file-open-outline'
|
<>
|
||||||
width={20}
|
<Icon
|
||||||
height={20}
|
icon='material-symbols:file-open-outline'
|
||||||
/>
|
width={20}
|
||||||
Lihat Dokumen
|
height={20}
|
||||||
</>
|
/>
|
||||||
) : (
|
Lihat Dokumen
|
||||||
'-'
|
</>
|
||||||
)}
|
) : (
|
||||||
</Button>
|
'-'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<FileInput
|
<FileInput
|
||||||
name={`deliveries.${idx}.document`}
|
name={`deliveries.${idx}.document`}
|
||||||
@@ -1194,6 +1347,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
toast.error(
|
toast.error(
|
||||||
'Ukuran dokumen maksimal 2 MB!'
|
'Ukuran dokumen maksimal 2 MB!'
|
||||||
);
|
);
|
||||||
|
e.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -1215,15 +1369,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`deliveries.${idx}.delivery_cost`}
|
name={`deliveries.${idx}.delivery_cost`}
|
||||||
value={delivery.delivery_cost || ''}
|
value={delivery.delivery_cost || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleDeliveryCostChange(idx, e.target.value)
|
handleDeliveryCostChange(idx, e.target.value)
|
||||||
}
|
}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='currency'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'deliveries',
|
'deliveries',
|
||||||
'delivery_cost',
|
'delivery_cost',
|
||||||
@@ -1231,14 +1387,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-48',
|
wrapper:
|
||||||
|
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`deliveries.${idx}.delivery_cost_per_item`}
|
name={`deliveries.${idx}.delivery_cost_per_item`}
|
||||||
value={delivery.delivery_cost_per_item || ''}
|
value={delivery.delivery_cost_per_item || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -1248,6 +1404,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
maskType='currency'
|
||||||
|
decimals={0}
|
||||||
|
min={0}
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'deliveries',
|
'deliveries',
|
||||||
'delivery_cost_per_item',
|
'delivery_cost_per_item',
|
||||||
@@ -1255,7 +1414,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-48',
|
wrapper:
|
||||||
|
'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -1280,17 +1440,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td>
|
||||||
<Button
|
<div className='flex flex-col items-start gap-2'>
|
||||||
type='button'
|
<Button
|
||||||
color='error'
|
type='button'
|
||||||
onClick={() => removeDelivery(idx)}
|
color='error'
|
||||||
>
|
onClick={() => removeDelivery(idx)}
|
||||||
<Icon
|
>
|
||||||
icon='material-symbols:delete-outline-rounded'
|
<Icon
|
||||||
width={24}
|
icon='material-symbols:delete-outline-rounded'
|
||||||
height={24}
|
width={24}
|
||||||
/>
|
height={24}
|
||||||
</Button>
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { SortingState } from '@tanstack/react-table';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
import { TableToolbar } from '@/components/table/TableToolbar';
|
||||||
|
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
|
import { type CellContext } from '@tanstack/react-table';
|
||||||
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
|
|
||||||
|
const dummyRecordings: Recording[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
flock: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Flock Recording 1',
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
created_user: {
|
||||||
|
id: 1,
|
||||||
|
id_user: 1,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
name: 'Admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recording_date: '2024-01-01',
|
||||||
|
location: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Location 1',
|
||||||
|
address: 'Jl. Contoh No. 1',
|
||||||
|
area: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Area 1',
|
||||||
|
},
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
created_user: {
|
||||||
|
id: 1,
|
||||||
|
id_user: 1,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
name: 'Admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coop: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Coop 1',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
location: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Location 1',
|
||||||
|
address: 'Jl. Contoh No. 1',
|
||||||
|
area: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Area 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pic: {
|
||||||
|
id: 1,
|
||||||
|
id_user: 1,
|
||||||
|
email: 'pic@example.com',
|
||||||
|
name: 'PIC User',
|
||||||
|
},
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
created_user: {
|
||||||
|
id: 1,
|
||||||
|
id_user: 1,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
name: 'Admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
feed_data: [
|
||||||
|
{
|
||||||
|
feed_name: 'Feed 1',
|
||||||
|
feed_qty: 100,
|
||||||
|
feed_stock: 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body_weight: [
|
||||||
|
{
|
||||||
|
chicken_weight: 2.5,
|
||||||
|
chicken_count: 1000,
|
||||||
|
average_chicken_weight: 2.5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vaccination: [
|
||||||
|
{
|
||||||
|
vaccine_name: 'Vaccine 1',
|
||||||
|
total_stock: 200,
|
||||||
|
used_stock: 150,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mortality: [
|
||||||
|
{
|
||||||
|
condition: 'NORMAL',
|
||||||
|
count: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
created_at: '2024-01-01',
|
||||||
|
updated_at: '2024-01-01',
|
||||||
|
created_user: {
|
||||||
|
id: 1,
|
||||||
|
id_user: 1,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
name: 'Admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Recording, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={type === 'dropdown' ? 0 : undefined}
|
||||||
|
className={cn(
|
||||||
|
{
|
||||||
|
'dropdown-content': type === 'dropdown',
|
||||||
|
'mt-2': type === 'collapse',
|
||||||
|
},
|
||||||
|
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
href={`recording/detail/?recordingId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
href={`recording/detail/edit/?recordingId=${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'
|
||||||
|
color='error'
|
||||||
|
className='text-error hover:text-inherit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:delete-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecordingTable = () => {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [selectedRecordings, setSelectedRecordings] = useState<number[]>([]);
|
||||||
|
const [, setSelectedRecording] = useState<Recording | undefined>(undefined);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
|
||||||
|
const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false);
|
||||||
|
|
||||||
|
const singleDeleteModal = useModal();
|
||||||
|
const bulkApproveModal = useModal();
|
||||||
|
const bulkRejectModal = useModal();
|
||||||
|
|
||||||
|
const searchChangeHandler = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageSizeChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
setPageSize(newVal.value as number);
|
||||||
|
setPage(1);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginatedData = useMemo(() => {
|
||||||
|
const filteredData = dummyRecordings.filter(
|
||||||
|
(recording) =>
|
||||||
|
recording.flock.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
recording.location.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
recording.coop.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
const start = (page - 1) * pageSize;
|
||||||
|
return filteredData.slice(start, start + pageSize);
|
||||||
|
}, [page, pageSize, search]);
|
||||||
|
|
||||||
|
const bulkApproveHandler = async () => {
|
||||||
|
setIsBulkApproveLoading(true);
|
||||||
|
console.log(
|
||||||
|
'Approved recordings:',
|
||||||
|
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsBulkApproveLoading(false);
|
||||||
|
setSelectedRecordings([]);
|
||||||
|
bulkApproveModal.closeModal();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkRejectHandler = async () => {
|
||||||
|
setIsBulkRejectLoading(true);
|
||||||
|
console.log(
|
||||||
|
'Rejected recordings:',
|
||||||
|
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsBulkRejectLoading(false);
|
||||||
|
setSelectedRecordings([]);
|
||||||
|
bulkRejectModal.closeModal();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleDeleteHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
singleDeleteModal.closeModal();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<TableToolbar
|
||||||
|
addButton={{
|
||||||
|
href: 'recording/add',
|
||||||
|
label: 'Tambah Recording',
|
||||||
|
}}
|
||||||
|
search={{
|
||||||
|
value: search,
|
||||||
|
onChange: searchChangeHandler,
|
||||||
|
placeholder: 'Cari Recording',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TableRowSizeSelector
|
||||||
|
value={pageSize}
|
||||||
|
onChange={pageSizeChangeHandler}
|
||||||
|
options={ROWS_OPTIONS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bulk action buttons */}
|
||||||
|
<div className={'flex justify-end items-center'}>
|
||||||
|
{selectedRecordings.length > 0 && (
|
||||||
|
<div className='flex gap-2 mb-4'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={() => bulkApproveModal.openModal()}
|
||||||
|
className='flex items-center gap-2'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Approve ({selectedRecordings.length})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => bulkRejectModal.openModal()}
|
||||||
|
className='flex items-center gap-2'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:cancel-outline'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Reject ({selectedRecordings.length})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={bulkApproveModal.ref}
|
||||||
|
type='success'
|
||||||
|
text={`Apakah anda yakin ingin menyetujui ${selectedRecordings.length} data Recording yang dipilih?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isBulkApproveLoading,
|
||||||
|
onClick: bulkApproveHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={bulkRejectModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menolak ${selectedRecordings.length} data Recording yang dipilih?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isBulkRejectLoading,
|
||||||
|
onClick: bulkRejectHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
data={paginatedData}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={
|
||||||
|
table.getRowModel().rows.length > 0 &&
|
||||||
|
table
|
||||||
|
.getRowModel()
|
||||||
|
.rows.every((row) => selectedRecordings.includes(row.index))
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedRecordings(
|
||||||
|
table.getRowModel().rows.map((row) => row.index)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedRecordings([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='checkbox'
|
||||||
|
checked={selectedRecordings.includes(row.index)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedRecordings([...selectedRecordings, row.index]);
|
||||||
|
} else {
|
||||||
|
setSelectedRecordings(
|
||||||
|
selectedRecordings.filter((i) => i !== row.index)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => pageSize * (page - 1) + props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'flock.name',
|
||||||
|
header: 'Flock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'recording_date',
|
||||||
|
header: 'Tanggal Recording',
|
||||||
|
cell: (props) =>
|
||||||
|
new Date(props.row.original.recording_date).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'location.name',
|
||||||
|
header: 'Lokasi',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'coop.name',
|
||||||
|
header: 'Kandang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'mortality',
|
||||||
|
header: 'Total Mortality',
|
||||||
|
cell: (props) =>
|
||||||
|
props.row.original.mortality.reduce(
|
||||||
|
(acc, curr) => acc + curr.count,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props: CellContext<Recording, unknown>) => {
|
||||||
|
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 = () => {
|
||||||
|
setSelectedRecording(props.row.original);
|
||||||
|
singleDeleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 2 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='dropdown'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 2 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='collapse'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
pageSize={pageSize}
|
||||||
|
page={page}
|
||||||
|
totalItems={dummyRecordings.length}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={false}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20': paginatedData.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={singleDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Recording ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: singleDeleteHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordingTable;
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
|
||||||
|
import { Recording } from '@/types/api/production/recording';
|
||||||
|
|
||||||
|
export const RecordingFormSchema = Yup.object({
|
||||||
|
flock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
flock_id: Yup.number()
|
||||||
|
.default(0)
|
||||||
|
.typeError('Flock wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'is-valid-flock',
|
||||||
|
'Flock wajib diisi!',
|
||||||
|
(value) => value !== undefined && value !== null && value > 0
|
||||||
|
)
|
||||||
|
.required('Flock wajib diisi!'),
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.default(0)
|
||||||
|
.typeError('Lokasi wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'is-valid-location',
|
||||||
|
'Lokasi wajib diisi!',
|
||||||
|
(value) => value !== undefined && value !== null && value > 0
|
||||||
|
)
|
||||||
|
.required('Lokasi wajib diisi!'),
|
||||||
|
coop: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
coop_id: Yup.number()
|
||||||
|
.default(0)
|
||||||
|
.typeError('Kandang wajib diisi!')
|
||||||
|
.test(
|
||||||
|
'is-valid-coop',
|
||||||
|
'Kandang wajib diisi!',
|
||||||
|
(value) => value !== undefined && value !== null && value > 0
|
||||||
|
)
|
||||||
|
.required('Kandang wajib diisi!'),
|
||||||
|
recording_date: Yup.date()
|
||||||
|
.required('Tanggal recording wajib diisi')
|
||||||
|
.typeError('Format tanggal tidak valid'),
|
||||||
|
feed_data: Yup.array()
|
||||||
|
.of(
|
||||||
|
Yup.object({
|
||||||
|
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
|
||||||
|
feed_qty: Yup.mixed<number | ''>().notRequired(),
|
||||||
|
feed_stock: Yup.number()
|
||||||
|
.required('Jumlah pakan yang digunakan wajib diisi!')
|
||||||
|
.min(1, 'Jumlah pakan minimal 1!')
|
||||||
|
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
|
||||||
|
.test(
|
||||||
|
'is-not-exceed-qty',
|
||||||
|
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!',
|
||||||
|
function (value) {
|
||||||
|
const { feed_qty } = this.parent;
|
||||||
|
if (value === undefined) return true;
|
||||||
|
if (
|
||||||
|
feed_qty === undefined ||
|
||||||
|
feed_qty === '' ||
|
||||||
|
typeof feed_qty !== 'number'
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return value <= feed_qty;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.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;
|
||||||
|
|
||||||
|
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
|
||||||
|
|
||||||
|
export const getRecordingFormInitialValues = (
|
||||||
|
initialValues?: Recording
|
||||||
|
): RecordingFormValues => ({
|
||||||
|
flock: initialValues?.flock
|
||||||
|
? {
|
||||||
|
value: initialValues.flock.id,
|
||||||
|
label: initialValues.flock.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
flock_id: initialValues?.flock?.id ?? 0,
|
||||||
|
location: initialValues?.location
|
||||||
|
? {
|
||||||
|
value: initialValues.location.id,
|
||||||
|
label: initialValues.location.name,
|
||||||
|
}
|
||||||
|
: 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,
|
||||||
|
chicken_count: 0,
|
||||||
|
average_chicken_weight: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vaccination: initialValues?.vaccination
|
||||||
|
? initialValues.vaccination.map((vaccine) => ({
|
||||||
|
vaccine_id: vaccine.vaccine_name,
|
||||||
|
total_stock: vaccine.total_stock,
|
||||||
|
used_stock: vaccine.used_stock,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
vaccine_id: '',
|
||||||
|
total_stock: '',
|
||||||
|
used_stock: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mortality: initialValues?.mortality ?? [
|
||||||
|
{
|
||||||
|
condition: '',
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -126,13 +126,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
|
|||||||
{
|
{
|
||||||
title: 'Flock',
|
title: 'Flock',
|
||||||
link: '/master-data/flock',
|
link: '/master-data/flock',
|
||||||
icon: 'material-symbols:raven-outline-rounded'
|
icon: 'material-symbols:raven-outline-rounded',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
||||||
export const ROWS_OPTIONS = [
|
export const ROWS_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: '10',
|
label: '10',
|
||||||
@@ -215,3 +214,9 @@ export const PRODUCT_FLAG_OPTIONS = [
|
|||||||
export const SUPPLIER_FLAG_OPTIONS = [
|
export const SUPPLIER_FLAG_OPTIONS = [
|
||||||
{ label: 'EKSPEDISI', value: 'EKSPEDISI' },
|
{ label: 'EKSPEDISI', value: 'EKSPEDISI' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const RECORDING_FLAG_OPTIONS = [
|
||||||
|
{ label: 'Ayam Afkir', value: 'Ayam Afkir' },
|
||||||
|
{ label: 'Ayam Culling', value: 'Ayam Culling' },
|
||||||
|
{ label: 'Ayam Mati', value: 'Ayam Mati' },
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
|
import { BaseApiService } from './base';
|
||||||
import {
|
import {
|
||||||
ProjectFlock,
|
|
||||||
CreateProjectFlockPayload,
|
CreateProjectFlockPayload,
|
||||||
|
ProjectFlock,
|
||||||
|
UpdateProjectFlockPayload,
|
||||||
} from '@/types/api/production/project-flock';
|
} from '@/types/api/production/project-flock';
|
||||||
|
import {
|
||||||
|
CreateRecordingPayload,
|
||||||
|
Recording,
|
||||||
|
UpdateRecordingPayload,
|
||||||
|
} from '@/types/api/production/recording';
|
||||||
import {
|
import {
|
||||||
Chickin,
|
Chickin,
|
||||||
CreateChickinPayload,
|
CreateChickinPayload,
|
||||||
UpdateChickinPayload,
|
UpdateChickinPayload,
|
||||||
} from '@/types/api/production/chickin';
|
} from '@/types/api/production/chickin';
|
||||||
import { BaseApiService } from '@/services/api/base';
|
|
||||||
|
|
||||||
export const ProjectFlockApi = new BaseApiService<
|
export const ProjectFlockApi = new BaseApiService<
|
||||||
ProjectFlock,
|
ProjectFlock,
|
||||||
CreateProjectFlockPayload,
|
CreateProjectFlockPayload,
|
||||||
unknown
|
UpdateProjectFlockPayload
|
||||||
>('/production/project_flocks');
|
>('/production/project_flocks');
|
||||||
|
export const RecordingApi = new BaseApiService<
|
||||||
|
Recording,
|
||||||
|
CreateRecordingPayload,
|
||||||
|
UpdateRecordingPayload
|
||||||
|
>('/flock/recordings');
|
||||||
export const ChickinApi = new BaseApiService<
|
export const ChickinApi = new BaseApiService<
|
||||||
Chickin,
|
Chickin,
|
||||||
CreateChickinPayload,
|
CreateChickinPayload,
|
||||||
|
|||||||
@@ -8,4 +8,8 @@
|
|||||||
--step-bg: var(--color-error);
|
--step-bg: var(--color-error);
|
||||||
--step-fg: var(--color-error-content);
|
--step-fg: var(--color-error-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table :where(th, td) {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -1,4 +1,5 @@
|
|||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { BaseMetadata } from '../base-metadata';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
export type BaseInventoryAdjustment = {
|
export type BaseInventoryAdjustment = {
|
||||||
@@ -28,3 +29,6 @@ export type CreateInventoryAdjustmentPayload = {
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
note: string;
|
note: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateInventoryAdjustmentPayload =
|
||||||
|
Partial<CreateInventoryAdjustmentPayload>;
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1,14 +1,14 @@
|
|||||||
import { BaseMetadata } from "@/types/api/api-general";
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
|
||||||
export type BaseFlock = {
|
export type BaseFlock = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Flock = BaseMetadata & BaseFlock;
|
export type Flock = BaseMetadata & BaseFlock;
|
||||||
|
|
||||||
export type CreateFlockPayload = {
|
export type CreateFlockPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UpdateFlockPayload = CreateFlockPayload;
|
export type UpdateFlockPayload = CreateFlockPayload;
|
||||||
+10
-11
@@ -1,9 +1,9 @@
|
|||||||
import { Area } from "@/types/api/master-data/area";
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { Fcr } from "@/types/api/master-data/fcr";
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { Flock } from "@/types/api/master-data/flock";
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
import { Kandang } from "@/types/api/master-data/kandang";
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { Location } from "@/types/api/master-data/location";
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { BaseMetadata } from "@/types/api/api-general";
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
|
||||||
export type BaseProjectFlock = {
|
export type BaseProjectFlock = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -21,15 +21,14 @@ export type BaseProjectFlock = {
|
|||||||
period: number;
|
period: number;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
kandangs: Kandang[];
|
kandangs: Kandang[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PeriodFlock = {
|
export type PeriodFlock = {
|
||||||
flock: Flock;
|
flock: Flock;
|
||||||
next_period: number;
|
next_period: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type ProjectFlock = BaseMetadata & BaseProjectFlock;
|
||||||
export type ProjectFlock = BaseMetadata & BaseProjectFlock
|
|
||||||
|
|
||||||
export type CreateProjectFlockPayload = {
|
export type CreateProjectFlockPayload = {
|
||||||
flock_id: number;
|
flock_id: number;
|
||||||
@@ -39,6 +38,6 @@ export type CreateProjectFlockPayload = {
|
|||||||
location_id: number;
|
location_id: number;
|
||||||
period: number;
|
period: number;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
|
export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Flock } from '@/types/api/master-data/flock';
|
||||||
|
|
||||||
|
export type BaseRecording = {
|
||||||
|
id: number;
|
||||||
|
flock: Flock;
|
||||||
|
recording_date: string;
|
||||||
|
location: Location;
|
||||||
|
coop: Kandang;
|
||||||
|
feed_data: {
|
||||||
|
feed_name: string;
|
||||||
|
feed_qty: number;
|
||||||
|
feed_stock: number;
|
||||||
|
}[];
|
||||||
|
body_weight: {
|
||||||
|
chicken_weight: number;
|
||||||
|
chicken_count: number;
|
||||||
|
average_chicken_weight: number;
|
||||||
|
}[];
|
||||||
|
vaccination: {
|
||||||
|
vaccine_name: string;
|
||||||
|
total_stock: number;
|
||||||
|
used_stock: number;
|
||||||
|
}[];
|
||||||
|
mortality: {
|
||||||
|
condition: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Recording = BaseMetadata & BaseRecording;
|
||||||
|
|
||||||
|
export type CreateRecordingPayload = {
|
||||||
|
flock_id: number;
|
||||||
|
recording_date: string;
|
||||||
|
location_id: number;
|
||||||
|
coop_id: number;
|
||||||
|
feed_data: {
|
||||||
|
feed_id: string;
|
||||||
|
feed_qty: number;
|
||||||
|
feed_stock: number;
|
||||||
|
}[];
|
||||||
|
body_weight: {
|
||||||
|
chicken_weight: number;
|
||||||
|
chicken_count: number;
|
||||||
|
average_chicken_weight: number;
|
||||||
|
}[];
|
||||||
|
vaccination: {
|
||||||
|
vaccine_id: string;
|
||||||
|
total_stock: number;
|
||||||
|
used_stock: number;
|
||||||
|
}[];
|
||||||
|
mortality: {
|
||||||
|
condition: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateRecordingPayload = CreateRecordingPayload;
|
||||||
Reference in New Issue
Block a user