diff --git a/package-lock.json b/package-lock.json index a39060ef..72580442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", + "react-number-format": "^5.4.4", "react-select": "^5.10.2", "swr": "^2.3.6", "tailwind-merge": "^3.3.1", @@ -5811,6 +5812,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-select": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", diff --git a/package.json b/package.json index e970499c..2e806ddd 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hot-toast": "^2.6.0", + "react-number-format": "^5.4.4", "react-select": "^5.10.2", "swr": "^2.3.6", "tailwind-merge": "^3.3.1", diff --git a/src/app/production/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx index 1823b41a..af834b37 100644 --- a/src/app/production/chickin/add/page.tsx +++ b/src/app/production/chickin/add/page.tsx @@ -59,7 +59,7 @@ const AddChickin = () => { ? listProjectFlock?.data.map((projectFlock) => { return { value: projectFlock.id, - label: `${projectFlock?.flock.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`, + label: `${projectFlock?.flock?.name} - ${projectFlock?.category} - Periode ${projectFlock.period}`, }; }) : []; @@ -242,6 +242,7 @@ const AddChickin = () => { created_user: projectFlock.data.created_user, created_at: projectFlock.data.created_at, updated_at: projectFlock.data.updated_at, + approval: projectFlock.data.approval, }} afterSubmit={handleAfterSubmit} /> diff --git a/src/app/production/chickin/detail/page.tsx b/src/app/production/chickin/detail/page.tsx index d62c92bf..aef7c4b7 100644 --- a/src/app/production/chickin/detail/page.tsx +++ b/src/app/production/chickin/detail/page.tsx @@ -1,24 +1,51 @@ -'use client' +'use client'; -import { isResponseError, isResponseSuccess } from "@/lib/api-helper"; -import { ChickinApi } from "@/services/api/production"; -import { useRouter, useSearchParams } from "next/navigation"; -import useSWR from "swr"; +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import Modal, { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { ChickinApi } from '@/services/api/production'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { + Chickin, + ChickinApprovalPayload, +} from '@/types/api/production/chickin'; +import { Icon } from '@iconify/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import useSWR from 'swr'; const DetailChickin = () => { const router = useRouter(); const searchParams = useSearchParams(); const chickinId = searchParams.get('chickinId'); + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const confirmModal = useModal(); + const deleteModal = useModal(); + const chickinModal = useModal(); const { data: chickin, isLoading, - } = useSWR( - chickinId, - (id: number) => ChickinApi.getSingle(id) + mutate: refreshChickin, + } = useSWR(chickinId, (id: number) => ChickinApi.getSingle(id)); + + const [isApprovedDisabled, setIsApprovedDisabled] = useState( + // chickin.data?.approval.step_number == 1 ? false : true + true + ); + const [isRejectedDisabled, setIsRejectedDisabled] = useState( + !isApprovedDisabled + ); + const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( + !isApprovedDisabled ? 'APPROVED' : 'REJECTED' ); - if(!chickinId){ + if (!chickinId) { router.back(); return ( @@ -28,46 +55,291 @@ const DetailChickin = () => { ); } - if ( - !isLoading && - (!chickin || isResponseError(chickin)) - ) { + if (!isLoading && (!chickin || isResponseError(chickin))) { router.replace('/404'); return; } + if (!isResponseSuccess(chickin)) { + return ( +
+ +
+ ); + } + + const confirmationModalClickHandler = async ({ + action = 'APPROVED', + }: { + action: 'APPROVED' | 'REJECTED'; + }) => { + if (chickin?.data.id === undefined) return; + setIsApproveLoading(true); + const approveChickinRes = await ChickinApi.customRequest< + BaseApiResponse, + ChickinApprovalPayload + >(`/approvals`, { + method: 'POST', + payload: { + action: action, + approvable_ids: [chickin.data.id], + }, + }); + + if (isResponseSuccess(approveChickinRes)) { + if (refreshChickin) { + await refreshChickin(); + } + toast.success(approveChickinRes.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + } + confirmModal.closeModal(); + setIsApproveLoading(false); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + const deleteProjectFlockRes = await ChickinApi.delete( + chickin.data?.id as number + ); + + if (isResponseSuccess(deleteProjectFlockRes)) { + toast.success(deleteProjectFlockRes?.message as string); + router.push('/production/chickin'); + } + if (isResponseError(deleteProjectFlockRes)) { + toast.error(deleteProjectFlockRes?.message as string); + } + setIsDeleteLoading(false); + }; + return ( <> -
+
{isLoading && } {!isLoading && isResponseSuccess(chickin) && ( <> -
-
-
- Informasi Project Flock + {/*
+ + +
*/} + +
+
+
Flock
+
+ { + chickin.data.project_flock_kandang?.project_flock.flock + .name + } +
- -
-
-
Flock
-
{chickin.data.project_flock_kandang?.project_flock.flock.name}
+
+
Area
+
+ { + chickin.data.project_flock_kandang?.project_flock.area + .name + } +
+
+
+
Kategori
+
+ {chickin.data.project_flock_kandang?.project_flock.category} +
+
+
+
Lokasi
+
+ { + chickin.data.project_flock_kandang?.project_flock.location + .name + } +
+
+
+
Periode
+
+ {chickin.data.project_flock_kandang?.project_flock.period} +
+
+
+
Kandang
+
+ {chickin.data.project_flock_kandang?.kandang.name}
-
-
-
-
- Informasi Chickin + + +
+
+
Flock Kandang
+
+ { + chickin.data.project_flock_kandang?.project_flock.flock + .name + }{' '} + - {chickin.data.project_flock_kandang?.kandang.name} +
+
+
+
Tanggal Chickin
+
+ {chickin.data.chick_in_date + ? new Date(chickin.data.chick_in_date).toLocaleDateString( + 'id-ID' + ) + : '-'} +
+
+
+
Jumlah (Ekor)
+
+ {chickin.data.quantity?.toLocaleString('id-ID')} +
+
+
+
Catatan
+
{chickin.data.note}
+
+
+ +
)}
+ + + + +
+

+ Chickin Kandang -{' '} + {chickin?.data?.project_flock_kandang && + chickin?.data?.project_flock_kandang.kandang?.name} +

+ +
+ { + refreshChickin(); + chickinModal.closeModal(); + }} + /> +
+ + { + confirmationModalClickHandler({ + action: approvalAction, + }); + }, + }} + /> ); -} +}; -export default DetailChickin; \ No newline at end of file +export default DetailChickin; diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 00000000..1895c2c5 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { HTMLAttributes, ReactNode } from "react"; + +import { cn } from "@/lib/helper"; +import Image from "next/image"; + +export interface CardProps + extends Omit, "className"> { + title?: string; + subtitle?: string; + image?: string; + imageAlt?: string; + imageWidth?: number; + imageHeight?: number; + 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, + imageWidth, + imageHeight, + 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 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); + } + return cn("w-full 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) { + const imageDimensions = getImageDimensions(); + return ( +
+
+ {imageAlt +
+
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} + {children} + {actions &&
{actions}
} +
+ {footer &&
{footer}
} +
+ ); + } + + return ( +
+ {image && ( +
+ {imageAlt +
+ )} +
+ {title &&

{title}

} + {subtitle &&

{subtitle}

} + {children} + {actions &&
{actions}
} +
+ {footer &&
{footer}
} +
+ ); +}; + +export default Card; diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx new file mode 100644 index 00000000..4375ca20 --- /dev/null +++ b/src/components/input/NumberInput.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { ChangeEvent } from 'react'; +import { NumericFormat, OnValueChange } from 'react-number-format'; +import TextInput, { TextInputProps } from '@/components/input/TextInput'; + +interface NumberInputProps extends Omit { + thousandSeparator?: string; + decimalSeparator?: string; + decimalScale?: number; + allowNegative?: boolean; + prefix?: string; + suffix?: string; + fixedDecimalScale?: boolean; +} + +const NumberInput = ({ + thousandSeparator = ',', + decimalSeparator = '.', + decimalScale = 5, + allowNegative = true, + onChange, + ...restProps +}: NumberInputProps) => { + const valueChangeHandler: OnValueChange = ( + numberFormatValues, + sourceInfo + ) => { + const newChangeEvent = sourceInfo.event as + | ChangeEvent + | undefined; + + if (newChangeEvent) { + newChangeEvent.target.value = numberFormatValues.value; + + onChange?.(newChangeEvent); + } + }; + + return ( + + ); +}; + +export default NumberInput; diff --git a/src/components/pages/production/chickin/ChickinTable.tsx b/src/components/pages/production/chickin/ChickinTable.tsx index 2b3fdc07..65ab3c16 100644 --- a/src/components/pages/production/chickin/ChickinTable.tsx +++ b/src/components/pages/production/chickin/ChickinTable.tsx @@ -11,8 +11,8 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; -import { cn } from '@/lib/helper'; -import { ChickinApi, ProjectFlockApi } from '@/services/api/production'; +import { cn, formatNumber } from '@/lib/helper'; +import { ChickinApi } from '@/services/api/production'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Chickin } from '@/types/api/production/chickin'; import { Icon } from '@iconify/react'; @@ -124,6 +124,13 @@ const ChickinTable = () => { { accessorFn: (row) => row.quantity, header: 'Jumlah Chickin', + cell: (props) => { + if (props.row.original.quantity) { + return formatNumber(props.row.original.quantity); + } else { + return '-'; + } + } }, { accessorFn: (row) => row.chick_in_date, @@ -159,7 +166,7 @@ const ChickinTable = () => { deleteModal.openModal(); }; - const editClickHandler = () => { + const editClickHandler = () => { setSelectedChickin(props.row.original); chickinModal.openModal(); }; @@ -279,7 +286,7 @@ const RowOptionsMenu = ({ 'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow' )} > - {/* */} +