diff --git a/package-lock.json b/package-lock.json
index 6b7c09e6..e1f28d3e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
+ "inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -30,6 +31,7 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
+ "@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1636,6 +1638,13 @@
"@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": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2793,9 +2802,9 @@
"license": "MIT"
},
"node_modules/daisyui": {
- "version": "5.3.9",
- "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.9.tgz",
- "integrity": "sha512-741x1pGGSGHcrBYtdE7iKbqW1OoiijYdAZ8oJPZR9MhSKLcMBlHjKfN3YlM2/K7t5jd7O0sg4SqkVNPylalRFw==",
+ "version": "5.3.10",
+ "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz",
+ "integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4193,6 +4202,12 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
diff --git a/package.json b/package.json
index 2e806ddd..b371e4e7 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"axios": "^1.12.2",
"clsx": "^2.1.1",
"formik": "^2.4.6",
+ "inputmask": "^5.0.9",
"moment": "^2.30.1",
"next": "15.5.3",
"react": "19.1.0",
@@ -32,6 +33,7 @@
"@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4",
+ "@types/inputmask": "^5.0.7",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/src/app/production/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx
index af834b37..3ef73396 100644
--- a/src/app/production/chickin/add/page.tsx
+++ b/src/app/production/chickin/add/page.tsx
@@ -141,8 +141,8 @@ const AddChickin = () => {
options={options}
isLoading={isLoadingListProjectFlock}
value={{
- label: `${projectFlock.data.flock.name} - ${projectFlock.data.category} - Periode ${projectFlock.data.period}`,
- value: projectFlock.data.id,
+ label: `${projectFlock.data?.flock?.name} - ${projectFlock.data?.category} - Periode ${projectFlock.data?.period}`,
+ value: projectFlock.data?.id,
}}
onChange={(val) =>
router.push(
@@ -159,7 +159,7 @@ const AddChickin = () => {
- data={projectFlock.data.kandangs}
+ data={projectFlock.data?.kandangs}
columns={[
{
header: '#',
@@ -202,7 +202,7 @@ const AddChickin = () => {
containerClassName: cn({
'mb-20':
isResponseSuccess(projectFlock) &&
- projectFlock.data.kandangs?.length === 0,
+ projectFlock.data?.kandangs?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
@@ -239,10 +239,10 @@ const AddChickin = () => {
diff --git a/src/app/production/chickin/detail/page.tsx b/src/app/production/chickin/detail/page.tsx
index aef7c4b7..96647c55 100644
--- a/src/app/production/chickin/detail/page.tsx
+++ b/src/app/production/chickin/detail/page.tsx
@@ -18,6 +18,11 @@ import { useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
+/**
+ * TODO: Refactor code - pindahin detail ke reuseable component
+ * setelah implement approval and reject
+ */
+
const DetailChickin = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -112,6 +117,7 @@ const DetailChickin = () => {
if (isResponseError(deleteProjectFlockRes)) {
toast.error(deleteProjectFlockRes?.message as string);
}
+ deleteModal.closeModal();
setIsDeleteLoading(false);
};
diff --git a/src/app/production/recording/add/page.tsx b/src/app/production/recording/add/page.tsx
new file mode 100644
index 00000000..d41fc183
--- /dev/null
+++ b/src/app/production/recording/add/page.tsx
@@ -0,0 +1,11 @@
+import RecordingForm from '@/components/pages/production/recording/form/RecordingForm';
+
+const AddRecording = () => {
+ return (
+
+
+
+ );
+};
+
+export default AddRecording;
diff --git a/src/app/production/recording/detail/edit/page.tsx b/src/app/production/recording/detail/edit/page.tsx
new file mode 100644
index 00000000..de53a354
--- /dev/null
+++ b/src/app/production/recording/detail/edit/page.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (!isLoadingRecording && (!recording || isResponseError(recording))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingRecording && (
+
+ )}
+ {!isLoadingRecording && isResponseSuccess(recording) && (
+
+ )}
+
+ );
+};
+
+export default RecordingEdit;
diff --git a/src/app/production/recording/detail/layout.tsx b/src/app/production/recording/detail/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/recording/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children};
+};
+
+export default Layout;
diff --git a/src/app/production/recording/detail/page.tsx b/src/app/production/recording/detail/page.tsx
new file mode 100644
index 00000000..77b82a68
--- /dev/null
+++ b/src/app/production/recording/detail/page.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (!isLoadingRecording && (!recording || isResponseError(recording))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingRecording && (
+
+ )}
+ {!isLoadingRecording && isResponseSuccess(recording) && (
+
+ )}
+
+ );
+};
+
+export default RecordingDetail;
diff --git a/src/app/production/recording/page.tsx b/src/app/production/recording/page.tsx
new file mode 100644
index 00000000..f31ac19a
--- /dev/null
+++ b/src/app/production/recording/page.tsx
@@ -0,0 +1,11 @@
+import RecordingTable from '@/components/pages/production/recording/RecordingTable';
+
+const Recording = () => {
+ return (
+
+ );
+};
+
+export default Recording;
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
new file mode 100644
index 00000000..5dc5022d
--- /dev/null
+++ b/src/components/Badge.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { HTMLAttributes, ReactNode } from 'react';
+
+import { cn } from '@/lib/helper';
+
+export interface BadgeProps
+ extends Omit, '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 (
+
+ {children}
+
+ );
+};
+
+export default Badge;
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index 1895c2c5..06438390 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -1,18 +1,17 @@
-"use client";
+'use client';
-import { HTMLAttributes, ReactNode } from "react";
+import {
+ HTMLAttributes,
+ ReactNode,
+} from 'react';
-import { cn } from "@/lib/helper";
-import Image from "next/image";
+import { cn } from '@/lib/helper';
-export interface CardProps
- extends Omit, "className"> {
+export interface CardProps extends Omit, 'className'> {
title?: string;
subtitle?: string;
image?: string;
imageAlt?: string;
- imageWidth?: number;
- imageHeight?: number;
actions?: ReactNode;
footer?: ReactNode;
className?: {
@@ -24,8 +23,8 @@ export interface CardProps
actions?: string;
footer?: string;
};
- variant?: "default" | "compact" | "bordered" | "shadow" | "image-full";
- size?: "sm" | "md" | "lg";
+ variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full';
+ size?: 'sm' | 'md' | 'lg';
}
const Card = ({
@@ -33,110 +32,85 @@ const Card = ({
subtitle,
image,
imageAlt,
- imageWidth,
- imageHeight,
actions,
footer,
className,
- variant = "default",
- size = "md",
+ variant = 'default',
+ size = 'md',
children,
...props
}: CardProps) => {
const getCardClasses = () => {
- const baseClasses = "card bg-base-100";
+ 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",
+ '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]",
+ 'sm': 'w-64',
+ 'md': 'w-96',
+ 'lg': 'w-[28rem]',
};
return cn(
baseClasses,
variantClasses[variant],
- variant !== "image-full" ? sizeClasses[size] : "",
- className?.wrapper,
+ variant !== 'image-full' ? sizeClasses[size] : '',
+ className?.wrapper
);
};
- const getImageDimensions = () => {
- if (variant === "image-full") {
- return {
- width: imageWidth || 128,
- height: imageHeight || 128,
- };
- }
-
- const cardWidths = {
- sm: 256, // w-64
- md: 384, // w-96
- lg: 448, // w-[28rem]
- };
-
- return {
- width: imageWidth || cardWidths[size],
- height: imageHeight || 192,
- };
- };
-
const getImageClasses = () => {
- if (variant === "image-full") {
- return cn("object-cover", className?.image);
+ if (variant === 'image-full') {
+ return cn('w-32 h-32 object-cover', className?.image);
}
- return cn("w-full object-cover", className?.image);
+ return cn('h-48 object-cover', className?.image);
};
const getBodyClasses = () => {
- const baseClasses = "card-body";
+ const baseClasses = 'card-body';
- if (variant === "compact" || variant === "image-full") {
- return cn(baseClasses, "p-4", className?.body);
+ if (variant === 'compact' || variant === 'image-full') {
+ return cn(baseClasses, 'p-4', className?.body);
}
- return cn(baseClasses, "p-6", className?.body);
+ return cn(baseClasses, 'p-6', className?.body);
};
const getTitleClasses = () => {
const sizeClasses = {
- sm: "text-lg",
- md: "text-xl",
- lg: "text-2xl",
+ 'sm': 'text-lg',
+ 'md': 'text-xl',
+ 'lg': 'text-2xl',
};
- return cn("card-title font-bold", sizeClasses[size], className?.title);
+ return cn('card-title font-bold', sizeClasses[size], className?.title);
};
const getSubtitleClasses = () => {
- return cn("text-base-content/70 text-sm mt-1", className?.subtitle);
+ return cn('text-base-content/70 text-sm mt-1', className?.subtitle);
};
const getActionsClasses = () => {
- return cn("card-actions justify-end mt-4", className?.actions);
+ 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);
+ return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
};
- if (variant === "image-full" && image) {
- const imageDimensions = getImageDimensions();
+ if (variant === 'image-full' && image) {
return (
-
@@ -155,11 +129,9 @@ const Card = ({
{image && (
-
diff --git a/src/components/Collapse.tsx b/src/components/Collapse.tsx
index cb05d5b0..8506f65c 100644
--- a/src/components/Collapse.tsx
+++ b/src/components/Collapse.tsx
@@ -68,7 +68,7 @@ export const Collapse = ({
'collapse',
variant === 'arrow' && 'collapse-arrow',
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',
!open && 'w-fit',
className
diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx
index 4375ca20..89b02845 100644
--- a/src/components/input/NumberInput.tsx
+++ b/src/components/input/NumberInput.tsx
@@ -1,10 +1,10 @@
-'use client';
+"use client";
-import { ChangeEvent } from 'react';
-import { NumericFormat, OnValueChange } from 'react-number-format';
-import TextInput, { TextInputProps } from '@/components/input/TextInput';
+import { ChangeEvent, ReactNode } from "react";
+import { NumericFormat, OnValueChange } from "react-number-format";
+import TextInput, { TextInputProps } from "@/components/input/TextInput";
-interface NumberInputProps extends Omit
{
+interface NumberInputProps extends Omit {
thousandSeparator?: string;
decimalSeparator?: string;
decimalScale?: number;
@@ -12,19 +12,23 @@ interface NumberInputProps extends Omit {
prefix?: string;
suffix?: string;
fixedDecimalScale?: boolean;
+ inputPrefix?: ReactNode;
+ inputSuffix?: ReactNode;
}
const NumberInput = ({
- thousandSeparator = ',',
- decimalSeparator = '.',
+ thousandSeparator = ",",
+ decimalSeparator = ".",
decimalScale = 5,
allowNegative = true,
onChange,
+ inputPrefix,
+ inputSuffix,
...restProps
}: NumberInputProps) => {
const valueChangeHandler: OnValueChange = (
numberFormatValues,
- sourceInfo
+ sourceInfo,
) => {
const newChangeEvent = sourceInfo.event as
| ChangeEvent
@@ -45,6 +49,8 @@ const NumberInput = ({
onValueChange={valueChangeHandler}
decimalScale={decimalScale}
allowNegative={allowNegative}
+ startAdornment={inputPrefix}
+ endAdornment={inputSuffix}
{...restProps}
/>
);
diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx
index cbb0c9db..9b57cabc 100644
--- a/src/components/input/SelectInput.tsx
+++ b/src/components/input/SelectInput.tsx
@@ -104,7 +104,7 @@ const SelectInput = (props: SelectInputProps) => {
const SelectComponent = createables ? CreatableSelect : Select;
/** 🎯 handleChange tanpa any */
- const handleChange = (val: MultiValue | SingleValue | null): void => {
+ const handleChange = (val: MultiValue | SingleValue): void => {
if (!val) {
onChange?.(null);
return;
diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx
index d8d58929..0e7891f7 100644
--- a/src/components/input/TextArea.tsx
+++ b/src/components/input/TextArea.tsx
@@ -1,10 +1,6 @@
'use client';
-import {
- ChangeEventHandler,
- FocusEventHandler,
- ReactNode,
-} from 'react';
+import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react';
import { cn } from '@/lib/helper';
@@ -52,7 +48,7 @@ const TextArea = ({
onBlur,
readOnly = false,
isLoading = false,
- rows = 3
+ rows = 3,
}: TextAreaProps) => {
return (
)}
- {startAdornment && startAdornment}
+ {startAdornment && startAdornment}
-
-
- {(isLoading || endAdornment) && (
-
- {isLoading && }
-
- {endAdornment && endAdornment}
-
+
+
+ {(isLoading || endAdornment) && (
+
+ {isLoading && }
+
+ {endAdornment && endAdornment}
+
+ )}
{!isError && bottomLabel && (
{bottomLabel}
diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts
index 5df66930..ed8fb479 100644
--- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts
+++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts
@@ -61,32 +61,24 @@ const DeliveryObjectSchema: Yup.ObjectSchema
= Yup.object({
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
.min(1, 'Biaya minimal 1!')
.typeError('Biaya harus berupa angka!')
- .test(
- 'one-of-cost-fields',
- 'Biaya pengiriman atau biaya per item wajib diisi!',
- function (value) {
- const { delivery_cost_per_item } = this.parent;
- return (
- (value !== undefined && value > 0) ||
- (delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
- );
- }
- ),
+ .test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
+ const { delivery_cost_per_item } = this.parent;
+ return (
+ (value !== undefined && value > 0) ||
+ (delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
+ );
+ }),
delivery_cost_per_item: Yup.number()
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
.min(1, 'Biaya per item minimal 1!')
.typeError('Biaya per item harus berupa angka!')
- .test(
- 'one-of-cost-fields',
- 'Biaya pengiriman atau biaya per item wajib diisi!',
- function (value) {
- const { delivery_cost } = this.parent;
- return (
- (value !== undefined && value > 0) ||
- (delivery_cost !== undefined && delivery_cost > 0)
- );
- }
- ),
+ .test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
+ const { delivery_cost } = this.parent;
+ return (
+ (value !== undefined && value > 0) ||
+ (delivery_cost !== undefined && delivery_cost > 0)
+ );
+ }),
document_path: Yup.string().optional(),
document_index: Yup.number().optional(),
document: Yup.mixed()
diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx
index 34027209..d8239d14 100644
--- a/src/components/pages/inventory/movement/form/MovementForm.tsx
+++ b/src/components/pages/inventory/movement/form/MovementForm.tsx
@@ -7,8 +7,8 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
+import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions';
import {
@@ -29,6 +29,7 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { toast } from 'react-hot-toast';
import FileInput from '@/components/input/FileInput';
+import CheckboxInput from '@/components/input/CheckboxInput';
interface MovementFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -217,7 +218,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) {
return {
isError: false,
- errorMessage: undefined,
+ errorMessage: '',
};
}
@@ -229,7 +230,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return {
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) {
return {
isError: false,
- errorMessage: undefined,
+ errorMessage: '',
};
}
@@ -255,7 +259,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return {
isError: Boolean(touchedField && errorField),
- errorMessage: touchedField ? errorField : undefined,
+ errorMessage: touchedField && errorField ? errorField : '',
};
};
@@ -706,7 +710,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
value={formik.values.source_warehouse}
onChange={(val) => {
+ formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
+ formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue(
'source_warehouse_id',
(val as WarehouseOptionType)?.value
@@ -764,7 +770,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
value={formik.values.destination_warehouse}
onChange={(val) => {
+ formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
+ formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
(val as WarehouseOptionType)?.value
@@ -831,30 +839,53 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{type !== 'detail' && (
|
- 0
- }
- onChange={(e) => {
- if (e.target.checked) {
- setSelectedProducts(
- formik.values.products?.map(
- (_, idx) => idx
- ) ?? []
- );
- } else {
- setSelectedProducts([]);
+
+ 0
}
- }}
- />
+ onChange={(
+ e: React.ChangeEvent
+ ) => {
+ if (e.target.checked) {
+ setSelectedProducts(
+ formik.values.products?.map(
+ (_, idx) => idx
+ ) ?? []
+ );
+ } else {
+ setSelectedProducts([]);
+ }
+ }}
+ classNames={{
+ wrapper: 'flex justify-center',
+ checkbox: 'checkbox checkbox-sm',
+ }}
+ />
+
|
)}
- Produk |
- Qty |
+
+ Produk
+
+ *
+
+ |
+
+ Qty
+
+ *
+
+ |
{type !== 'detail' && Aksi | }
@@ -863,23 +894,30 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{type !== 'detail' && (
|
- {
- if (e.target.checked) {
- setSelectedProducts([
- ...selectedProducts,
- idx,
- ]);
- } else {
- setSelectedProducts(
- selectedProducts.filter((i) => i !== idx)
- );
- }
- }}
- />
+
+
+ ) => {
+ if (e.target.checked) {
+ setSelectedProducts([
+ ...selectedProducts,
+ idx,
+ ]);
+ } else {
+ setSelectedProducts(
+ selectedProducts.filter((i) => i !== idx)
+ );
+ }
+ }}
+ classNames={{
+ wrapper: 'flex justify-center',
+ checkbox: 'checkbox checkbox-sm',
+ }}
+ />
+
|
)}
@@ -887,10 +925,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={product.product ?? undefined}
onChange={(val) => {
+ formik.setFieldTouched(
+ `products.${idx}.product`,
+ true
+ );
formik.setFieldValue(
`products.${idx}.product`,
val
);
+ formik.setFieldTouched(
+ `products.${idx}.product_id`,
+ true
+ );
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
@@ -911,7 +957,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isClearable
{...isRepeaterInputError(
'products',
- 'product',
+ 'product_id',
idx
)}
className={{
@@ -954,17 +1000,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
{type !== 'detail' && (
-
+
+
+
|
)}
@@ -1006,43 +1054,106 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{/* Deliveries table */}
-
Pengiriman
+
Pengiriman
{type !== 'detail' && (
|
- 0
- }
- onChange={(e) => {
- if (e.target.checked) {
- setSelectedDeliveries(
- formik.values.deliveries?.map(
- (_, idx) => idx
- ) ?? []
- );
- } else {
- setSelectedDeliveries([]);
+
+ 0
}
- }}
- />
+ onChange={(
+ e: React.ChangeEvent
+ ) => {
+ if (e.target.checked) {
+ setSelectedDeliveries(
+ formik.values.deliveries?.map(
+ (_, idx) => idx
+ ) ?? []
+ );
+ } else {
+ setSelectedDeliveries([]);
+ }
+ }}
+ classNames={{
+ wrapper: 'flex justify-center',
+ checkbox: 'checkbox checkbox-sm',
+ }}
+ />
+
|
)}
- Produk |
- Qty |
- Supplier |
- Plat Nomor |
+
+ Produk
+
+ *
+
+ |
+
+ Qty
+
+ *
+
+ |
+
+ Supplier
+
+ *
+
+ |
+
+ Plat Nomor
+
+ *
+
+ |
Dokumen |
- Biaya Pengiriman (Rp.) |
- Biaya Per Item (Rp.) |
- Nama Sopir |
+
+ Biaya Pengiriman (Rp.)
+
+ *
+
+ |
+
+ Biaya Per Item (Rp.)
+
+ *
+
+ |
+
+ Nama Sopir
+
+ *
+
+ |
{type !== 'detail' && Aksi | }
@@ -1051,23 +1162,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{type !== 'detail' && (
|
- {
- if (e.target.checked) {
- setSelectedDeliveries([
- ...selectedDeliveries,
- idx,
- ]);
- } else {
- setSelectedDeliveries(
- selectedDeliveries.filter((i) => i !== idx)
- );
- }
- }}
- />
+
+
+ ) => {
+ if (e.target.checked) {
+ setSelectedDeliveries([
+ ...selectedDeliveries,
+ idx,
+ ]);
+ } else {
+ setSelectedDeliveries(
+ selectedDeliveries.filter(
+ (i) => i !== idx
+ )
+ );
+ }
+ }}
+ classNames={{
+ wrapper: 'flex justify-center',
+ checkbox: 'checkbox checkbox-sm',
+ }}
+ />
+
|
)}
@@ -1075,10 +1195,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={delivery.products[0]?.product ?? undefined}
onChange={(val) => {
+ formik.setFieldTouched(
+ `deliveries.${idx}.products.0.product`,
+ true
+ );
formik.setFieldValue(
`deliveries.${idx}.products.0.product`,
val
);
+ formik.setFieldTouched(
+ `deliveries.${idx}.products.0.product_id`,
+ true
+ );
formik.setFieldValue(
`deliveries.${idx}.products.0.product_id`,
(val as OptionType)?.value
@@ -1087,6 +1215,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
options={getFilteredProductWarehouseOptions()}
isDisabled={type === 'detail'}
isClearable
+ isError={
+ isDeliveryProductInputError(idx, 0, 'product_id')
+ .isError
+ }
+ errorMessage={
+ isDeliveryProductInputError(idx, 0, 'product_id')
+ .errorMessage
+ }
className={{
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
@@ -1122,10 +1258,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={delivery.supplier}
onChange={(val) => {
+ formik.setFieldTouched(
+ `deliveries.${idx}.supplier`,
+ true
+ );
formik.setFieldValue(
`deliveries.${idx}.supplier`,
val
);
+ formik.setFieldTouched(
+ `deliveries.${idx}.supplier_id`,
+ true
+ );
formik.setFieldValue(
`deliveries.${idx}.supplier_id`,
(val as OptionType)?.value
@@ -1136,6 +1280,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isLoading={isLoadingSuppliers}
isDisabled={type === 'detail'}
isClearable
+ {...isRepeaterInputError(
+ 'deliveries',
+ 'supplier_id',
+ idx
+ )}
className={{
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
@@ -1163,27 +1312,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
{type === 'detail' ? (
-
+ <>
+
+
+
+ >
) : (
{
toast.error(
'Ukuran dokumen maksimal 2 MB!'
);
+ e.target.value = '';
return;
}
formik.setFieldValue(
@@ -1215,9 +1369,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
|
-
@@ -1231,14 +1384,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
readOnly={type === 'detail'}
className={{
- wrapper: 'w-full min-w-48',
+ wrapper:
+ 'w-full min-w-52 md:min-w-72 lg:min-w-80',
}}
/>
|
-
@@ -1255,7 +1408,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
readOnly={type === 'detail'}
className={{
- wrapper: 'w-full min-w-48',
+ wrapper:
+ 'w-full min-w-52 md:min-w-72 lg:min-w-80',
}}
/>
|
@@ -1280,17 +1434,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{type !== 'detail' && (
-
+
+
+
|
)}
diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx
new file mode 100644
index 00000000..85ec092f
--- /dev/null
+++ b/src/components/pages/production/recording/RecordingTable.tsx
@@ -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;
+ deleteClickHandler: () => void;
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const RecordingTable = () => {
+ const [search, setSearch] = useState('');
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const [sorting, setSorting] = useState([]);
+ const [selectedRecordings, setSelectedRecordings] = useState([]);
+ const [, setSelectedRecording] = useState(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) => {
+ 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 (
+
+
+
+ {/* Bulk action buttons */}
+
+ {selectedRecordings.length > 0 && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
(
+ 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 }) => (
+ {
+ 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) => {
+ 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 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ]}
+ 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',
+ }}
+ />
+
+
+
+ );
+};
+
+export default RecordingTable;
diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts
new file mode 100644
index 00000000..4b0b37dd
--- /dev/null
+++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts
@@ -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().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().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()
+ .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;
+
+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,
+ },
+ ],
+});
diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx
new file mode 100644
index 00000000..6018ae84
--- /dev/null
+++ b/src/components/pages/production/recording/form/RecordingForm.tsx
@@ -0,0 +1,1760 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useFormik } from 'formik';
+import { Icon } from '@iconify/react';
+import Button from '@/components/Button';
+import TextInput from '@/components/input/TextInput';
+import NumberInput from '@/components/input/NumberInput';
+import CheckboxInput from '@/components/input/CheckboxInput';
+import SelectInput, { OptionType } from '@/components/input/SelectInput';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import { FormHeader } from '@/components/helper/form/FormHeader';
+import { FormActions } from '@/components/helper/form/FormActions';
+import {
+ CreateRecordingPayload,
+ Recording,
+} from '@/types/api/production/recording';
+import {
+ RecordingFormSchema,
+ RecordingFormValues,
+ getRecordingFormInitialValues,
+ UpdateRecordingFormSchema,
+} from './RecordingForm.schema';
+import { useRecordingFormHandlers } from './useRecordingFormHandlers';
+import { ProjectFlockApi } from '@/services/api/production';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
+import useSWR from 'swr';
+import { ProductWarehouseApi } from '@/services/api/inventory';
+import { ProjectFlock } from '@/types/api/production/project-flock';
+import { Warehouse } from '@/types/api/master-data/warehouse';
+import { LocationApi } from '@/services/api/master-data';
+import Card from '@/components/Card';
+
+interface RecordingFormProps {
+ type?: 'add' | 'edit' | 'detail';
+ initialValues?: Recording;
+}
+
+const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
+ const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
+ const [flockSelectInputValue, setFlockSelectInputValue] = useState('');
+ const [selectedProjectFlock, setSelectedProjectFlock] =
+ useState(null);
+ const [selectedFeed, setSelectedFeed] = useState([]);
+ const [selectedWeight, setSelectedWeight] = useState([]);
+ const [selectedVaccine, setSelectedVaccine] = useState([]);
+ const [selectedMortality, setSelectedMortality] = useState([]);
+ const [, setRecordingFormErrorMessage] = useState('');
+
+ const {
+ deleteModal,
+ recordingFormErrorMessage,
+ isDeleteLoading,
+ createRecordingHandler,
+ updateRecordingHandler,
+ deleteRecordingClickHandler,
+ confirmationModalDeleteClickHandler,
+ } = useRecordingFormHandlers(initialValues?.id);
+
+ const formikInitialValues = useMemo(
+ () => getRecordingFormInitialValues(initialValues),
+ [initialValues]
+ );
+
+ const formik = useFormik({
+ initialValues: formikInitialValues,
+ validationSchema:
+ type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema,
+ validateOnChange: true,
+ validateOnBlur: true,
+ onSubmit: async (values) => {
+ setRecordingFormErrorMessage('');
+ const payload: CreateRecordingPayload = {
+ flock_id: values.flock_id,
+ location_id: values.location_id,
+ coop_id: values.coop_id,
+ recording_date:
+ values.recording_date instanceof Date
+ ? values.recording_date.toISOString()
+ : '',
+ feed_data: (values.feed_data ?? []).map((p) => ({
+ feed_id: p.feed_id,
+ feed_qty:
+ typeof p.feed_qty === 'number'
+ ? p.feed_qty
+ : parseFloat(String(p.feed_qty)) || 0,
+ feed_stock:
+ typeof p.feed_stock === 'number'
+ ? p.feed_stock
+ : parseFloat(String(p.feed_stock)) || 0,
+ })),
+ body_weight: (values.body_weight ?? []).map((b) => ({
+ chicken_weight:
+ typeof b.chicken_weight === 'number'
+ ? b.chicken_weight
+ : parseFloat(String(b.chicken_weight)) || 0,
+ chicken_count:
+ typeof b.chicken_count === 'number'
+ ? b.chicken_count
+ : parseFloat(String(b.chicken_count)) || 0,
+ average_chicken_weight:
+ typeof b.average_chicken_weight === 'number'
+ ? b.average_chicken_weight
+ : parseFloat(String(b.average_chicken_weight)) || 0,
+ })),
+ vaccination: (values.vaccination ?? []).map((v) => ({
+ vaccine_id: v.vaccine_id,
+ total_stock:
+ typeof v.total_stock === 'number'
+ ? v.total_stock
+ : parseFloat(String(v.total_stock)) || 0,
+ used_stock:
+ typeof v.used_stock === 'number'
+ ? v.used_stock
+ : parseFloat(String(v.used_stock)) || 0,
+ })),
+ mortality: (values.mortality ?? []).map((m) => ({
+ condition: m.condition,
+ count:
+ typeof m.count === 'number'
+ ? m.count
+ : parseFloat(String(m.count)) || 0,
+ })),
+ };
+
+ switch (type) {
+ case 'add':
+ await createRecordingHandler(payload);
+ break;
+ case 'edit':
+ await updateRecordingHandler(initialValues?.id as number, payload);
+ break;
+ }
+ },
+ });
+
+ // Locations
+ const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue }).toString()}`;
+ const { data: locations, isLoading: isLoadingLocations } = useSWR(
+ locationsUrl,
+ LocationApi.getAllFetcher
+ );
+
+ // Project Flocks
+ const projectFlocksUrl = useMemo(() => {
+ if (!formik.values.location_id) return null;
+ const params = new URLSearchParams({
+ search: flockSelectInputValue,
+ location_id: formik.values.location_id.toString(),
+ });
+ return `${ProjectFlockApi.basePath}?${params.toString()}`;
+ }, [formik.values.location_id, flockSelectInputValue]);
+
+ const { data: projectFlocks, isLoading: isLoadingFlocks } = useSWR(
+ projectFlocksUrl,
+ ProjectFlockApi.getAllFetcher
+ );
+
+ // Pakan Products
+ const pakanUrl = useMemo(() => {
+ if (!formik.values.location_id) return null;
+ const params = new URLSearchParams({
+ flag: 'PAKAN',
+ search: '',
+ location_id: formik.values.location_id.toString(),
+ });
+ return `${ProductWarehouseApi.basePath}?${params.toString()}`;
+ }, [formik.values.location_id]);
+
+ const { data: pakanProducts, isLoading: isLoadingPakan } = useSWR(
+ pakanUrl,
+ ProductWarehouseApi.getAllFetcher
+ );
+
+ // OVK Products
+ const ovkUrl = useMemo(() => {
+ if (!formik.values.location_id) return null;
+ const params = new URLSearchParams({
+ flag: 'OVK',
+ search: '',
+ location_id: formik.values.location_id.toString(),
+ });
+ return `${ProductWarehouseApi.basePath}?${params.toString()}`;
+ }, [formik.values.location_id]);
+
+ const { data: ovkProducts, isLoading: isLoadingOvk } = useSWR(
+ ovkUrl,
+ ProductWarehouseApi.getAllFetcher
+ );
+
+ // COMPUTED VALUES
+ const buildWarehouseLabel = useCallback((warehouse: Warehouse) => {
+ const parts: string[] = [warehouse.name];
+
+ if ('kandang' in warehouse && warehouse.kandang) {
+ parts.push(warehouse.kandang.name);
+ }
+
+ if ('location' in warehouse && warehouse.location) {
+ parts.push(warehouse.location.name);
+ }
+
+ if (warehouse.area) {
+ parts.push(warehouse.area.name);
+ }
+
+ return parts.join(' - ');
+ }, []);
+
+ const locationOptions = isResponseSuccess(locations)
+ ? locations.data.map((loc) => ({ value: loc.id, label: loc.name }))
+ : [];
+
+ const flockOptions = isResponseSuccess(projectFlocks)
+ ? projectFlocks.data.map((flock) => ({
+ value: flock.id,
+ label: flock.flock.name,
+ }))
+ : [];
+
+ const coopOptions = useMemo(() => {
+ if (!selectedProjectFlock || !selectedProjectFlock.kandangs) return [];
+ return selectedProjectFlock.kandangs.map((kandang) => ({
+ value: kandang.id,
+ label: kandang.name,
+ }));
+ }, [selectedProjectFlock]);
+
+ const filteredPakanProducts = useMemo(() => {
+ if (!isResponseSuccess(pakanProducts) || !formik.values.location_id)
+ return [];
+
+ return pakanProducts.data.filter((product) => {
+ const warehouse = product.warehouse;
+
+ const hasLocationMatch =
+ 'location' in warehouse && warehouse.location
+ ? warehouse.location.id === formik.values.location_id
+ : false;
+
+ const hasPakanFlag = product.product.flags?.includes('PAKAN');
+
+ return hasLocationMatch && hasPakanFlag;
+ });
+ }, [pakanProducts, formik.values.location_id]);
+
+ const pakanOptions = useMemo(
+ () =>
+ filteredPakanProducts.map((product) => ({
+ value: product.id,
+ label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
+ })),
+ [filteredPakanProducts, buildWarehouseLabel]
+ );
+
+ const pakanStockMap = useMemo(() => {
+ const map = new Map();
+ filteredPakanProducts.forEach((product) => {
+ map.set(product.id, product.quantity);
+ });
+ return map;
+ }, [filteredPakanProducts]);
+
+ const filteredOvkProducts = useMemo(() => {
+ if (!isResponseSuccess(ovkProducts) || !formik.values.location_id)
+ return [];
+
+ return ovkProducts.data.filter((product) => {
+ const warehouse = product.warehouse;
+
+ // Validate location match
+ const hasLocationMatch =
+ 'location' in warehouse && warehouse.location
+ ? warehouse.location.id === formik.values.location_id
+ : false;
+
+ // Validate product has OVK flag
+ const hasOvkFlag = product.product.flags?.includes('OVK');
+
+ return hasLocationMatch && hasOvkFlag;
+ });
+ }, [ovkProducts, formik.values.location_id]);
+
+ const ovkOptions = useMemo(
+ () =>
+ filteredOvkProducts.map((product) => ({
+ value: product.id,
+ label: `${product.product.name} - ${buildWarehouseLabel(product.warehouse)} (Stock: ${product.quantity.toLocaleString('id-ID')})`,
+ })),
+ [filteredOvkProducts, buildWarehouseLabel]
+ );
+
+ const ovkStockMap = useMemo(() => {
+ const map = new Map();
+ filteredOvkProducts.forEach((product) => {
+ map.set(product.id, product.quantity);
+ });
+ return map;
+ }, [filteredOvkProducts]);
+
+ // EFFECTS
+ useEffect(() => {
+ if (initialValues?.flock && isResponseSuccess(projectFlocks)) {
+ const flock = projectFlocks.data.find(
+ (f) => f.id === initialValues.flock.id
+ );
+ if (flock) {
+ setSelectedProjectFlock(flock);
+ }
+ }
+ }, [initialValues, projectFlocks]);
+
+ // Auto-calculate average weight when chicken weight or count changes
+ useEffect(() => {
+ if (formik.values.body_weight) {
+ const updatedBodyWeight = formik.values.body_weight.map((weight) => ({
+ ...weight,
+ average_chicken_weight:
+ weight.chicken_count > 0
+ ? Math.round(weight.chicken_weight / weight.chicken_count)
+ : 0,
+ }));
+
+ // Only update if values are different to avoid infinite loops
+ const hasChanges = updatedBodyWeight.some(
+ (updated, idx) =>
+ updated.average_chicken_weight !==
+ formik.values.body_weight[idx]?.average_chicken_weight
+ );
+
+ if (hasChanges) {
+ formik.setFieldValue('body_weight', updatedBodyWeight);
+ }
+ }
+ }, [
+ formik.values.body_weight?.map((w) => w.chicken_weight),
+ formik.values.body_weight?.map((w) => w.chicken_count),
+ ]);
+
+ // EVENT HANDLERS - Select Inputs
+ const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const locationValue = (val as OptionType)?.value;
+
+ formik.setFieldValue('location', val, false);
+ formik.setFieldValue('location_id', locationValue || 0, false);
+
+ formik.setFieldValue('flock', null, false);
+ formik.setFieldValue('flock_id', 0, false);
+ formik.setFieldValue('coop', null, false);
+ formik.setFieldValue('coop_id', 0, false);
+ setSelectedProjectFlock(null);
+ setFlockSelectInputValue('');
+ };
+
+ const flockChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const flockValue = (val as OptionType)?.value;
+
+ const selected = isResponseSuccess(projectFlocks)
+ ? projectFlocks.data.find((flock) => flock.id === flockValue)
+ : null;
+
+ setSelectedProjectFlock(selected || null);
+
+ formik.setFieldValue('flock', val, false);
+ formik.setFieldValue('flock_id', flockValue || 0, false);
+
+ formik.setFieldValue('coop', null, false);
+ formik.setFieldValue('coop_id', 0, false);
+ };
+
+ const coopChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const coopValue = (val as OptionType)?.value;
+
+ formik.setFieldValue('coop', val, false);
+ formik.setFieldValue('coop_id', coopValue || 0, false);
+ };
+
+ // EVENT HANDLERS - Feed Data
+ const addFeedData = () => {
+ const newFeedData = [
+ ...(formik.values.feed_data || []),
+ {
+ feed: null,
+ feed_id: '',
+ feed_qty: '',
+ feed_stock: 0,
+ },
+ ];
+ formik.setFieldValue('feed_data', newFeedData);
+ };
+
+ const removeFeedData = (idx: number) => {
+ const updatedFeedData = formik.values.feed_data?.filter(
+ (_, i) => i !== idx
+ );
+ formik.setFieldValue('feed_data', updatedFeedData);
+ };
+
+ const removeSelectedFeedData = () => {
+ const updatedFeedData = formik.values.feed_data?.filter(
+ (_, idx) => !selectedFeed.includes(idx)
+ );
+ formik.setFieldValue('feed_data', updatedFeedData);
+ setSelectedFeed([]);
+ };
+
+ // EVENT HANDLERS - Body Weight
+ const addBodyWeight = () => {
+ const newBodyWeight = [
+ ...(formik.values.body_weight || []),
+ {
+ chicken_weight: 0,
+ chicken_count: 0,
+ average_chicken_weight: 0,
+ },
+ ];
+ formik.setFieldValue('body_weight', newBodyWeight);
+ };
+
+ // Handle calculation when chicken_weight changes
+ const handleChickenWeightChange = useCallback(
+ (idx: number, value: number) => {
+ formik.setFieldValue(`body_weight.${idx}.chicken_weight`, value);
+
+ const currentWeight = formik.values.body_weight?.[idx];
+ if (currentWeight) {
+ const chickenCount = currentWeight.chicken_count;
+ if (chickenCount > 0 && value > 0) {
+ const averageWeight = Math.round(value / chickenCount);
+ formik.setFieldValue(
+ `body_weight.${idx}.average_chicken_weight`,
+ averageWeight
+ );
+ } else {
+ formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, '');
+ }
+ }
+ },
+ [formik]
+ );
+
+ // Handle calculation when chicken_count changes
+ const handleChickenCountChange = useCallback(
+ (idx: number, value: number) => {
+ formik.setFieldValue(`body_weight.${idx}.chicken_count`, value);
+
+ const currentWeight = formik.values.body_weight?.[idx];
+ if (currentWeight) {
+ const chickenWeight = currentWeight.chicken_weight;
+ if (chickenWeight > 0 && value > 0) {
+ const averageWeight = Math.round(chickenWeight / value);
+ formik.setFieldValue(
+ `body_weight.${idx}.average_chicken_weight`,
+ averageWeight
+ );
+ } else {
+ formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, '');
+ }
+ }
+ },
+ [formik]
+ );
+
+ // Handle calculation when average_weight changes
+ const handleAverageWeightChange = useCallback(
+ (idx: number, value: number) => {
+ formik.setFieldValue(`body_weight.${idx}.average_chicken_weight`, value);
+
+ const currentWeight = formik.values.body_weight?.[idx];
+ if (currentWeight) {
+ const chickenCount = currentWeight.chicken_count;
+ if (chickenCount > 0 && value > 0) {
+ const totalWeight = value * chickenCount;
+ formik.setFieldValue(
+ `body_weight.${idx}.chicken_weight`,
+ totalWeight
+ );
+ } else if (value === 0) {
+ formik.setFieldValue(`body_weight.${idx}.chicken_weight`, '');
+ }
+ }
+ },
+ [formik]
+ );
+
+ // Create wrapper handlers that match NumberInput's onChange signature
+ const handleChickenWeightChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
+ handleChickenWeightChange(idx, value);
+ },
+ [handleChickenWeightChange]
+ );
+
+ const handleChickenCountChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
+ handleChickenCountChange(idx, value);
+ },
+ [handleChickenCountChange]
+ );
+
+ const handleAverageWeightChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseFloat(e.target.value.replace(/[^\d,.-]/g, '').replace(/,/g, '')) || 0;
+ handleAverageWeightChange(idx, value);
+ },
+ [handleAverageWeightChange]
+ );
+
+ const handleVaccinationStockChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
+ formik.setFieldValue(`vaccination.${idx}.used_stock`, value);
+ },
+ [formik]
+ );
+
+ const handleMortalityCountChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
+ formik.setFieldValue(`mortality.${idx}.count`, value);
+ },
+ [formik]
+ );
+
+ const removeBodyWeight = (idx: number) => {
+ const updatedBodyWeight = formik.values.body_weight?.filter(
+ (_, i) => i !== idx
+ );
+ formik.setFieldValue('body_weight', updatedBodyWeight);
+ };
+
+ const removeSelectedBodyWeight = () => {
+ const updatedBodyWeight = formik.values.body_weight?.filter(
+ (_, idx) => !selectedWeight.includes(idx)
+ );
+ formik.setFieldValue('body_weight', updatedBodyWeight);
+ setSelectedWeight([]);
+ };
+
+ // EVENT HANDLERS - Vaccination
+ const addVaccination = () => {
+ const newVaccination = [
+ ...(formik.values.vaccination || []),
+ {
+ vaccine: null,
+ vaccine_id: '',
+ total_stock: '',
+ used_stock: 0,
+ },
+ ];
+ formik.setFieldValue('vaccination', newVaccination);
+ };
+
+ const removeVaccination = (idx: number) => {
+ const updatedVaccination = formik.values.vaccination?.filter(
+ (_, i) => i !== idx
+ );
+ formik.setFieldValue('vaccination', updatedVaccination);
+ };
+
+ const removeSelectedVaccination = () => {
+ const updatedVaccination = formik.values.vaccination?.filter(
+ (_, idx) => !selectedVaccine.includes(idx)
+ );
+ formik.setFieldValue('vaccination', updatedVaccination);
+ setSelectedVaccine([]);
+ };
+
+ // EVENT HANDLERS - Mortality
+ const addMortality = () => {
+ const newMortality = [
+ ...(formik.values.mortality || []),
+ {
+ condition: RECORDING_FLAG_OPTIONS[0].value,
+ count: 0,
+ },
+ ];
+ formik.setFieldValue('mortality', newMortality);
+ };
+
+ const removeMortality = (idx: number) => {
+ const updatedMortality = formik.values.mortality?.filter(
+ (_, i) => i !== idx
+ );
+ formik.setFieldValue('mortality', updatedMortality);
+ };
+
+ const removeSelectedMortality = () => {
+ const updatedMortality = formik.values.mortality?.filter(
+ (_, idx) => !selectedMortality.includes(idx)
+ );
+ formik.setFieldValue('mortality', updatedMortality);
+ setSelectedMortality([]);
+ };
+
+ const handleFeedStockChangeWrapper = useCallback(
+ (idx: number) => (e: React.ChangeEvent) => {
+ const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
+ formik.setFieldValue(`feed_data.${idx}.feed_stock`, value);
+ },
+ [formik]
+ );
+
+ // HELPER FUNCTIONS
+ const isRepeaterInputError = <
+ T extends 'feed_data' | 'body_weight' | 'vaccination' | 'mortality',
+ >(
+ arrayName: T,
+ column: T extends 'feed_data'
+ ? keyof RecordingFormValues['feed_data'][0]
+ : T extends 'body_weight'
+ ? keyof RecordingFormValues['body_weight'][0]
+ : T extends 'vaccination'
+ ? keyof RecordingFormValues['vaccination'][0]
+ : T extends 'mortality'
+ ? keyof RecordingFormValues['mortality'][0]
+ : never,
+ idx: number
+ ) => {
+ if (
+ !formik.touched[arrayName] ||
+ !Array.isArray(formik.touched[arrayName])
+ ) {
+ return {
+ isError: false,
+ errorMessage: '',
+ };
+ }
+
+ const touchedField = formik.touched[arrayName]?.[idx]?.[column as string];
+ const errorField = formik.errors[arrayName]?.[idx] as Record<
+ string,
+ string
+ >;
+
+ return {
+ isError: touchedField && Boolean(errorField?.[column as string]),
+ errorMessage:
+ touchedField && errorField?.[column as string]
+ ? errorField[column as string]
+ : '',
+ };
+ };
+
+ return (
+ <>
+
+
+ {type !== 'add' && (
+
+ )}
+ >
+ );
+};
+
+export default RecordingForm;
diff --git a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts
new file mode 100644
index 00000000..334b791d
--- /dev/null
+++ b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts
@@ -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,
+ };
+};
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 9a648e03..2b87c4d7 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -126,13 +126,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
{
title: 'Flock',
link: '/master-data/flock',
- icon: 'material-symbols:raven-outline-rounded'
+ icon: 'material-symbols:raven-outline-rounded',
},
],
},
] as const;
-
export const ROWS_OPTIONS = [
{
label: '10',
@@ -215,3 +214,9 @@ export const PRODUCT_FLAG_OPTIONS = [
export const SUPPLIER_FLAG_OPTIONS = [
{ 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' },
+];
diff --git a/src/services/api/production.ts b/src/services/api/production.ts
index 23a3fe68..1ccf4a6e 100644
--- a/src/services/api/production.ts
+++ b/src/services/api/production.ts
@@ -1,19 +1,30 @@
+import { BaseApiService } from './base';
import {
- ProjectFlock,
CreateProjectFlockPayload,
+ ProjectFlock,
+ UpdateProjectFlockPayload,
} from '@/types/api/production/project-flock';
+import {
+ CreateRecordingPayload,
+ Recording,
+ UpdateRecordingPayload,
+} from '@/types/api/production/recording';
import {
Chickin,
CreateChickinPayload,
UpdateChickinPayload,
} from '@/types/api/production/chickin';
-import { BaseApiService } from '@/services/api/base';
export const ProjectFlockApi = new BaseApiService<
ProjectFlock,
CreateProjectFlockPayload,
- unknown
+ UpdateProjectFlockPayload
>('/production/project_flocks');
+export const RecordingApi = new BaseApiService<
+ Recording,
+ CreateRecordingPayload,
+ UpdateRecordingPayload
+>('/flock/recordings');
export const ChickinApi = new BaseApiService<
Chickin,
CreateChickinPayload,
diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css
index 9a148fb4..dadaa2fa 100644
--- a/src/styles/daisyui.css
+++ b/src/styles/daisyui.css
@@ -8,4 +8,8 @@
--step-bg: var(--color-error);
--step-fg: var(--color-error-content);
}
+
+ .table :where(th, td) {
+ vertical-align: top;
+ }
}
diff --git a/src/types/api/inventory/adjustment.d.ts b/src/types/api/inventory/adjustment.d.ts
index 852389fe..4cb4dab4 100644
--- a/src/types/api/inventory/adjustment.d.ts
+++ b/src/types/api/inventory/adjustment.d.ts
@@ -1,4 +1,5 @@
import { Product } from '@/types/api/master-data/product';
+import { BaseMetadata } from '../base-metadata';
import { Warehouse } from '@/types/api/master-data/warehouse';
export type BaseInventoryAdjustment = {
@@ -28,3 +29,6 @@ export type CreateInventoryAdjustmentPayload = {
quantity: number;
note: string;
};
+
+export type UpdateInventoryAdjustmentPayload =
+ Partial;
diff --git a/src/types/api/master-data/flock.d.ts b/src/types/api/master-data/flock.d.ts
index 3ac5d390..e0dcfda4 100644
--- a/src/types/api/master-data/flock.d.ts
+++ b/src/types/api/master-data/flock.d.ts
@@ -1,14 +1,14 @@
-import { BaseMetadata } from "@/types/api/api-general";
+import { BaseMetadata } from '@/types/api/api-general';
export type BaseFlock = {
id: number;
name: string;
-}
+};
export type Flock = BaseMetadata & BaseFlock;
export type CreateFlockPayload = {
name: string;
-}
+};
-export type UpdateFlockPayload = CreateFlockPayload;
\ No newline at end of file
+export type UpdateFlockPayload = CreateFlockPayload;
diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts
new file mode 100644
index 00000000..6fac0bc8
--- /dev/null
+++ b/src/types/api/production/recording.d.ts
@@ -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;