From 885e4250fd3c9a3cde419030e1f232eccd001960 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 5 Dec 2025 22:55:11 +0700 Subject: [PATCH] feat(FE-279): Add functionality closing project flock --- .../project-flock/chickin/add/page.tsx | 2 +- .../production/project-flock/chickin/page.tsx | 2 +- .../project-flock/closing/layout.tsx | 11 + .../production/project-flock/closing/page.tsx | 63 ++++ src/app/production/project-flock/layout.tsx | 3 +- src/components/helper/RequireAuth.tsx | 199 ++++++++++-- src/components/helper/drawer/DrawerHeader.tsx | 104 ++++++ src/components/input/RadioInput.tsx | 249 ++++++++++----- .../form/InventoryAdjustmentForm.tsx | 6 +- .../production/chickin/form/ChickinForm.tsx | 2 +- .../chickin/ProjectFlockChickinDetail.tsx | 301 +++++++++++++++++- .../closing/ProjectFlockClosingForm.tsx | 297 +++++++++++++++++ .../detail/ProjectFlockDetail.tsx | 188 +++++++---- .../project-flock/form/ProjectFlockForm.tsx | 57 ++-- .../api/production/project-flock-kandang.ts | 183 ++++++++++- .../api/production/project-flock-kandang.d.ts | 22 ++ 16 files changed, 1464 insertions(+), 225 deletions(-) create mode 100644 src/app/production/project-flock/closing/layout.tsx create mode 100644 src/app/production/project-flock/closing/page.tsx create mode 100644 src/components/helper/drawer/DrawerHeader.tsx create mode 100644 src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx diff --git a/src/app/production/project-flock/chickin/add/page.tsx b/src/app/production/project-flock/chickin/add/page.tsx index bcb4d612..831979cb 100644 --- a/src/app/production/project-flock/chickin/add/page.tsx +++ b/src/app/production/project-flock/chickin/add/page.tsx @@ -10,7 +10,7 @@ const AddChickin = () => { return ( <> -
+
diff --git a/src/app/production/project-flock/chickin/page.tsx b/src/app/production/project-flock/chickin/page.tsx index 5d105aab..d40c39a3 100644 --- a/src/app/production/project-flock/chickin/page.tsx +++ b/src/app/production/project-flock/chickin/page.tsx @@ -2,7 +2,7 @@ import ChickinTable from '@/components/pages/production/chickin/ChickinTable'; const Chickin = () => { return ( -
+
); diff --git a/src/app/production/project-flock/closing/layout.tsx b/src/app/production/project-flock/closing/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/production/project-flock/closing/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/project-flock/closing/page.tsx b/src/app/production/project-flock/closing/page.tsx new file mode 100644 index 00000000..d734f669 --- /dev/null +++ b/src/app/production/project-flock/closing/page.tsx @@ -0,0 +1,63 @@ +'use client'; +import ProjectFlockClosingForm from '@/components/pages/production/project-flock/closing/ProjectFlockClosingForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { ProjectFlockKandangApi } from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +const ProjectFlockClosingPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const projectFlockId = searchParams.get('projectFlockId'); + const projectFlockKandangId = searchParams.get('projectFlockKandangId'); + + const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } = + useSWR(projectFlockKandangId, (id: number) => + ProjectFlockKandangApi.getSingle(id) + ); + + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); + + if (!projectFlockId || !projectFlockKandangId) { + router.back(); + + return ( +
+ +
+ ); + } + + if ( + !isLoadingProjectFlock && + (!projectFlock || isResponseError(projectFlock)) && + !isLoadingProjectFlockKandang && + (!projectFlockKandang || isResponseError(projectFlockKandang)) + ) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingProjectFlock || + (isLoadingProjectFlockKandang && ( + + ))} + {isResponseSuccess(projectFlock) && + isResponseSuccess(projectFlockKandang) && ( + + )} +
+ ); +}; + +export default ProjectFlockClosingPage; diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index f441abad..698064cf 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -19,8 +19,9 @@ export default function ProjectFlockLayout({ const isEdit = pathname.includes('/detail/edit'); const isDetail = pathname.includes('/detail'); const isChickin = pathname.includes('/chickin/add/kandang'); + const isClosing = pathname.includes('/closing'); - const isOpen = isAdd || isEdit || isDetail || isChickin; + const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing; const handleBackdropClick = () => { const unsub = useUiStore.getState().subscribeIsValid((isValid) => { diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..dbd4b6bc 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; -import { AxiosError } from 'axios'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { GetMeResponse } from '@/types/api/api-general'; + +// TODO: delete this later, DONT HARDCODE USER DATA +const DUMMY_USER = { + id: 1, + email: 'admin@mbugroup.id', + npk: '0001', + name: 'Super Admin', + image: null, + created_at: '2025-09-30T03:24:20.899229Z', + updated_at: '2025-09-30T03:24:20.899229Z', + roles: [ + { + id: 1, + key: 'mbu.super_admin', + name: 'MBU Administrator', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + permissions: [ + { + id: 1, + name: 'mbu:purchase:read', + action: 'read', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 2, + name: 'mbu:purchase:create', + action: 'create', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 3, + name: 'mbu:purchase:approve', + action: 'approve', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + ], + }, + { + id: 2, + key: 'lti.super_admin', + name: 'LTI Administrator', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + permissions: [ + { + id: 4, + name: 'lti:purchase:read', + action: 'read', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 5, + name: 'lti:purchase:create', + action: 'create', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 6, + name: 'lti:purchase:approve', + action: 'approve', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + ], + }, + { + id: 3, + key: 'manbu.super_admin', + name: 'MANBU Administrator', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + permissions: [ + { + id: 7, + name: 'manbu:purchase:read', + action: 'read', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 8, + name: 'manbu:purchase:create', + action: 'create', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 9, + name: 'manbu:purchase:approve', + action: 'approve', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + ], + }, + ], +}; interface RequireAuthProps { children?: ReactNode; @@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { - data: userResponse, - isLoading: isLoadingUserResponse, - error: userErrorResponse, - } = useSWRImmutable< - GetMeResponse & { ok?: boolean }, - AxiosError, - SWRHttpKey - >('/sso/userinfo', httpClientFetcher, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - }); + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/sso/userinfo', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/components/helper/drawer/DrawerHeader.tsx b/src/components/helper/drawer/DrawerHeader.tsx new file mode 100644 index 00000000..f9d70a04 --- /dev/null +++ b/src/components/helper/drawer/DrawerHeader.tsx @@ -0,0 +1,104 @@ +'use client'; + +import { Icon } from '@iconify/react'; +import Link from 'next/link'; +import { ReactNode } from 'react'; +import { cn } from '@/lib/helper'; + +export interface DrawerHeaderProps { + // Left side props + leftIcon?: string; + leftIconSize?: number; + leftIconHref?: string; + leftIconOnClick?: () => void; + leftIconClassName?: string; + + // Subtitle/label props + subtitle?: string | ReactNode; + subtitleClassName?: string; + + // Right side actions (children) + children?: ReactNode; + + // Container props + className?: string; + showDivider?: boolean; +} + +const DrawerHeader = ({ + leftIcon = 'mdi:close', + leftIconSize = 24, + leftIconHref, + leftIconOnClick, + leftIconClassName, + subtitle, + subtitleClassName, + children, + className, + showDivider = true, +}: DrawerHeaderProps) => { + const renderLeftIcon = () => { + const iconElement = ( + + ); + + if (leftIconHref) { + return ( + + {iconElement} + + ); + } + + if (leftIconOnClick) { + return ( + + ); + } + + return iconElement; + }; + + return ( +
+ {/* Left Side */} +
+ {renderLeftIcon()} + + {showDivider && subtitle && ( +
+ )} + + {subtitle && ( +
+ {subtitle} +
+ )} +
+ + {/* Right Side Actions */} + {children && ( +
+ {children} +
+ )} +
+ ); +}; + +export default DrawerHeader; diff --git a/src/components/input/RadioInput.tsx b/src/components/input/RadioInput.tsx index 71a731aa..e508e7ba 100644 --- a/src/components/input/RadioInput.tsx +++ b/src/components/input/RadioInput.tsx @@ -1,6 +1,11 @@ 'use client'; -import { ChangeEventHandler, ReactNode } from 'react'; +import { + ChangeEventHandler, + ReactNode, + createContext, + useContext, +} from 'react'; import { cn } from '@/lib/helper'; export interface RadioOption { @@ -8,37 +13,74 @@ export interface RadioOption { value: string; } -export interface RadioInputProps { - label?: string; - bottomLabel?: string; +// DaisyUI Radio Colors +export type RadioColor = + | 'neutral' + | 'primary' + | 'secondary' + | 'accent' + | 'success' + | 'warning' + | 'info' + | 'error'; + +// DaisyUI Radio Sizes +export type RadioSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +// Context untuk RadioGroup +interface RadioGroupContextValue { name: string; value?: string; - options: RadioOption[]; - variant?: string; - className?: { - wrapper?: string; - label?: string; - radioWrapper?: string; - radio?: string; - }; - isError?: boolean; - isValid?: boolean; - errorMessage?: string; - required?: boolean; + color?: RadioColor; + size?: RadioSize; disabled?: boolean; - startAdornment?: ReactNode; - endAdornment?: ReactNode; onChange?: ChangeEventHandler; onBlur?: (e: React.FocusEvent) => void; } -const RadioInput = ({ +const RadioGroupContext = createContext( + undefined +); + +const useRadioGroup = () => { + const context = useContext(RadioGroupContext); + if (!context) { + throw new Error('RadioGroupItem must be used within RadioGroup'); + } + return context; +}; + +// RadioGroup Component +export interface RadioGroupProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string; + options?: RadioOption[]; + color?: RadioColor; + size?: RadioSize; + className?: { + wrapper?: string; + label?: string; + radioWrapper?: string; + }; + isError?: boolean; + errorMessage?: string; + required?: boolean; + disabled?: boolean; + onChange?: ChangeEventHandler; + onBlur?: (e: React.FocusEvent) => void; + children?: ReactNode; +} + +export const RadioGroup = ({ label, bottomLabel, name, value, options, - variant = 'radio-primary', + color = 'primary', + size = 'md', className, isError, errorMessage, @@ -46,68 +88,125 @@ const RadioInput = ({ disabled = false, onChange, onBlur, -}: RadioInputProps) => { - return ( -
- {/* Label atas */} - {label && ( - - )} + children, +}: RadioGroupProps) => { + const contextValue: RadioGroupContextValue = { + name, + value, + color, + size, + disabled, + onChange, + onBlur, + }; - {/* Daftar opsi radio */} -
- {options.map((option) => ( + return ( + +
+ {/* Label atas */} + {label && ( - ))} + )} + + {/* Daftar opsi radio */} +
+ {/* Jika options diberikan, render otomatis */} + {options && + options.map((option) => ( + + ))} + + {/* Atau gunakan children untuk custom rendering */} + {children} +
+ + {/* Label bawah */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + + {/* Pesan error */} + {isError && errorMessage && ( +

{errorMessage}

+ )}
- - {/* Label bawah */} - {!isError && bottomLabel && ( -

{bottomLabel}

- )} - - {/* Pesan error */} - {isError && errorMessage && ( -

{errorMessage}

- )} -
+ ); }; -export default RadioInput; +// RadioGroupItem Component +export interface RadioGroupItemProps { + value: string; + label?: string; + className?: string; + disabled?: boolean; + color?: RadioColor; + size?: RadioSize; +} + +export const RadioGroupItem = ({ + value, + label, + className, + disabled: itemDisabled, + color: itemColor, + size: itemSize, +}: RadioGroupItemProps) => { + const { + name, + value: groupValue, + color: groupColor, + size: groupSize, + disabled: groupDisabled, + onChange, + onBlur, + } = useRadioGroup(); + + const isDisabled = itemDisabled ?? groupDisabled; + const radioColor = itemColor ?? groupColor; + const radioSize = itemSize ?? groupSize; + + return ( + + ); +}; diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 44faaf6d..2c6c463c 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -24,7 +24,7 @@ import Button from '@/components/Button'; import { Icon } from '@iconify/react'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import TextInput from '@/components/input/TextInput'; -import RadioInput from '@/components/input/RadioInput'; +import { RadioGroup } from '@/components/input/RadioInput'; import TextArea from '@/components/input/TextArea'; interface InventoryAdjustmentFormProps { @@ -347,7 +347,7 @@ const InventoryAdjustmentForm = ({ /> {/* Radio Button Flag Stock */} - {approvals && !approvalsLoading && ( diff --git a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx index 3028edfd..3b2b8f45 100644 --- a/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx +++ b/src/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail.tsx @@ -10,7 +10,7 @@ import SelectInput, { import PillBadge from '@/components/PillBadge'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; -import { cn } from '@/lib/helper'; +import { cn, formatDate, formatTitleCase } from '@/lib/helper'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { ProjectFlockKandangApi } from '@/services/api/production'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -21,6 +21,7 @@ import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { FormHeader } from '@/components/helper/form/FormHeader'; +import Link from 'next/link'; const ProjectFlockChickinDetail = ({ projectFlockId, @@ -101,11 +102,26 @@ const ProjectFlockChickinDetail = ({ }, [projectFlockId, listProjectFlock]); return ( <> - +
+ + + +
+
+ Chick In {projectFlock?.flock_name} +
+
+
+ {/* -
+ backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`} + /> */} + {/*
-
- */} + {/* Informasi Umum */} + {projectFlock && ( +
+
+

Informasi Umum

+ {/* Badge Row */} +
+ = 3 + ? 'error' + : undefined + } + className={{ + badge: 'rounded-lg px-2', + }} + > + = 3 + ? 'error' + : undefined + } + />{' '} + {projectFlock.approval.step_name} + +
+ + + {` ${formatTitleCase(projectFlock.category)}`} + +
+ {/* Information Grid */} +
+
+ Submitted +
+
+ + {' '} + {projectFlock.created_user.name} + +
+ +
+ History +
+
+ +
+ + {/* BARIS 1 */} +
+ Area +
+
{projectFlock.area.name}
+ + {/* BARIS 2 */} +
+ Lokasi +
+
{projectFlock.location.name}
+ +
+ FCR +
+
{projectFlock.fcr.name}
+ + {/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */} +
+ {' '} + Kategori +
+
+ {formatTitleCase(projectFlock.category)} +
+
+
+
+ )} + {/* - - */} + {/* Card Kandangs */} +
+
+

Daftar Kandang

+ {isResponseSuccess(listProjectFlock) ? ( + <> + {/* Badge Row */} +
+ + {' '} + Disetujui ( + {isResponseSuccess(listProjectFlockKandang) && + listProjectFlockKandang.data.filter( + (k) => k.approval?.step_number == 1 + ).length} + ) + +
+ + {' '} + Pengajuan ( + {isResponseSuccess(listProjectFlockKandang) && + listProjectFlockKandang.data.filter( + (k) => k.approval?.step_number == 2 + ).length} + ) + +
+ + + Belum Chickin ( + {isResponseSuccess(listProjectFlockKandang) && + listProjectFlockKandang.data.filter( + (k) => k.approval == null + ).length} + ) + +
+ {/* Card Kandang */} + +
+ {isResponseSuccess(listProjectFlockKandang) && + listProjectFlockKandang.data.map((kandang) => ( +
+
+ + + + + {kandang.kandang.name} + +
+ +
+ ))} +
+
+ + ) : ( +
+ + Pilih project flock terlebih dahulu... + +
+ )} +
+
+ {/* - +
*/} ); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx new file mode 100644 index 00000000..a078ed85 --- /dev/null +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -0,0 +1,297 @@ +'use client'; +import Button from '@/components/Button'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import Table from '@/components/Table'; +import Badge from '@/components/Badge'; +import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { + ClosingExpense, + ProjectFlockKandang, +} from '@/types/api/production/project-flock-kandang'; +import { Purchase } from '@/types/api/purchase/purchase'; +import { Icon } from '@iconify/react'; +import useSWR from 'swr'; +import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import { useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; + +const ProjectFlockClosingForm = ({ + projectFlock, + projectFlockKandang, +}: { + projectFlock: ProjectFlock; + projectFlockKandang: ProjectFlockKandang; +}) => { + const closeModal = useModal(); + const isCanClose = projectFlock.approval.step_number <= 2; + const [isClosingLoading, setIsClosingLoading] = useState(false); + + const { data: closingData, isLoading } = useSWR( + `${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`, + () => ProjectFlockKandangApi.checkClosing(projectFlockKandang.id) + ); + + const confirmationModalCloseClickHandler = async () => { + setIsClosingLoading(true); + const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( + projectFlock?.id as number, + { + closed_date: formatDate(new Date(), 'yyyy-MM-dd'), + action: isCanClose ? 'close' : 'unclose', + } + ); + + if (isResponseSuccess(deleteProjectFlockRes)) { + toast.success(deleteProjectFlockRes?.message as string); + } + if (isResponseError(deleteProjectFlockRes)) { + toast.error(deleteProjectFlockRes?.message as string); + } + setIsClosingLoading(false); + closeModal.closeModal(); + }; + + const errorStock = useMemo(() => { + return isResponseSuccess(closingData) + ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) + : false; + }, [closingData]); + + const errorExpense = useMemo(() => { + return isResponseSuccess(closingData) + ? closingData?.data?.expenses.every((expense) => expense.step < 5) + : false; + }, [closingData]); + + const isCanCloseValid = !errorStock && !errorExpense; + + return ( + <> + + + {/* Informasi Kandang */} +
+
+

Informasi Kandang

+ + {/* Badge Row */} +
+ + {' '} + Aktif + +
+ + + {` Kapasitas ${formatNumber(projectFlockKandang.kandang.capacity)} Ekor`} + +
+ + {/* Information Grid */} +
+ {/* Area */} +
+ Area +
+
{projectFlock.area.name}
+ + {/* Lokasi */} +
+ Lokasi +
+
{projectFlock.location.name}
+ + {/* Kandang */} +
+ Kandang +
+
{projectFlockKandang.kandang.name}
+ + {/* Jumlah DOC */} +
+ Jumlah DOC +
+
+ {formatNumber( + projectFlockKandang.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
+
+ + {/* Table Biaya */} +
+
+

Biaya

+ + data={ + isResponseSuccess(closingData) ? closingData.data?.expenses : [] + } + columns={[ + { + header: 'PO Number', + accessorKey: 'po_number', + }, + { + header: 'Total', + accessorKey: 'total', + }, + { + header: 'Status', + accessorKey: 'status', + cell(props) { + return ( + + {formatTitleCase(props.row.original.status)} + + ); + }, + }, + ]} + className={{ + containerClassName: cn('my-4'), + tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', + tableClassName: 'font-inter w-full table-sm min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-3 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-3 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + /> + {errorExpense && ( +
+ *Pastikan semua biaya sudah selesai sebelum melakukan closing. +
+ )} +
+ + {/* Table Persediaan Gudang */} +
+
+

Persediaan Gudang

+ + data={ + isResponseSuccess(closingData) + ? closingData.data?.stock_remaining + : [] + } + columns={[ + { + header: 'Product', + accessorKey: 'product.name', + }, + { + header: 'Kategori', + accessorKey: 'product.product_category.name', + }, + { + header: 'Quantity', + accessorKey: 'quantity', + }, + { + header: 'UOM', + accessorKey: 'product.uom.name', + }, + ]} + className={{ + containerClassName: cn('my-4'), + tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', + tableClassName: 'font-inter w-full table-sm min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-3 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-3 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + /> + {errorStock && ( +
+ *Masih ada sisa stock yang belum dihabiskan. +
+ )} +
+ +
+ +
+ + + + ); +}; + +export default ProjectFlockClosingForm; diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index fc1a87d3..17272d20 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -1,7 +1,9 @@ import Badge from '@/components/Badge'; import Button from '@/components/Button'; import Card from '@/components/Card'; +import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput'; import Tooltip from '@/components/Tooltip'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { formatCurrency, formatDate, @@ -13,6 +15,11 @@ import { Icon } from '@iconify/react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import toast from 'react-hot-toast'; const ProjectFlockDetail = ({ projectFlock, @@ -20,55 +27,60 @@ const ProjectFlockDetail = ({ projectFlock: ProjectFlock; }) => { const router = useRouter(); + const deleteModal = useModal(); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [openBudgets, setOpenBudget] = useState(false); + const [selectedKandangId, setSelectedKamdangId] = useState( + null + ); + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + const deleteProjectFlockRes = await ProjectFlockApi.delete( + projectFlock?.id as number + ); + + if (isResponseSuccess(deleteProjectFlockRes)) { + toast.success(deleteProjectFlockRes?.message as string); + router.push('/production/project-flock'); + } + if (isResponseError(deleteProjectFlockRes)) { + toast.error(deleteProjectFlockRes?.message as string); + } + setIsDeleteLoading(false); + }; return ( <>
{/* Header */} -
-
- - - -
-
- Created On {formatDate(projectFlock.created_at, 'MMM DD, YYYY')} -
-
-
- {projectFlock?.approval?.step_number == 2 && ( - - - - - - )} - - + + + + -
-
+ + + + + {/* Informasi Umum */}
@@ -79,11 +91,11 @@ const ProjectFlockDetail = ({ = 3 + : projectFlock.approval?.step_number >= 3 ? 'error' : undefined } @@ -96,16 +108,16 @@ const ProjectFlockDetail = ({ width={12} height={12} color={ - projectFlock.approval.step_number == 1 + projectFlock.approval?.step_number == 1 ? 'neutral' - : projectFlock.approval.step_number == 2 + : projectFlock.approval?.step_number == 2 ? 'success' - : projectFlock.approval.step_number >= 3 + : projectFlock.approval?.step_number >= 3 ? 'error' : undefined } />{' '} - {projectFlock.approval.step_name} + {projectFlock.approval?.step_name}
-
+ setSelectedKamdangId(e.target.value)} + value={selectedKandangId?.toString()} + size='md' + color='neutral' + > {projectFlock.kandangs.map((kandang) => (
setSelectedKamdangId(kandang.id.toString())} > -
- {' '} - {kandang.name} -
-
- Created On{' '} - {formatDate(projectFlock.created_at, 'MMM DD, YYYY')} + +
+ + Kapasitas {kandang.capacity} Ekor +
))} -
+
+
+ + + + + + +
+ + ); }; diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 38a57844..208e7894 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -48,6 +48,8 @@ import ProjectFlockKandangTable from '@/components/pages/production/project-floc import { Nonstock } from '@/types/api/master-data/nonstock'; import { useUiStore } from '@/stores/ui/ui.store'; import Link from 'next/link'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { formatDate } from '@/lib/helper'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; @@ -675,28 +677,20 @@ const ProjectFlockForm = ({ <>
{/* Header */} -
-
- - - -
-
- {formType == 'add' ? 'Add Flock' : 'Update Flock'} -
-
-
+ + {formType == 'edit' && ( -
-
+ )} + {projectFlockFormErrorMessage && (
@@ -770,21 +764,6 @@ const ProjectFlockForm = ({ Reject - {initialValues?.approval?.step_number == 2 && ( - - )}
)}
('project-flock-kandang'); +> { + constructor(basePath: string = '') { + super(basePath); + } + + /** + * Close or Unclose Project Flock Kandang + */ + async closing( + id: number, + payload: ClosingProjectFlockKandangPayload + ): Promise | undefined> { + try { + const path = `${this.basePath}/${id}/closing`; + + const headers = { + 'Content-Type': 'application/json', + ...(this.header ?? {}), + }; + + return await httpClient>(path, { + method: 'POST', + body: payload, + headers, + }); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + + /** + * Check Closing Requirements for Project Flock Kandang + * TODO: Replace with actual API call when backend is ready + */ + async checkClosing( + id: number + ): Promise | undefined> { + // Dummy data - replace with actual API call when backend is ready + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + status: 'success', + message: 'Cek persyaratan closing kandang', + data: { + unfinished_expenses: 2, + stock_remaining: [ + { + id: 1, + product_id: 1, + warehouse_id: 1, + quantity: 0, + product: { + id: 1, + name: 'Pakan Starter', + brand: 'Brand A', + sku: 'PKN-STR-001', + product_price: 15000, + selling_price: 17000, + tax: 0, + expiry_period: 365, + flags: ['active'], + uom: { + id: 1, + name: 'Kg', + created_user: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2024-01-01', + updated_at: '2024-01-01', + }, + product_category: { + id: 1, + name: 'Pakan', + code: 'PKN', + created_user: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2024-01-01', + updated_at: '2024-01-01', + }, + suppliers: [], + created_user: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2024-01-01', + updated_at: '2024-01-01', + }, + warehouse: { + id: 1, + name: 'Gudang Utama', + type: 'AREA', + area: { + id: 1, + name: 'Area 1', + }, + created_user: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2024-01-01', + updated_at: '2024-01-01', + }, + created_user: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-01-01', + updated_at: '2025-01-01', + }, + ], + expenses: [ + { + id: 1, + po_number: 'PO-BOP-LTI-00001', + category: 'NON-BOP', + total: 110000, + status: 'SELESAI', + step_name: 'Approval Finance', + step: 5, + reference_number: 'BOP-LTI-00001', + }, + { + id: 3, + po_number: 'PO-BOP-LTI-00003', + category: 'BOP', + total: 110000, + status: 'SELESAI', + step_name: 'Approval Finance', + step: 5, + reference_number: 'BOP-LTI-00003', + }, + ], + }, + }); + }, 500); // Simulate network delay + }); + + /* + // Original API call - uncomment when backend is ready + try { + const path = `${this.basePath}/${id}/closing/check`; + + return await httpClient>(path, { + method: 'GET', + }); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + */ + } +} + +export const ProjectFlockKandangApi = new ProjectFlockKandangService( + '/production/project-flock-kandangs' +); diff --git a/src/types/api/production/project-flock-kandang.d.ts b/src/types/api/production/project-flock-kandang.d.ts index b7b22b99..388eed32 100644 --- a/src/types/api/production/project-flock-kandang.d.ts +++ b/src/types/api/production/project-flock-kandang.d.ts @@ -39,3 +39,25 @@ export type LookupProjectFlockKandangPayload = { project_flock_id: number; kandang_id: number; }; + +export type ClosingProjectFlockKandangPayload = { + action: 'close' | 'unclose'; + closed_date?: string; // YYYY-MM-DD, DD-MM-YYYY, or RFC3339 +}; + +export type ClosingExpense = { + id: number; + po_number: string; + category: string; + total: number; + status: string; + step_name: string; + step: number; + reference_number: string; +}; + +export type CheckClosingResponse = { + unfinished_expenses: number; + stock_remaining: ProductWarehouse[]; + expenses: ClosingExpense[]; +};