From 4b6144d0b49d7e3c2246e0c6f8828b00427cdd93 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 27 Nov 2025 13:36:12 +0700 Subject: [PATCH 001/105] refactor(FE-Storyless): update import paths for schema files to use absolute paths --- src/components/Card.tsx | 2 +- .../pages/production/recording/form/RecordingForm.tsx | 2 +- .../pages/production/recording/grading/form/GradingForm.tsx | 2 +- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 2 +- .../purchase/form/order/PurchaseOrderStaffApprovalForm.tsx | 2 +- .../pages/purchase/form/request/PurchaseRequestForm.tsx | 3 +-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index d3ff80b1..ff4c35f2 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -4,7 +4,7 @@ import { HTMLAttributes, ReactNode, useState } from 'react'; import { cn } from '@/lib/helper'; import Image from 'next/image'; -import Collapse from './Collapse'; +import Collapse from '@/components/Collapse'; import { Icon } from '@iconify/react'; export interface CardProps diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5900c84a..52297258 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -48,7 +48,7 @@ import { getRecordingLayingFormInitialValues, UpdateRecordingGrowingFormSchema, UpdateRecordingLayingFormSchema, -} from './RecordingForm.schema'; +} from '@/components/pages/production/recording/form/RecordingForm.schema'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 9c3ba37a..1e91c78d 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -28,7 +28,7 @@ import { RecordingGradingFormValues, UpdateRecordingGradingFormSchema, getRecordingGradingFormInitialValues, -} from '../../form/RecordingForm.schema'; +} from '@/components/pages/production/recording/form/RecordingForm.schema'; import { cn, formatDate } from '@/lib/helper'; import toast from 'react-hot-toast'; diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 7909ade9..79762da9 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -18,7 +18,7 @@ import { PurchaseRequestAcceptApprovalFormDefaultValues, PurchaseRequestAcceptApprovalFormInitialValues, PurchaseRequestAcceptApprovalFormSchema, -} from './PurchaseOrderForm.schema'; +} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema'; import { isResponseError } from '@/lib/api-helper'; import { PurchaseApi } from '@/services/api/purchase'; import { diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 63756ad9..69c3fd13 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -21,7 +21,7 @@ import { PurchaseRequestStaffApprovalFormInitialValues, PurchaseRequestStaffApprovalFormSchema, PurchaseStaffApprovalItemSchema, -} from './PurchaseOrderForm.schema'; +} from '@/components/pages/purchase/form/order/PurchaseOrderForm.schema'; import { isResponseError } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; import { PurchaseApi } from '@/services/api/purchase'; diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 7100b134..396ce7bb 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -22,13 +22,12 @@ import { PurchaseRequestFormValues, getPurchaseRequestFormInitialValues, UpdatePurchaseRequestFormSchema, -} from './PurchaseRequestForm.schema'; +} from '@/components/pages/purchase/form/request/PurchaseRequestForm.schema'; import { SupplierApi, AreaApi, LocationApi, WarehouseApi, - ProductApi, } from '@/services/api/master-data'; import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; From 7a76719547358f67d25a2d285772f5ad099d450d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 27 Nov 2025 13:46:53 +0700 Subject: [PATCH 002/105] refactor(FE-Storyless): remove console, window and err catch --- .../pages/production/recording/form/RecordingForm.tsx | 3 +-- .../production/recording/grading/form/GradingForm.tsx | 3 +-- .../form/order/PurchaseOrderStaffApprovalForm.tsx | 3 +-- .../pages/purchase/order/PurchaseOrderInvoice.tsx | 8 ++++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 52297258..43ffc98b 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2924,8 +2924,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, 1000); } } - } catch (error) { - console.error('Error creating recording:', error); + } catch { toast.error( 'Gagal membuat recording. Silakan coba lagi.' ); diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 1e91c78d..417c6356 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -173,8 +173,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { deleteModal.closeModal(); toast.success(res?.message || 'Successfully delete Grading!'); router.push('/production/recording'); - } catch (err) { - console.error(err); + } catch { setGradingFormErrorMessage('Failed to delete Grading'); } finally { setIsDeleteLoading(false); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 69c3fd13..791e2592 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -241,9 +241,8 @@ const PurchaseOrderStaffApprovalForm = ({ ); formik.setFieldValue('items', updatedPurchaseItems); } - } catch (error) { + } catch { toast.error('Terjadi kesalahan saat menghapus item pembelian'); - console.error('Delete item error:', error); } }, [ initialValues?.id, diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index d7497d7e..36aea9c7 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -12,6 +12,7 @@ import { pdf, } from '@react-pdf/renderer'; import { Icon } from '@iconify/react'; +import toast from 'react-hot-toast'; import Button from '@/components/Button'; import { Purchase } from '@/types/api/purchase/purchase'; @@ -251,7 +252,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { const handleDownloadPDF = async () => { if (!purchaseData) { - alert('No purchase order data available'); + toast.error('No purchase order data available'); return; } @@ -502,9 +503,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); - } catch (error) { - console.error('Error generating PDF:', error); - alert('Failed to generate PDF. Please try again.'); + } catch { + toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); } From 892bb19dfdb81a03802fa91b21a86b70579382d6 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 28 Nov 2025 16:41:01 +0700 Subject: [PATCH 003/105] refactor(FE): change project flock form, detail and chickin view using drawer --- src/app/inventory/adjustment/detail/page.tsx | 5 - src/app/production/project-flock/add/page.tsx | 8 + src/app/production/project-flock/layout.tsx | 55 ++++ src/app/production/project-flock/page.tsx | 2 +- src/components/Drawer.tsx | 111 ++++++- .../pages/marketing/form/MarketingForm.tsx | 17 - .../supplier/form/SupplierForm.tsx | 1 - .../project-flock/ProjectFlockTable.tsx | 10 +- .../project-flock/form/ProjectFlockForm.tsx | 306 +++++++++--------- .../form/ProjectFlockKandangTable.tsx | 2 - src/services/api/expense.ts | 4 - 11 files changed, 325 insertions(+), 196 deletions(-) create mode 100644 src/app/production/project-flock/layout.tsx diff --git a/src/app/inventory/adjustment/detail/page.tsx b/src/app/inventory/adjustment/detail/page.tsx index acb9f8db..eb13647d 100644 --- a/src/app/inventory/adjustment/detail/page.tsx +++ b/src/app/inventory/adjustment/detail/page.tsx @@ -12,8 +12,6 @@ const DetailInventoryAdjustment = () => { // Ambil data dari router state useEffect(() => { - console.log('Router State'); - console.log(window.history.state); const state = window.history.state?.usr as | { inventoryAdjustment?: InventoryAdjustment } | undefined; @@ -26,9 +24,6 @@ const DetailInventoryAdjustment = () => { const finalData = inventoryAdjustment; - console.log('Final Data'); - console.log(finalData); - if (!finalData) { return (
diff --git a/src/app/production/project-flock/add/page.tsx b/src/app/production/project-flock/add/page.tsx index b323b5f3..726c6050 100644 --- a/src/app/production/project-flock/add/page.tsx +++ b/src/app/production/project-flock/add/page.tsx @@ -1,8 +1,16 @@ 'use client'; import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; +import React, { useImperativeHandle } from 'react'; +import toast from 'react-hot-toast'; const AddProjectFlock = () => { + // useImperativeHandle(ref, () => ({ + // validate() { + // toast.success('Validating'); + // return false; + // }, + // })); return (
diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx new file mode 100644 index 00000000..e7c45600 --- /dev/null +++ b/src/app/production/project-flock/layout.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; +import Drawer from '@/components/Drawer'; +import React, { ReactNode } from 'react'; +import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable'; + +export default function ProjectFlockLayout({ + children, +}: { + children: ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + + const isAdd = pathname.endsWith('/add'); + const isEdit = pathname.includes('/detail/edit'); + const isDetail = pathname.includes('/detail'); + const isChickin = pathname.includes('/chickin/add/kandang'); + + const isOpen = isAdd || isEdit || isDetail || isChickin; + + // const childRef = useRef(null); + + const handleBackdropClick = () => { + // const isValid = childRef.current?.validate(); // 🔥 trigger validation child + + // if (!isValid) { + // toast.error('Form belum valid, Drawer tidak bisa close'); + // return; + // } + router.push('/production/project-flock'); + }; + + return ( + <> + {/* List page always rendered */} +
+ +
+ + {/* Render Drawer only on /add */} + { + if (!v) router.push('/production/project-flock'); + }} + closeOnBackdropClick={false} + onBackdropClick={handleBackdropClick} + variant='right' + sidebarContent={isOpen &&
{children}
} + /> + + ); +} diff --git a/src/app/production/project-flock/page.tsx b/src/app/production/project-flock/page.tsx index 79feb41f..e93c6bc4 100644 --- a/src/app/production/project-flock/page.tsx +++ b/src/app/production/project-flock/page.tsx @@ -2,7 +2,7 @@ import ProjectFlockTable from '@/components/pages/production/project-flock/Proje const ProjectFlock = () => { return ( -
+
); diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx index f0efb417..17b8a56f 100644 --- a/src/components/Drawer.tsx +++ b/src/components/Drawer.tsx @@ -10,28 +10,102 @@ interface DrawerProps { open: boolean; setOpen: (newOpenState: boolean) => void; openOnLarge?: boolean; + variant?: 'sidebar' | 'left' | 'right'; + zIndex?: string; + className?: DrawerClassName; + onBackdropClick?: () => void; + closeOnBackdropClick?: boolean; } +type DrawerClassName = { + drawer?: string; + drawerContent?: string; + drawerSide?: string; + drawerOverlay?: string; + drawerSidebarContent?: string; +}; + const Drawer = ({ children, sidebarContent, open, setOpen, openOnLarge, + variant = 'sidebar', + zIndex = '20', + className, + onBackdropClick, + closeOnBackdropClick = true, }: DrawerProps) => { + const getDrawerClassNames = (): DrawerClassName => { + const baseClassNames = { + drawer: 'drawer', + drawerContent: 'drawer-content', + drawerSide: 'drawer-side', + drawerOverlay: 'drawer-overlay', + drawerSidebarContent: 'min-h-full bg-base-100', + }; + + if (variant === 'sidebar') { + return { + ...baseClassNames, + drawerSidebarContent: cn( + baseClassNames.drawerSidebarContent, + 'w-full max-w-[300px] lg:w-[300px]' + ), + }; + } else if (variant === 'right') { + return { + ...baseClassNames, + drawer: cn(baseClassNames.drawer, 'drawer-end'), + drawerSide: cn( + baseClassNames.drawerSide, + 'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21' + ), + drawerSidebarContent: cn( + baseClassNames.drawerSidebarContent, + 'w-full min-w-120 sm:w-fit' + ), + }; + } else if (variant === 'left') { + return { + ...baseClassNames, + drawerSide: cn( + baseClassNames.drawerSide, + 'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21' + ), + drawerSidebarContent: cn( + baseClassNames.drawerSidebarContent, + 'w-full min-w-120 sm:w-fit' + ), + }; + } + return baseClassNames; // Fallback for default or unknown variant + }; + + const varianClassName = getDrawerClassNames(); + const toggleDrawer = () => { setOpen(!open); }; const closeDrawer = () => { - setOpen(false); + if (closeOnBackdropClick) { + setOpen(false); + } + onBackdropClick && onBackdropClick(); }; return (
-
{children}
+ {/* Drawer Content */} +
+ {children} +
-
+ {/* Drawer Side */} +
+ + ); +}; + +export default SalesReportTable; From 411c2586f5c1e2bae689e84edeaab9a0150a6eb5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 3 Dec 2025 22:32:11 +0700 Subject: [PATCH 014/105] chore(format): prettier format --- .gitlab-ci.yml | 2 -- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6cf254e7..4a413bc4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -370,7 +370,7 @@ const RecordingTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); - const [approvalNotes, setApprovalNotes] = useState(''); + const [, setApprovalNotes] = useState(''); const singleDeleteModal = useModal(); const approveModal = useModal(); From 3e07316678a0dae4167da3cfb20a37b92e7bbd2b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 4 Dec 2025 02:05:34 +0700 Subject: [PATCH 015/105] feat(FE-328-329-330): Adding Feature Inventory Product Stocks --- .gitlab-ci.yml | 2 - src/app/inventory/product/detail/page.tsx | 50 ++++ src/app/inventory/product/page.tsx | 11 + src/components/helper/RequireAuth.tsx | 199 +++++++++++++--- .../adjustment/InventoryAdjustmentTable.tsx | 6 +- .../form/InventoryAdjustmentForm.tsx | 4 +- .../product/InventoryProductTable.tsx | 224 ++++++++++++++++++ .../product/detail/InventoryProductDetail.tsx | 125 ++++++++++ .../product/detail/StockLogTable.tsx | 81 +++++++ .../detail/StockProductWarehouseTable.tsx | 65 +++++ src/services/api/inventory.ts | 9 +- src/types/api/inventory/product.d.ts | 45 ++++ 12 files changed, 780 insertions(+), 41 deletions(-) create mode 100644 src/app/inventory/product/detail/page.tsx create mode 100644 src/app/inventory/product/page.tsx create mode 100644 src/components/pages/inventory/product/InventoryProductTable.tsx create mode 100644 src/components/pages/inventory/product/detail/InventoryProductDetail.tsx create mode 100644 src/components/pages/inventory/product/detail/StockLogTable.tsx create mode 100644 src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx create mode 100644 src/types/api/inventory/product.d.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - diff --git a/src/app/inventory/product/detail/page.tsx b/src/app/inventory/product/detail/page.tsx new file mode 100644 index 00000000..6daa7a86 --- /dev/null +++ b/src/app/inventory/product/detail/page.tsx @@ -0,0 +1,50 @@ +'use client'; + +import InventoryProductDetail from '@/components/pages/inventory/product/detail/InventoryProductDetail'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { InventoryProductApi } from '@/services/api/inventory'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +const InventoryProductDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const inventoryProductId = searchParams.get('inventoryProductId'); + + const { data: inventoryProduct, isLoading: isLoadingInventoryProduct } = + useSWR(inventoryProductId, (id: number) => + InventoryProductApi.getSingle(id) + ); + + if (!inventoryProductId) { + router.back(); + + return ( +
+ +
+ ); + } + + if ( + !isLoadingInventoryProduct && + (!inventoryProduct || isResponseError(inventoryProduct)) + ) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingInventoryProduct && ( + + )} + {!isLoadingInventoryProduct && isResponseSuccess(inventoryProduct) && ( + + )} +
+ ); +}; + +export default InventoryProductDetailPage; diff --git a/src/app/inventory/product/page.tsx b/src/app/inventory/product/page.tsx new file mode 100644 index 00000000..4815b8a1 --- /dev/null +++ b/src/app/inventory/product/page.tsx @@ -0,0 +1,11 @@ +import InventoryProductTable from '@/components/pages/inventory/product/InventoryProductTable'; + +const InventoryProductPage = () => { + return ( +
+ +
+ ); +}; + +export default InventoryProductPage; 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/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 049b0661..30807d1c 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -6,7 +6,7 @@ import Table from '@/components/Table'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; -import { inventoryAdjustmentApi } from '@/services/api/inventory'; +import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; import { Icon } from '@iconify/react'; @@ -41,8 +41,8 @@ const InventoryAdjustmentTable = () => { // Fetch Data const { data: inventoryAdjustments, isLoading } = useSWR( - `${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, - inventoryAdjustmentApi.getAllFetcher + `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, + InventoryAdjustmentApi.getAllFetcher ); // State diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index bbfb3154..44faaf6d 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -1,7 +1,7 @@ 'use client'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { inventoryAdjustmentApi } from '@/services/api/inventory'; +import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { CreateInventoryAdjustmentPayload, InventoryAdjustment, @@ -52,7 +52,7 @@ const InventoryAdjustmentForm = ({ const createInventoryAdjustmentHandler = useCallback( async (payload: CreateInventoryAdjustmentPayload) => { const createInventoryAdjustmentRes = - await inventoryAdjustmentApi.create(payload); + await InventoryAdjustmentApi.create(payload); if (isResponseError(createInventoryAdjustmentRes)) { setInventoryAdjustmentFormErrorMessage( diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx new file mode 100644 index 00000000..da660568 --- /dev/null +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -0,0 +1,224 @@ +'use client'; + +import Button from '@/components/Button'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import Table from '@/components/Table'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { ROWS_OPTIONS } from '@/config/constant'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency } from '@/lib/helper'; +import { InventoryProductApi } from '@/services/api/inventory'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { InventoryProduct } from '@/types/api/inventory/product'; +import { Icon } from '@iconify/react'; +import { + CellContext, + ColumnDef, + Row, + SortingState, +} from '@tanstack/react-table'; +import { ChangeEventHandler, useMemo, useState } from 'react'; +import useSWR from 'swr'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; +}) => ( + + + +); + +const InventoryProductTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + const [sorting, setSorting] = useState([]); + + const { data: inventoryProducts, isLoading } = useSWR( + `${InventoryProductApi.basePath}${getTableFilterQueryString()}`, + InventoryProductApi.getAllFetcher + ); + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + setPage(1); + }; + + const columns: ColumnDef[] = useMemo( + () => [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'product_price', + header: 'Harga Beli', + cell: (props) => { + return props.row.original.product_price + ? formatCurrency(props.row.original.product_price) + : '-'; + }, + }, + { + accessorKey: 'selling_price', + header: 'Harga Jual', + cell: (props) => { + return props.row.original.selling_price + ? formatCurrency(props.row.original.selling_price) + : '-'; + }, + }, + { + accessorFn: (row) => row.product_category.name, + header: 'Kategori', + }, + { + accessorFn: (row) => row.uom.name, + header: 'Satuan', + }, + { + header: 'Aksi', + cell: (props) => { + const currentPageSize = + props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ], + [] + ); + + return ( + <> +
+
+
+
+
+ +
+ + +
+
+ + + data={ + isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : [] + } + columns={columns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(inventoryProducts) + ? inventoryProducts?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(inventoryProducts) + ? inventoryProducts?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(inventoryProducts) && + inventoryProducts?.data?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + }} + /> +
+ + ); +}; + +export default InventoryProductTable; diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx new file mode 100644 index 00000000..59b1fed0 --- /dev/null +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -0,0 +1,125 @@ +import Card from '@/components/Card'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable'; +import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable'; +import { formatCurrency, formatNumber } from '@/lib/helper'; +import { InventoryProduct } from '@/types/api/inventory/product'; +import { useMemo } from 'react'; + +const InventoryProductDetail = ({ + inventoryProduct, + refresh, +}: { + inventoryProduct?: InventoryProduct; + refresh?: () => void; +}) => { + const totalStok = useMemo(() => { + return ( + inventoryProduct?.product_warehouses?.reduce( + (total, warehouse) => total + (warehouse.current_stock || 0), + 0 + ) || 0 + ); + }, [inventoryProduct]); + + const stockLogs = useMemo(() => { + return ( + inventoryProduct?.product_warehouses?.flatMap( + (warehouse) => warehouse.stock_logs || [] + ) || [] + ); + }, [inventoryProduct]); + + return ( +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
SKU:{inventoryProduct?.sku}
Nama Produk:{inventoryProduct?.name}
Kategory:{inventoryProduct?.product_category.name}
Satuan:{inventoryProduct?.uom.name}
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Harga Jual: + {inventoryProduct?.product_price + ? formatCurrency(inventoryProduct.product_price) + : '-'} +
Harga Beli: + {inventoryProduct?.selling_price + ? formatCurrency(inventoryProduct?.selling_price) + : '-'} +
Pajak: + {inventoryProduct?.tax + ? formatCurrency(inventoryProduct?.tax) + : '-'} +
Total Stok:{formatNumber(totalStok)}
+
+
+
+ + + + +
+ ); +}; + +export default InventoryProductDetail; diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx new file mode 100644 index 00000000..8c9e874f --- /dev/null +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -0,0 +1,81 @@ +import Card from '@/components/Card'; +import Table from '@/components/Table'; +import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; +import { StockLog } from '@/types/api/inventory/product'; + +const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => { + return ( + + + data={stockLogs} + columns={[ + { + header: 'ID', + accessorKey: 'id', + }, + { + header: 'Tanggal', + accessorKey: 'created_at', + cell: (props) => { + return formatDate(props.row.original.created_at, 'DD-MMM-yyyy'); + }, + }, + { + header: 'Peningkatan', + accessorKey: 'increase', + cell: (props) => { + return formatNumber(props.row.original.increase); + }, + }, + { + header: 'Penurunan', + accessorKey: 'decrease', + cell: (props) => { + return formatNumber(props.row.original.decrease); + }, + }, + { + header: 'Jenis Transaksi', + accessorKey: 'loggable_type', + cell: (props) => { + return props.row.original.loggable_type + ? formatTitleCase(props.row.original.loggable_type) + : '-'; + }, + }, + { + header: 'Catatan', + accessorKey: 'notes', + cell: (props) => { + return props.row.original.notes ? props.row.original.notes : '-'; + }, + }, + { + header: 'Oleh', + accessorKey: 'created_by', + }, + ]} + className={{ + containerClassName: 'mt-6', + 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 StockLogTable; diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx new file mode 100644 index 00000000..10343d8a --- /dev/null +++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx @@ -0,0 +1,65 @@ +import Card from '@/components/Card'; +import Table from '@/components/Table'; +import { formatNumber } from '@/lib/helper'; +import { + InventoryProduct, + ProductWarehouseStock, +} from '@/types/api/inventory/product'; + +const StockProductWarehouseTable = ({ + productWarehouseStock, +}: { + productWarehouseStock?: ProductWarehouseStock[]; +}) => { + return ( + + + data={productWarehouseStock ?? []} + columns={[ + { + header: 'Nama Gudang', + accessorKey: 'warehouse_name', + }, + { + header: 'Lokasi', + accessorKey: 'location', + cell: (props) => { + return Boolean(props.row.original.location) + ? props.row.original.location + : '-'; + }, + }, + { + header: 'Stok', + accessorFn(row) { + return row.current_stock; + }, + cell: (props) => { + return formatNumber(props.row.original.current_stock); + }, + }, + ]} + className={{ + containerClassName: 'mt-6', + 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 StockProductWarehouseTable; diff --git a/src/services/api/inventory.ts b/src/services/api/inventory.ts index e5d3adfc..fa406917 100644 --- a/src/services/api/inventory.ts +++ b/src/services/api/inventory.ts @@ -12,6 +12,7 @@ import { CreateInventoryAdjustmentPayload, InventoryAdjustment, } from '@/types/api/inventory/adjustment'; +import { InventoryProduct } from '@/types/api/inventory/product'; export const ProductWarehouseApi = new BaseApiService< ProductWarehouse, @@ -25,8 +26,14 @@ export const MovementApi = new BaseApiService< unknown >('/inventory/transfers'); -export const inventoryAdjustmentApi = new BaseApiService< +export const InventoryAdjustmentApi = new BaseApiService< InventoryAdjustment, CreateInventoryAdjustmentPayload, unknown >('/inventory/adjustments'); + +export const InventoryProductApi = new BaseApiService< + InventoryProduct, + unknown, + unknown +>('/inventory/product-stocks'); diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts new file mode 100644 index 00000000..85253e2a --- /dev/null +++ b/src/types/api/inventory/product.d.ts @@ -0,0 +1,45 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Uom } from '@/types/api/master-data/uom'; + +export type BaseInventoryProduct = { + id: number; + name: string; + brand: string; + sku: string; + product_price: number; + selling_price?: number; + tax?: number; + expiry_period?: number; + uom: Uom; + product_category: ProductCategory; + suppliers: Supplier[]; + flags: string[]; + product_warehouses?: ProductWarehouseStock[]; +}; + +export type ProductWarehouseStock = { + id: number; + product_id: number; + warehouse_id: number; + warehouse_name: string; + location: Location | string; + current_stock: number; + stock_logs: StockLog[]; +}; + +export type StockLog = { + id: number; + increase: number; + decrease: number; + loggable_type: string; + loggable_id: number; + notes: string; + product_warehouse_id: number; + created_by: number; + created_at: string; +}; + +export type InventoryProduct = BaseInventoryProduct & BaseMetadata; From 3b846bf11c4fa84cc4b016b49f237b3a8edc1310 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 4 Dec 2025 02:06:33 +0700 Subject: [PATCH 016/105] fix(FE): fixing error suspense layout --- src/app/inventory/product/detail/layout.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/inventory/product/detail/layout.tsx diff --git a/src/app/inventory/product/detail/layout.tsx b/src/app/inventory/product/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/inventory/product/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; From 991a594ee18a8d171aaa38c78b2de4acbf1fd97b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:51:11 +0700 Subject: [PATCH 017/105] refactor(FE-326): Add ClosingApiService and types for closing sales data --- src/services/api/closing.ts | 163 +++++++++++++++++++++++++++++ src/types/api/closing/closing.d.ts | 26 +++++ 2 files changed, 189 insertions(+) create mode 100644 src/services/api/closing.ts create mode 100644 src/types/api/closing/closing.d.ts diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts new file mode 100644 index 00000000..6dab6f8d --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,163 @@ +import { BaseApiService } from './base'; +import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { ClosingSales } from '@/types/api/closing/closing'; +import { sleep } from '@/lib/helper'; + +const adminUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', +}; + +const DUMMY_SALES: ClosingSales[] = [ + { + id: 1, + realization_date: '2025-01-15', + week_age: 4, + age_label: 'Week 4', + delivery_order_number: 'DO-2025-001', + product: { + id: 1, + name: 'Ayam Broiler', + brand: 'Brand A', + sku: 'AYM-001', + product_price: 20000, + selling_price: 25000, + tax: 10, + expiry_period: 30, + uom: { + id: 1, + name: 'Kg', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + suppliers: [], + flags: [], + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'LC001', + name: 'Live Chicken', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + customer: { + id: 1, + name: 'PT. Bumi Mandiri', + pic_id: 1, + pic: adminUser, + type: 'COMPANY', + address: 'Jl. Industri No. 123', + phone: '+62-21-1234567', + email: 'info@bumimandiri.com', + account_number: '1234567890', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + quantity: 1000, + weight: 1850, + average: 1.85, + price: 25000, + total: 25000000, + kandang: { + id: 1, + name: 'Singaparna 1', + status: 'ACTIVE', + location: { + id: 1, + name: 'Singaparna', + address: 'Jl. Singaparna No. 1', + area: { + id: 1, + name: 'Tasikmalaya', + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + capacity: 1000, + pic: { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin User', + }, + created_user: adminUser, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + kandang_id: 1, + payment_status: 'PAID', + created_user: adminUser, + created_at: '2025-01-15T10:00:00Z', + updated_at: '2025-01-15T10:00:00Z', + }, +]; + +export class ClosingApiService extends BaseApiService< + ClosingSales, + unknown, + unknown +> { + constructor(basePath: string = '') { + super(basePath); + } + + async getPenjualan( + id: number + ): Promise | undefined> { + try { + // TODO: Remove dummy data when real API is ready + await sleep(750); + + const saleData = DUMMY_SALES.find((sale) => sale.id === id); + + if (!saleData) { + return { + code: 404, + status: 'error', + message: 'Sales data not found!', + }; + } + + return { + code: 200, + status: 'success', + message: 'Successfully get sales data!', + meta: { + page: 1, + limit: 10, + total_pages: 1, + total_results: 1, + }, + data: saleData, + }; + + // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; + // return await this.customRequest>(getPenjualanPath); + } catch (error) { + console.error('Error fetching penjualan:', error); + return undefined; + } + } +} + +export const ClosingApi = new ClosingApiService('/closing'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts new file mode 100644 index 00000000..d4b94770 --- /dev/null +++ b/src/types/api/closing/closing.d.ts @@ -0,0 +1,26 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { ProductCategory } from '@type/api/master-data/product-category'; +import { Customer } from '@type/api/master-data/customer'; + +export type BaseClosingSales = { + id: number; + realization_date: string; + week_age: number; + age_label: string; + delivery_order_number: string; + product: Product; + product_category: ProductCategory; + customer: Customer; + quantity: number; + weight: number; + average: number; + price: number; + total: number; + kandang: Kandang; + kandang_id: number; + payment_status: string; +}; + +export type ClosingSales = BaseMetadata & BaseClosingSales; From 647b002065d77bbe201975331978eb5089aa681f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 11:55:57 +0700 Subject: [PATCH 018/105] refactor(FE-326): change SalesReportTable to use BaseClosingSales type and support initial values --- .../pages/closing/sale/SalesReportTable.tsx | 147 ++---------------- 1 file changed, 15 insertions(+), 132 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 525d1dcf..85fe50a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,25 +7,12 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; +import { BaseClosingSales } from '@/types/api/closing/closing'; -type BaseClosingSales = { - id: number; - realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; - product: string; - product_type: string; - customer: string; - quantity: number; - weight: number; - average: number; - price: number; - total: number; - kandang: string; - kandang_id: number; - payment_status: string; -}; +interface SalesReportTableProps { + type?: 'detail'; + initialValues?: BaseClosingSales; +} const generateCustomHeaders = (template: { groups: Array<{ @@ -101,122 +88,18 @@ const generateCustomHeaders = (template: { return rows; }; -const SalesReportTable = () => { +const SalesReportTable = ({ + type = 'detail', + initialValues, +}: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo( - () => [ - { - id: 1, - realization_date: '2025-10-02', - week_age: 3, - age_label: '3 Weeks', - delivery_order_number: 'DO.MBU.699', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'TIAN YUSTIAN', - quantity: 1045, - weight: 1000, - average: 0.96, - price: 25300, - total: 25300000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 2, - realization_date: '2025-10-07', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1037', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'ZAENAL MUTAQIN', - quantity: 850, - weight: 1211.4, - average: 1.43, - price: 23700, - total: 28710180, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 3, - realization_date: '2025-10-09', - week_age: 4, - age_label: '4 Weeks', - delivery_order_number: 'DO.MBU.1107', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CORNELIUS TONY KUSTANTO', - quantity: 560, - weight: 990, - average: 1.77, - price: 23100, - total: 22869000, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 4, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1108', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'CV. KOPO AB', - quantity: 1088, - weight: 1934.3, - average: 1.78, - price: 23100, - total: 44682330, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 5, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1110', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'H. MAMAN ROMANSAH', - quantity: 624, - weight: 1121.4, - average: 1.8, - price: 22960, - total: 25747344, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - { - id: 6, - realization_date: '2025-10-09', - week_age: 0, - age_label: '', - delivery_order_number: 'DO.MBU.1133', - product: 'MBU BERLIAN CHICK A', - product_type: 'CHICKEN', - customer: 'PT. SAMUDERA MULIA LESTARI', - quantity: 624, - weight: 1102.3, - average: 1.77, - price: 23100, - total: 25463130, - kandang: 'ACE AWANG', - kandang_id: 1, - payment_status: 'Lunas', - }, - ], - [] - ); + const salesBroilerData: BaseClosingSales[] = useMemo(() => { + if (activeTabId === 'penjualan' && initialValues) { + return [initialValues]; + } + return []; + }, [initialValues, activeTabId]); const totals = useMemo(() => { const totalQuantity = salesBroilerData.reduce( From ba40bbb1d37c647f91cbfb567e86c9f28d388a26 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:07:10 +0700 Subject: [PATCH 019/105] refactor(FE-327): remove dummy sales data and update ClosingApiService to fetch real sales data from API --- src/services/api/closing.ts | 149 ++---------------------------------- 1 file changed, 7 insertions(+), 142 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 6dab6f8d..1378ab58 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,123 +1,13 @@ import { BaseApiService } from './base'; -import { BaseApiResponse, CreatedUser } from '@/types/api/api-general'; +import { BaseApiResponse } from '@/types/api/api-general'; import { ClosingSales } from '@/types/api/closing/closing'; -import { sleep } from '@/lib/helper'; - -const adminUser: CreatedUser = { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', -}; - -const DUMMY_SALES: ClosingSales[] = [ - { - id: 1, - realization_date: '2025-01-15', - week_age: 4, - age_label: 'Week 4', - delivery_order_number: 'DO-2025-001', - product: { - id: 1, - name: 'Ayam Broiler', - brand: 'Brand A', - sku: 'AYM-001', - product_price: 20000, - selling_price: 25000, - tax: 10, - expiry_period: 30, - uom: { - id: 1, - name: 'Kg', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - suppliers: [], - flags: [], - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - product_category: { - id: 1, - code: 'LC001', - name: 'Live Chicken', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - customer: { - id: 1, - name: 'PT. Bumi Mandiri', - pic_id: 1, - pic: adminUser, - type: 'COMPANY', - address: 'Jl. Industri No. 123', - phone: '+62-21-1234567', - email: 'info@bumimandiri.com', - account_number: '1234567890', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - quantity: 1000, - weight: 1850, - average: 1.85, - price: 25000, - total: 25000000, - kandang: { - id: 1, - name: 'Singaparna 1', - status: 'ACTIVE', - location: { - id: 1, - name: 'Singaparna', - address: 'Jl. Singaparna No. 1', - area: { - id: 1, - name: 'Tasikmalaya', - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - capacity: 1000, - pic: { - id: 1, - id_user: 1, - email: 'admin@example.com', - name: 'Admin User', - }, - created_user: adminUser, - created_at: '2025-01-01T00:00:00Z', - updated_at: '2025-01-01T00:00:00Z', - }, - kandang_id: 1, - payment_status: 'PAID', - created_user: adminUser, - created_at: '2025-01-15T10:00:00Z', - updated_at: '2025-01-15T10:00:00Z', - }, -]; export class ClosingApiService extends BaseApiService< ClosingSales, unknown, unknown > { - constructor(basePath: string = '') { + constructor(basePath: string) { super(basePath); } @@ -125,36 +15,11 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - // TODO: Remove dummy data when real API is ready - await sleep(750); - - const saleData = DUMMY_SALES.find((sale) => sale.id === id); - - if (!saleData) { - return { - code: 404, - status: 'error', - message: 'Sales data not found!', - }; - } - - return { - code: 200, - status: 'success', - message: 'Successfully get sales data!', - meta: { - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }, - data: saleData, - }; - - // const getPenjualanPath = `${this.basePath}/${id}/penjualan`; - // return await this.customRequest>(getPenjualanPath); - } catch (error) { - console.error('Error fetching penjualan:', error); + const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + return await this.customRequest>( + getPenjualanPath + ); + } catch { return undefined; } } From 1a4a05308f0ee94f926af968954b702f077969f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:08:44 +0700 Subject: [PATCH 020/105] refactor(FE-326,327): enhance SalesReportTable to handle empty sales data and conditionally render summary row --- .../pages/closing/sale/SalesReportTable.tsx | 112 ++++++++++-------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 85fe50a5..18af45a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -102,6 +102,18 @@ const SalesReportTable = ({ }, [initialValues, activeTabId]); const totals = useMemo(() => { + if (salesBroilerData.length === 0) { + return { + totalQuantity: 0, + totalWeight: 0, + avgWeight: 0, + avgPricePartner: 0, + totalPartner: 0, + avgPriceAct: 0, + totalAct: 0, + }; + } + const totalQuantity = salesBroilerData.reduce( (sum, item) => sum + (item.quantity || 0), 0 @@ -113,7 +125,7 @@ const SalesReportTable = ({ const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; const validPriceItems = salesBroilerData.filter( - (item) => item.price != null + (item) => item.price != null && item.price > 0 ); const avgPricePartner = validPriceItems.length > 0 @@ -374,54 +386,56 @@ const SalesReportTable = ({ /> {/* Summary Row */} - - - - - - - - - - - - - - - - - - - -
- Total Penjualan - - - - - - - - - - - - - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} - - - - - - -
+ {salesBroilerData.length > 0 && ( + + + + + + + + + + + + + + + + + + + +
+ Total Penjualan + + - + + - + + - + + - + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} + + - + + - +
+ )}
), From dc6b0eaec6304c7d2302d2eee73353ac0cea233e Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 4 Dec 2025 14:10:46 +0700 Subject: [PATCH 021/105] fix(FE): change datatype location in inventory products --- .../inventory/product/detail/InventoryProductDetail.tsx | 8 ++++---- .../pages/inventory/product/detail/StockLogTable.tsx | 2 +- src/config/constant.ts | 2 +- src/types/api/inventory/product.d.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx index 59b1fed0..d650a982 100644 --- a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -79,8 +79,8 @@ const InventoryProductDetail = ({ Harga Jual : - {inventoryProduct?.product_price - ? formatCurrency(inventoryProduct.product_price) + {inventoryProduct?.selling_price + ? formatCurrency(inventoryProduct.selling_price) : '-'} @@ -88,8 +88,8 @@ const InventoryProductDetail = ({ Harga Beli : - {inventoryProduct?.selling_price - ? formatCurrency(inventoryProduct?.selling_price) + {inventoryProduct?.product_price + ? formatCurrency(inventoryProduct?.product_price) : '-'} diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index 8c9e874f..666a8b57 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -59,7 +59,7 @@ const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => { }, { header: 'Oleh', - accessorKey: 'created_by', + accessorKey: 'created_by.name', }, ]} className={{ diff --git a/src/config/constant.ts b/src/config/constant.ts index e0846283..8af85bc1 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -64,7 +64,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ icon: 'mdi:warehouse', submenu: [ { - title: 'Product', + title: 'Produk', link: '/inventory/product', icon: 'mdi:package-variant-closed', }, diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index 85253e2a..593a2bbc 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -1,4 +1,4 @@ -import { BaseMetadata } from '@/types/api/api-general'; +import { BaseMetadata, CreatedUser } from '@/types/api/api-general'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { Supplier } from '@/types/api/master-data/supplier'; @@ -25,7 +25,7 @@ export type ProductWarehouseStock = { product_id: number; warehouse_id: number; warehouse_name: string; - location: Location | string; + location: Location | null; current_stock: number; stock_logs: StockLog[]; }; @@ -38,7 +38,7 @@ export type StockLog = { loggable_id: number; notes: string; product_warehouse_id: number; - created_by: number; + created_by: CreatedUser; created_at: string; }; From 8ea29579ecb5d712856f065dc615ca3e2e51b84d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:10:57 +0700 Subject: [PATCH 022/105] feat(FE-326,327): add layout and closing detail page with sales report integration --- src/app/closing/detail/layout.tsx | 11 +++++++ src/app/closing/detail/page.tsx | 55 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/app/closing/detail/layout.tsx create mode 100644 src/app/closing/detail/page.tsx diff --git a/src/app/closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/closing/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/closing/detail/page.tsx b/src/app/closing/detail/page.tsx new file mode 100644 index 00000000..43a8469a --- /dev/null +++ b/src/app/closing/detail/page.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; + +const ClosingDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const closingId = searchParams.get('closingId'); + + const { data: closing, isLoading: isLoadingClosing } = useSWR( + closingId, + (id: string) => { + const numericId = parseInt(id, 10); + if (isNaN(numericId) || numericId <= 0) { + throw new Error('Invalid closing ID'); + } + return ClosingApi.getPenjualan(numericId); + } + ); + + if (!closingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingClosing && (!closing || isResponseError(closing))) { + // router.replace('/404'); + return; + } + + return ( +
+ {isLoadingClosing && ( +
+ +
+ )} + {!isLoadingClosing && isResponseSuccess(closing) && ( + + )} +
+ ); +}; + +export default ClosingDetailPage; From 492efb18e24506d56ff7c948eedc48e62ed9b544 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:15:58 +0700 Subject: [PATCH 023/105] chore: update next.js to version 15.5.7 in package.json and package-lock.json --- package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..535bb986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..85485ee3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 15ced14e20b9c9bca49ae2d291f736a173b4e1b6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:11 +0700 Subject: [PATCH 024/105] refactor(FE-Storyless): add footer rendering support to Table component --- src/components/Table.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index eafd3e7a..96f23e8e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -75,6 +75,8 @@ export interface TableProps { column: Column, headerGroup: HeaderGroup ) => ReactNode; + renderFooter?: boolean; + footerContent?: ReactNode; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -121,6 +123,8 @@ const Table = ({ customHeaderRows = [], renderCustomHeaders = false, onCustomHeaderCellRender, + renderFooter = false, + footerContent, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -327,6 +331,8 @@ const Table = ({ ))} + + {renderFooter && footerContent} From 949761d59d4b98faa6331321fbe07cee9a6c6744 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 14:25:27 +0700 Subject: [PATCH 025/105] feat(FE-326,327): add footer rendering to SalesReportTable for total sales display --- .../pages/closing/sale/SalesReportTable.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 18af45a5..552985a9 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -375,20 +375,9 @@ const SalesReportTable = ({ columns={salesColumns} renderCustomHeaders={true} customHeaderRows={salesCustomHeaderRows} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', - }} - /> - - {/* Summary Row */} - {salesBroilerData.length > 0 && ( - - + renderFooter={salesBroilerData.length > 0} + footerContent={ + - -
Total Penjualan @@ -433,9 +422,17 @@ const SalesReportTable = ({ -
- )} + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', + }} + /> ), From e0a851481421f00a2e8e669872952c4d9deacc9d Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 4 Dec 2025 16:13:47 +0700 Subject: [PATCH 026/105] fix(FE): adjust data types for project flock and product stock inventory --- .../inventory/product/InventoryProductTable.tsx | 9 +++++++++ .../product/detail/InventoryProductDetail.tsx | 17 +++++------------ .../project-flock/detail/ProjectFlockDetail.tsx | 4 ++-- src/types/api/inventory/product.d.ts | 1 + src/types/api/production/project-flock.d.ts | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index da660568..20c7859c 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -112,6 +112,15 @@ const InventoryProductTable = () => { accessorFn: (row) => row.product_category.name, header: 'Kategori', }, + { + accessorFn: (row) => row.total_stock, + header: 'Stok', + cell: (props) => { + return props.row.original.total_stock + ? formatCurrency(props.row.original.total_stock) + : '-'; + }, + }, { accessorFn: (row) => row.uom.name, header: 'Satuan', diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx index d650a982..8f209c83 100644 --- a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -8,20 +8,9 @@ import { useMemo } from 'react'; const InventoryProductDetail = ({ inventoryProduct, - refresh, }: { inventoryProduct?: InventoryProduct; - refresh?: () => void; }) => { - const totalStok = useMemo(() => { - return ( - inventoryProduct?.product_warehouses?.reduce( - (total, warehouse) => total + (warehouse.current_stock || 0), - 0 - ) || 0 - ); - }, [inventoryProduct]); - const stockLogs = useMemo(() => { return ( inventoryProduct?.product_warehouses?.flatMap( @@ -105,7 +94,11 @@ const InventoryProductDetail = ({ Total Stok : - {formatNumber(totalStok)} + + {inventoryProduct?.total_stock + ? formatNumber(inventoryProduct?.total_stock) + : '-'} + diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 28ab757c..fc1a87d3 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -221,7 +221,7 @@ const ProjectFlockDetail = ({ }} > {` ${formatCurrency( - projectFlock.project_budgets.reduce( + (projectFlock.project_budgets ?? []).reduce( (acc, curr) => acc + curr.price * curr.qty, 0 ) @@ -236,7 +236,7 @@ const ProjectFlockDetail = ({ {/* Card List Project Budgets */} {openBudgets && - projectFlock.project_budgets.map((budget) => ( + (projectFlock.project_budgets ?? []).map((budget) => ( Date: Thu, 4 Dec 2025 16:35:10 +0700 Subject: [PATCH 027/105] fix(FE): adjust data types for inventory product stock --- .../pages/inventory/product/InventoryProductTable.tsx | 6 +++--- .../inventory/product/detail/InventoryProductDetail.tsx | 2 +- .../pages/inventory/product/detail/StockLogTable.tsx | 2 +- .../inventory/product/detail/StockProductWarehouseTable.tsx | 4 ++-- src/types/api/inventory/product.d.ts | 4 +++- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index 20c7859c..3c188d17 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -9,7 +9,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatCurrency } from '@/lib/helper'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { InventoryProductApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { InventoryProduct } from '@/types/api/inventory/product'; @@ -117,8 +117,8 @@ const InventoryProductTable = () => { header: 'Stok', cell: (props) => { return props.row.original.total_stock - ? formatCurrency(props.row.original.total_stock) - : '-'; + ? formatNumber(props.row.original.total_stock) + : '0'; }, }, { diff --git a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx index 8f209c83..ad523929 100644 --- a/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx +++ b/src/components/pages/inventory/product/detail/InventoryProductDetail.tsx @@ -97,7 +97,7 @@ const InventoryProductDetail = ({ {inventoryProduct?.total_stock ? formatNumber(inventoryProduct?.total_stock) - : '-'} + : '0'} diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index 666a8b57..42f7bc29 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -59,7 +59,7 @@ const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => { }, { header: 'Oleh', - accessorKey: 'created_by.name', + accessorKey: 'created_user.name', }, ]} className={{ diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx index 10343d8a..6f48f7cd 100644 --- a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx +++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx @@ -31,8 +31,8 @@ const StockProductWarehouseTable = ({ header: 'Lokasi', accessorKey: 'location', cell: (props) => { - return Boolean(props.row.original.location) - ? props.row.original.location + return props.row.original.location != null + ? props.row.original.location.name : '-'; }, }, diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index d631d71f..cb8f98a1 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -3,6 +3,7 @@ import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { Supplier } from '@/types/api/master-data/supplier'; import { Uom } from '@/types/api/master-data/uom'; +import { Location } from '@/types/api/master-data/location'; export type BaseInventoryProduct = { id: number; @@ -39,7 +40,8 @@ export type StockLog = { loggable_id: number; notes: string; product_warehouse_id: number; - created_by: CreatedUser; + created_by: number; + created_user: CreatedUser; created_at: string; }; From c69d9dd6050710b1142a1072befb7423354f2083 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 4 Dec 2025 16:39:00 +0700 Subject: [PATCH 028/105] fix(FE): revert require auth component to correct file --- src/components/helper/RequireAuth.tsx | 199 +++++--------------------- 1 file changed, 33 insertions(+), 166 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -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', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } 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}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth; From b095208fae29059050f25be05131dd05040142ad Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 17:41:22 +0700 Subject: [PATCH 029/105] refactor(FE-327): Temporarily map Indonesian sales fields to English --- .../pages/closing/sale/SalesReportTable.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 552985a9..b702752a 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -88,6 +88,34 @@ const generateCustomHeaders = (template: { return rows; }; +// TODO: TEMPORARY - Remove this when backend API returns English field names +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { + if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { + return []; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return data.penjualan.map((item: any) => ({ + id: item.id, + realization_date: item.tanggal_realisasi, + age_label: item.umur_label, + umur_minggu: item.umur_minggu, + delivery_order_number: item.no_do, + product: item.produk, + jenis_produk: item.jenis_produk, + customer: item.customer, + quantity: item.qty, + weight: item.kg, + average: item.avg, + price: item.harga, + total: item.total, + kandang: item.kandang, + payment_status: item.status_pembayaran, + })); +}; +// END TODO + const SalesReportTable = ({ type = 'detail', initialValues, @@ -96,6 +124,13 @@ const SalesReportTable = ({ const salesBroilerData: BaseClosingSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { + // TODO: TEMPORARY - Remove this when backend API returns English field names + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { + return mapIndonesianDataToEnglish(initialValues); + } + // END TODO return [initialValues]; } return []; From 7d9a88cf3b8dc293ba71c671036b83b9aedc5086 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 20:14:29 +0700 Subject: [PATCH 030/105] feat(FE-326,327): Add sortable table headers and styling --- src/components/Table.tsx | 76 +++++++++++++--- .../pages/closing/sale/SalesReportTable.tsx | 88 +++++++++++-------- 2 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 96f23e8e..69406220 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -46,6 +46,7 @@ export interface CustomHeaderRow { colSpan?: number; rowSpan?: number; className?: string; + field?: string; }>; className?: string; } @@ -236,18 +237,69 @@ const Table = ({ headerRow.className || className.customHeaderRowClassName } > - {headerRow.cells.map((cell) => ( - - {cell.content} - - ))} + {headerRow.cells.map((cell) => { + const column = table + .getAllColumns() + .find((col) => col.id === cell.field); + + const canSort = column?.getCanSort(); + const sortingState = column?.getIsSorted(); + + return ( + +
+ {cell.content} + + {canSort && ( +
+ + +
+ )} +
+ + ); + })} ))} diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b702752a..b9f73635 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -14,6 +14,15 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface HeaderCell { + id: string; + content: React.ReactNode; + colSpan?: number; + rowSpan?: number; + className: string; + field?: string; +} + const generateCustomHeaders = (template: { groups: Array<{ label: string; @@ -41,31 +50,43 @@ const generateCustomHeaders = (template: { template.groups.forEach((group) => { if (group.subLabels) { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field || 'group'}-${subColumnIndex}`, content: group.label, colSpan: group.colSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', + }; + + mainRow.push(mainCell); group.subLabels.forEach((subLabel) => { - subRow.push({ + const subCell: HeaderCell = { id: `sub-${subColumnIndex}`, content: subLabel, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + if (group.label === 'Jumlah') { + subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + } + + subRow.push(subCell); subColumnIndex++; }); } else { - mainRow.push({ + const mainCell: HeaderCell = { id: `${group.field}-header`, content: group.label, rowSpan: group.rowSpan, className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300', - }); + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', + }; + + mainCell.field = group.field; + + mainRow.push(mainCell); } }); @@ -371,7 +392,7 @@ const SalesReportTable = ({ }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_partner', rowSpan: 2 }, + { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, { label: 'Kandang', field: 'kandang', rowSpan: 2 }, @@ -413,49 +434,37 @@ const SalesReportTable = ({ renderFooter={salesBroilerData.length > 0} footerContent={ - - + + Total Penjualan - - - - - - - - - - - - - - - - - + + + + + {formatNumber(totals.totalQuantity)} - + {formatNumber(totals.totalWeight)} - + {formatNumber(totals.avgWeight)} - + {formatCurrency(totals.avgPricePartner)} - + {formatCurrency(totals.totalPartner)} - + {formatCurrency(totals.avgPriceAct)} - + {formatCurrency(totals.totalAct)} - - - - - - - - + + } @@ -463,9 +472,10 @@ const SalesReportTable = ({ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'hidden', - bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300', + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} />
From 075d945a59a193d33f56b8e3d213592fcd7e0dac Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 4 Dec 2025 21:29:45 +0700 Subject: [PATCH 031/105] refactor(FE-326): Use placeholder for sales type in header --- src/components/pages/closing/sale/SalesReportTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index b9f73635..ca616107 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -419,7 +419,7 @@ const SalesReportTable = ({ content: (

- Penjualan Ayam Besar + Penjualan {'(diisi dengan jenis penjualan)'}

Date: Fri, 5 Dec 2025 11:14:52 +0700 Subject: [PATCH 032/105] refactor(FE-327): Split BaseClosingSales into BaseSales and wrapper --- src/types/api/closing/closing.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index d4b94770..6b17d8e1 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -4,7 +4,7 @@ import { Product } from '@type/api/master-data/product'; import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; -export type BaseClosingSales = { +export type BaseSales = { id: number; realization_date: string; week_age: number; @@ -23,4 +23,9 @@ export type BaseClosingSales = { payment_status: string; }; +export type BaseClosingSales = { + project_type: string; + penjualan: BaseSales[]; +}; + export type ClosingSales = BaseMetadata & BaseClosingSales; From 46e072bbcfaf091a9c0fafe30c3504dd95524c7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 11:15:41 +0700 Subject: [PATCH 033/105] refactor(FE-327): Map Indonesian sales fields and add API sample --- .../pages/closing/sale/SalesReportTable.tsx | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index ca616107..d93dbf2e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,7 +7,7 @@ import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; interface SalesReportTableProps { type?: 'detail'; @@ -110,8 +110,7 @@ const generateCustomHeaders = (template: { }; // TODO: TEMPORARY - Remove this when backend API returns English field names -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { +const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { return []; } @@ -120,11 +119,11 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { return data.penjualan.map((item: any) => ({ id: item.id, realization_date: item.tanggal_realisasi, + week_age: item.umur_minggu, age_label: item.umur_label, - umur_minggu: item.umur_minggu, delivery_order_number: item.no_do, product: item.produk, - jenis_produk: item.jenis_produk, + product_category: item.jenis_produk, customer: item.customer, quantity: item.qty, weight: item.kg, @@ -132,6 +131,7 @@ const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => { price: item.harga, total: item.total, kandang: item.kandang, + kandang_id: item.kandang_id, payment_status: item.status_pembayaran, })); }; @@ -143,16 +143,14 @@ const SalesReportTable = ({ }: SalesReportTableProps) => { const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseClosingSales[] = useMemo(() => { + const salesBroilerData: BaseSales[] = useMemo(() => { if (activeTabId === 'penjualan' && initialValues) { // TODO: TEMPORARY - Remove this when backend API returns English field names - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { return mapIndonesianDataToEnglish(initialValues); } // END TODO - return [initialValues]; + return []; } return []; }, [initialValues, activeTabId]); @@ -208,7 +206,7 @@ const SalesReportTable = ({ }; }, [salesBroilerData]); - const salesColumns: ColumnDef[] = useMemo( + const salesColumns: ColumnDef[] = useMemo( () => [ { id: 'realization_date', @@ -418,9 +416,7 @@ const SalesReportTable = ({ label: 'Penjualan', content: (
-

- Penjualan {'(diisi dengan jenis penjualan)'} -

+

Penjualan

+
+ +
+            {JSON.stringify(
+              {
+                code: 200,
+                status: 'success',
+                message: 'Retrieved sales report successfully',
+                data: {
+                  project_type: 'GROWING',
+                  flock_id: '1',
+                  period: 10,
+                  sales: [
+                    {
+                      id: 1,
+                      realization_date: '2025-12-05T02:22:17.443165Z',
+                      age: 20,
+                      do_number: 'SO-DO-10001',
+                      product: {
+                        id: 1,
+                        name: 'Laptop Gaming X500',
+                        product_price: 15000000,
+                        selling_price: 16500000.5,
+                        uom: {
+                          id: 1,
+                          name: 'KG',
+                        },
+                        flags: ['Best Seller', 'New Arrival'],
+                        product_category: {
+                          id: 5,
+                          name: 'Elektronik',
+                          code: 'DOC',
+                        },
+                      },
+                      customer: {
+                        id: 12345,
+                        name: 'PT. Solusi Teknologi Nusantara',
+                        type: 'Perusahaan',
+                        account_number: 'ACC1234567890',
+                        balance: 5000000.75,
+                        pic: {
+                          id: 101,
+                          name: 'Budi Santoso',
+                          email: 'budi.santoso@example.com',
+                          role: 'Manajer Akun',
+                        },
+                      },
+                      qty: 6348,
+                      weight: 19142,
+                      avg_weight: 3.02,
+                      price: 26419,
+                      total_price: 505712498,
+                      kandang: {
+                        id: 1,
+                        name: 'cibeber 1',
+                      },
+                      payment_status: 'Paid',
+                    },
+                  ],
+                },
+              },
+              null,
+              2
+            )}
+          
+
+
); }; From f205c6650985f7d3af07f5a127e18d2564815c9c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 17:49:59 +0700 Subject: [PATCH 034/105] refactor(FE-327): Rename Ekor label to Kuantitas --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index d93dbf2e..6796465e 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -69,7 +69,7 @@ const generateCustomHeaders = (template: { }; if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Ekor' ? 'quantity' : 'weight'; + subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; } subRow.push(subCell); @@ -244,7 +244,7 @@ const SalesReportTable = ({ { id: 'quantity', accessorKey: 'quantity', - header: 'Ekor', + header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; const isSummary = props.row.id === 'summary'; @@ -386,7 +386,7 @@ const SalesReportTable = ({ { label: 'Jumlah', colSpan: 2, - subLabels: ['Ekor', 'Kg'], + subLabels: ['Kuantitas', 'Kg'], }, { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, From 5869e0434b175f859a9b3e4fd753c4ac55f98bda Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:26:58 +0700 Subject: [PATCH 035/105] refactor(FE-327): change closing API paths and sales types --- src/services/api/closing.ts | 4 ++-- src/types/api/closing/closing.d.ts | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 1378ab58..66f88c76 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -15,7 +15,7 @@ export class ClosingApiService extends BaseApiService< id: number ): Promise | undefined> { try { - const getPenjualanPath = `http://localhost:4010/api/closing/${id}/penjualan`; + const getPenjualanPath = `${id}/penjualan`; return await this.customRequest>( getPenjualanPath ); @@ -25,4 +25,4 @@ export class ClosingApiService extends BaseApiService< } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 6b17d8e1..03217438 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,31 +1,27 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { Kandang } from '@type/api/master-data/kandang'; import { Product } from '@type/api/master-data/product'; -import { ProductCategory } from '@type/api/master-data/product-category'; import { Customer } from '@type/api/master-data/customer'; export type BaseSales = { id: number; realization_date: string; - week_age: number; - age_label: string; - delivery_order_number: string; + age: number; + do_number: string; product: Product; - product_category: ProductCategory; customer: Customer; - quantity: number; + qty: number; weight: number; - average: number; + avg_weight: number; price: number; - total: number; - kandang: Kandang; - kandang_id: number; + total_price: number; payment_status: string; }; export type BaseClosingSales = { project_type: string; - penjualan: BaseSales[]; + flock_id: number; + period: number; + sales: BaseSales[]; }; export type ClosingSales = BaseMetadata & BaseClosingSales; From 30db7ee95d806ed47785c1987b0ca43841b5ea51 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 18:27:45 +0700 Subject: [PATCH 036/105] refactor(FE-327): change SalesReportTable to use new API fields --- .../pages/closing/sale/SalesReportTable.tsx | 181 ++++-------------- 1 file changed, 32 insertions(+), 149 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 6796465e..64247890 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -8,6 +8,8 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; interface SalesReportTableProps { type?: 'detail'; @@ -109,34 +111,6 @@ const generateCustomHeaders = (template: { return rows; }; -// TODO: TEMPORARY - Remove this when backend API returns English field names -const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => { - if (!data || !data.penjualan || !Array.isArray(data.penjualan)) { - return []; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return data.penjualan.map((item: any) => ({ - id: item.id, - realization_date: item.tanggal_realisasi, - week_age: item.umur_minggu, - age_label: item.umur_label, - delivery_order_number: item.no_do, - product: item.produk, - product_category: item.jenis_produk, - customer: item.customer, - quantity: item.qty, - weight: item.kg, - average: item.avg, - price: item.harga, - total: item.total, - kandang: item.kandang, - kandang_id: item.kandang_id, - payment_status: item.status_pembayaran, - })); -}; -// END TODO - const SalesReportTable = ({ type = 'detail', initialValues, @@ -144,13 +118,8 @@ const SalesReportTable = ({ const [activeTabId, setActiveTabId] = useState('penjualan'); const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues) { - // TODO: TEMPORARY - Remove this when backend API returns English field names - if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) { - return mapIndonesianDataToEnglish(initialValues); - } - // END TODO - return []; + if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { + return initialValues.sales; } return []; }, [initialValues, activeTabId]); @@ -169,7 +138,7 @@ const SalesReportTable = ({ } const totalQuantity = salesBroilerData.reduce( - (sum, item) => sum + (item.quantity || 0), + (sum, item) => sum + (item.qty || 0), 0 ); const totalWeight = salesBroilerData.reduce( @@ -188,7 +157,7 @@ const SalesReportTable = ({ : 0; const totalPartner = salesBroilerData.reduce( - (sum, item) => sum + (item.total || 0), + (sum, item) => sum + (item.total_price || 0), 0 ); @@ -218,14 +187,14 @@ const SalesReportTable = ({ }, }, { - id: 'age_label', - accessorKey: 'age_label', + id: 'age', + accessorKey: 'age', header: 'Umur', cell: (props) => props.getValue() || '-', }, { - id: 'delivery_order_number', - accessorKey: 'delivery_order_number', + id: 'do_number', + accessorKey: 'do_number', header: 'No. DO', cell: (props) => props.getValue() || '-', }, @@ -233,17 +202,23 @@ const SalesReportTable = ({ id: 'product', accessorKey: 'product', header: 'Produk', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const product = props.getValue() as Product; + return product?.name || '-'; + }, }, { id: 'customer', accessorKey: 'customer', header: 'Customer', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const customer = props.getValue() as Customer; + return customer?.name || '-'; + }, }, { - id: 'quantity', - accessorKey: 'quantity', + id: 'qty', + accessorKey: 'qty', header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; @@ -270,8 +245,8 @@ const SalesReportTable = ({ }, }, { - id: 'average', - accessorKey: 'average', + id: 'avg_weight', + accessorKey: 'avg_weight', header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; @@ -289,26 +264,16 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_mitra', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -317,26 +282,16 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { id: 'total_act', - accessorKey: 'total', + accessorKey: 'total_price', header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -379,16 +334,16 @@ const SalesReportTable = ({ const headerTemplate = { groups: [ { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age_label', rowSpan: 2 }, - { label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 }, + { label: 'Umur', field: 'age', rowSpan: 2 }, + { label: 'No. DO', field: 'do_number', rowSpan: 2 }, { label: 'Produk', field: 'product', rowSpan: 2 }, { label: 'Customer', field: 'customer', rowSpan: 2 }, { label: 'Jumlah', colSpan: 2, - subLabels: ['Kuantitas', 'Kg'], + subLabels: ['Qty', 'Kg'], }, - { label: 'AVG (Kg)', field: 'average', rowSpan: 2 }, + { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, @@ -482,78 +437,6 @@ const SalesReportTable = ({ variant='lifted' /> -
- -
-            {JSON.stringify(
-              {
-                code: 200,
-                status: 'success',
-                message: 'Retrieved sales report successfully',
-                data: {
-                  project_type: 'GROWING',
-                  flock_id: '1',
-                  period: 10,
-                  sales: [
-                    {
-                      id: 1,
-                      realization_date: '2025-12-05T02:22:17.443165Z',
-                      age: 20,
-                      do_number: 'SO-DO-10001',
-                      product: {
-                        id: 1,
-                        name: 'Laptop Gaming X500',
-                        product_price: 15000000,
-                        selling_price: 16500000.5,
-                        uom: {
-                          id: 1,
-                          name: 'KG',
-                        },
-                        flags: ['Best Seller', 'New Arrival'],
-                        product_category: {
-                          id: 5,
-                          name: 'Elektronik',
-                          code: 'DOC',
-                        },
-                      },
-                      customer: {
-                        id: 12345,
-                        name: 'PT. Solusi Teknologi Nusantara',
-                        type: 'Perusahaan',
-                        account_number: 'ACC1234567890',
-                        balance: 5000000.75,
-                        pic: {
-                          id: 101,
-                          name: 'Budi Santoso',
-                          email: 'budi.santoso@example.com',
-                          role: 'Manajer Akun',
-                        },
-                      },
-                      qty: 6348,
-                      weight: 19142,
-                      avg_weight: 3.02,
-                      price: 26419,
-                      total_price: 505712498,
-                      kandang: {
-                        id: 1,
-                        name: 'cibeber 1',
-                      },
-                      payment_status: 'Paid',
-                    },
-                  ],
-                },
-              },
-              null,
-              2
-            )}
-          
-
-
); }; From eaf118845c4af32c0f3962ee60de20c7e5276f7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 19:15:38 +0700 Subject: [PATCH 037/105] feat(FE-327): Include Kandang in sales data and display name --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++++- src/types/api/closing/closing.d.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 64247890..e2956bb6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -10,6 +10,7 @@ import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; @@ -298,7 +299,10 @@ const SalesReportTable = ({ id: 'kandang', accessorKey: 'kandang', header: 'Kandang', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const kandang = props.getValue() as Kandang; + return kandang?.name || '-'; + }, }, { id: 'payment_status', diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts index 03217438..64d0d465 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -1,6 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; +import { Kandang } from '@type/api/master-data/kandang'; export type BaseSales = { id: number; @@ -14,6 +15,7 @@ export type BaseSales = { avg_weight: number; price: number; total_price: number; + kandang: Kandang; payment_status: string; }; From 885e4250fd3c9a3cde419030e1f232eccd001960 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 5 Dec 2025 22:55:11 +0700 Subject: [PATCH 038/105] 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[]; +}; From 85fdb4f7dd6ddae590ecc6816fbf45be367a4f8a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 00:15:30 +0700 Subject: [PATCH 039/105] refactor(FE): refactor chickin views and adjust approval logic in project flocks --- .../chickin/add/kandang/page.tsx | 2 +- src/components/FloatingActionsButton.tsx | 30 ++- .../production/chickin/form/ChickinForm.tsx | 226 +++++++++++------- .../chickin/form/tabs/ChickLogsView.tsx | 212 ++++++++-------- .../chickin/form/tabs/ChickinFormView.tsx | 213 ++++++++++------- .../project-flock/ProjectFlockTable.tsx | 41 +++- .../closing/ProjectFlockClosingForm.tsx | 2 +- .../detail/ProjectFlockDetail.tsx | 21 +- 8 files changed, 440 insertions(+), 307 deletions(-) diff --git a/src/app/production/project-flock/chickin/add/kandang/page.tsx b/src/app/production/project-flock/chickin/add/kandang/page.tsx index a22039d1..c3a93a80 100644 --- a/src/app/production/project-flock/chickin/add/kandang/page.tsx +++ b/src/app/production/project-flock/chickin/add/kandang/page.tsx @@ -44,7 +44,7 @@ export default function AddChickinKandang() { return ( <> -
+
{isLoading && } {!isLoading && isResponseSuccess(projectFlockKandang) && diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index 1ee5e6c0..c0033d72 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -1,5 +1,6 @@ 'use client'; +import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; @@ -11,12 +12,14 @@ type FloatingActionsButtonProps = { label?: string; onClick?: () => void; hidden?: boolean; + disabled?: boolean; }[]; approvals: { action: 'APPROVED' | 'REJECTED'; icon: string; label?: string; onClick?: () => void; + disabled?: boolean; }[]; selectedRowIds: number[]; onClose: () => void; @@ -69,10 +72,12 @@ const FloatingActionsButton = ({ .filter((action) => !action.hidden) .map((action, index) => { return ( - + ); })}
{/* Tombol Close */} - +
@@ -104,14 +110,18 @@ const FloatingActionsButton = ({ {/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
{approvals.map((approval, index) => ( - + ))}
diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index eadc9e66..d7210c8b 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -14,6 +14,9 @@ import ApprovalSteps, { import { PROJECT_FLOCK_KANDANG_APPROVAL_LINE } from '@/config/approval-line'; import ChickinFormView from '@/components/pages/production/chickin/form/tabs/ChickinFormView'; import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { Icon } from '@iconify/react'; +import Badge from '@/components/Badge'; const ChickinFormKandang = ({ formType = 'add', initialValues, @@ -24,6 +27,7 @@ const ChickinFormKandang = ({ afterSubmit?: () => void; }) => { const [activeTabId, setActiveTabId] = useState('formChickIn'); + const [openChickin, setOpenChickin] = useState(false); const { approvals, @@ -43,102 +47,142 @@ const ChickinFormKandang = ({ }; return ( -
- + - {approvals && !approvalsLoading && ( - - )} + {/* Informasi Kandang */} +
+
+

Informasi Kandang

- - - emptyContent={ -
- - Informasi Kandang belum tersedia... - -
- } - data={[initialValues?.kandang]} - columns={[ - { - header: 'Area', - accessorFn: () => initialValues?.project_flock?.area.name || '-', - }, - { - header: 'Lokasi', - accessorFn: () => - initialValues?.project_flock?.location.name || '-', - }, - { - header: 'Flock', - accessorFn: () => initialValues?.project_flock?.flock_name || '-', - }, - { - header: 'Kandang', - accessorFn: (row) => row?.name || '-', - }, - { - header: 'Kapasitas', - accessorFn: (row) => - (row?.capacity && formatNumber(row?.capacity)) || '-', - }, - { - header: 'Penanggung Jawab', - accessorFn: (row) => row?.pic?.name || '-', - }, - ]} - className={{ - 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', - paginationClassName: 'hidden', - }} + {approvals && !approvalsLoading && ( +
+ +
+ )} + + {/* Badge Row */} +
+ + {' '} + Aktif + +
+ + + {` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} + +
+ + {/* Information Grid */} +
+ {/* Area */} +
+ Area +
+
+ {initialValues.project_flock.area.name} +
+ + {/* Lokasi */} +
+ Lokasi +
+
+ {initialValues.project_flock?.location.name} +
+ + {/* Kandang */} +
+ Kandang +
+
{initialValues.kandang.name}
+ + {/* Jumlah DOC */} +
+ Jumlah DOC +
+
+ {formatNumber( + initialValues.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
+
+ +
+
+

Informasi Chick In

+ {/* Badge Row */} +
+ + {' '} + Perlu Chick In ({initialValues.available_qtys?.length ?? 0}) + +
+ setOpenChickin(!openChickin)} + > + {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} + + +
+
+ {openChickin && ( + - - - ), - }, - { - content: ( - - ), - id: 'logsChickIn', - label: 'Riwayat Chick In', - }, - ]} - variant='lifted' + )} + -
+ ); }; diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 8accf9ae..865091d7 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,17 +2,12 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import PillBadge from '@/components/PillBadge'; -import Table from '@/components/Table'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { ChickinApi } from '@/services/api/production/chickin'; -import { - Chickin, - ProjectFlockKandang, -} from '@/types/api/production/project-flock-kandang'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; @@ -54,105 +49,120 @@ const ChickinLogsView = ({ return ( <> - -
- {initialValues?.approval?.step_number == 1 && ( - - )} -
- - data={initialValues?.chickins || []} - columns={[ - { - header: '#', - cell: (props) => props.row.index + 1, - }, - { - accessorFn: (row) => row.chick_in_date, - header: 'Tanggal Chick In', - cell: (props) => { - return formatDate(props.getValue() as string, 'DD MMM YYYY'); - }, - }, - { - accessorFn: (row) => row.product_warehouse?.warehouse?.name, - header: 'Kandang', - }, - { - accessorFn: (row) => row.product_warehouse?.product?.name, - header: 'Produk', - }, - { - accessorFn: (row) => row.usage_qty ?? row.pending_usage_qty, - header: 'Jumlah Chick In', - cell: (props) => { - if (props.row.original.usage_qty != 0) { - return formatNumber(props.row.original.usage_qty); - } else if (props.row.original.pending_usage_qty != 0) { - return formatNumber(props.row.original.pending_usage_qty); - } else { - return '-'; - } - }, - }, - { - accessorFn: (row) => row.pending_usage_qty, - header: 'Status', - cell: (props) => { - return ( - - ); - }, - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': initialValues?.chickins?.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', - paginationClassName: 'hidden', - }} - /> +
+ {/* Card List Chickin Logs */} + {(initialValues?.chickins || []).length === 0 ? ( +
+ + Belum ada riwayat Chick In... + +
+ ) : ( + (initialValues?.chickins || []).map((chickin, index) => { + const isApproved = chickin.usage_qty !== 0; + const isPending = chickin.pending_usage_qty !== 0; + const quantity = isApproved + ? chickin.usage_qty + : isPending + ? chickin.pending_usage_qty + : 0; + + return ( + +
+ {/* Header with Status Badge */} +
+
+ Chick In #{index + 1} +
+ +
+ + {/* Tanggal Chick In */} +
+
+ {' '} + Tanggal Chick In +
+
+ {formatDate(chickin.chick_in_date, 'DD MMM YYYY')} +
+
+ + {/* Kandang */} +
+
+ {' '} + Kandang +
+
+ {chickin.product_warehouse?.warehouse?.name || '-'} +
+
+ + {/* Produk */} +
+
+ {' '} + Produk +
+
+ {chickin.product_warehouse?.product?.name || '-'} +
+
+ + {/* Jumlah Chick In */} +
+
+ {' '} + Jumlah Chick In +
+
+ {quantity > 0 ? `${formatNumber(quantity)} Ekor` : '-'} +
+
+
+
+ ); + }) + )} + + {initialValues?.approval?.step_number == 1 && ( + + )} + {chickinErrorMessage && (
setChickinErrorMessage('')}> {chickinErrorMessage}
)} - +
+ { handleReset(); }} onSubmit={formik.handleSubmit} > - - - data={formik.values.chickin_requests || []} - columns={[ - { - accessorFn: (row) => row.chick_in_date, - header: 'Tanggal Chick In', - cell(props) { - return ( - - ); - }, - }, - { - accessorFn: (row) => row.product_warehouse_id, - header: 'Produk', - cell(props) { - const availableQty = initialValues?.available_qtys?.find( - (availableQty) => - availableQty.product_warehouse.id === - props.row.original.product_warehouse_id - ); - return ( -
{availableQty?.product_warehouse?.product?.name}
- ); - }, - }, - { - accessorFn: (row) => row.product_warehouse_id, - header: 'Jumlah (ekor)', - cell(props) { - const availableQty = initialValues?.available_qtys?.find( - (availableQty) => - availableQty.product_warehouse.id === - props.row.original.product_warehouse_id - ); - return ( -
- {availableQty?.available_qty - ? formatNumber(availableQty?.available_qty) - : '-'} -
- ); - }, - }, - ]} - className={{ - 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-2 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-2 py-2 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', - }} - emptyContent={ -
- - Isi persediaan DOC untuk kandang belum tersedia... - + {(formik.values.chickin_requests || []).map((chickinRequest, index) => { + const availableQty = initialValues?.available_qtys?.find( + (availableQty) => + availableQty.product_warehouse.id === + chickinRequest.product_warehouse_id + ); + return ( + +
+
+ {formatNumber(availableQty?.available_qty ?? 0)} Ekor -{' '} + {availableQty?.product_warehouse?.product?.name} +
+ {chickinRequest.chick_in_date && ( + + )}
- } - /> -
-
- + + + ); + })} + {/* + data={formik.values.chickin_requests || []} + columns={[ + { + accessorFn: (row) => row.chick_in_date, + header: 'Tanggal Chick In', + cell(props) { + return ( + + ); + }, + }, + { + accessorFn: (row) => row.product_warehouse_id, + header: 'Produk', + cell(props) { + const availableQty = initialValues?.available_qtys?.find( + (availableQty) => + availableQty.product_warehouse.id === + props.row.original.product_warehouse_id + ); + return ( +
{availableQty?.product_warehouse?.product?.name}
+ ); + }, + }, + { + accessorFn: (row) => row.product_warehouse_id, + header: 'Jumlah (ekor)', + cell(props) { + const availableQty = initialValues?.available_qtys?.find( + (availableQty) => + availableQty.product_warehouse.id === + props.row.original.product_warehouse_id + ); + return ( +
+ {availableQty?.available_qty + ? formatNumber(availableQty?.available_qty) + : '-'} +
+ ); + }, + }, + ]} + className={{ + 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-2 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-2 py-2 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + emptyContent={ +
+ + Isi persediaan DOC untuk kandang belum tersedia... + +
+ } + /> */} + {formik.values.chickin_requests?.length > 0 && ( -
+ )} {chickinErrorMessage && (
setChickinErrorMessage('')}> {chickinErrorMessage} diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 613ea5fc..4be30f7a 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -10,8 +10,6 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import Table from '@/components/Table'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn, formatDate } from '@/lib/helper'; @@ -23,7 +21,7 @@ import { ProjectFlock } from '@/types/api/production/project-flock'; import { Icon } from '@iconify/react'; import { CellContext, SortingState } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; @@ -124,7 +122,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { }); const router = useRouter(); - // State + // ===== State ===== const [rowSelection, setRowSelection] = useState>({}); const selectedRowIds = Object.keys(rowSelection) .filter((id) => rowSelection[id]) @@ -151,7 +149,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); - // Fetch Data + // ===== Fetch Data ===== const { data: projectFlocks, isLoading, @@ -192,7 +190,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { KandangApi.getAllFetcher ); - // Data to Options Mapping + // ===== Data to Options Mapping ====== const optionsArea = isResponseSuccess(areas) ? areas?.data.map((area) => ({ value: area.id, @@ -212,7 +210,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { })) : []; - // Handler + // ====== HANDLER ====== const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; setPageSize(newVal.value as number); @@ -220,17 +218,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ProjectFlockApi.delete(selectedProjectFlock?.id as number); + await ProjectFlockApi.delete(selectedSingleRow?.id as number); refreshProjectFlocks(); deleteModal.closeModal(); toast.success('Successfully delete Project Flock!'); setIsDeleteLoading(false); + setRowSelection({}); }; const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; - const confirmApprovalHandler = async ( notes: string, approvalAction: 'APPROVED' | 'REJECTED' @@ -260,10 +258,29 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setIsApproveLoading(false); }; + // ====== EFFECT ====== useEffect(() => { refreshProjectFlocks(); }, [refresh]); + // ====== MEMO ====== + const selectedSingleRow: ProjectFlock | null | undefined = useMemo(() => { + return selectedRowIds.length === 1 + ? isResponseSuccess(projectFlocks) + ? projectFlocks?.data.find((row) => row.id === selectedRowIds[0]) + : null + : null; + }, [rowSelection]); + + const canApprove = useMemo(() => { + if (!selectedSingleRow || isApproveLoading) return false; + + const isPengajuan = selectedSingleRow.approval.step_number == 1; + const isNotRejected = selectedSingleRow.approval.action != 'REJECTED'; + + return isPengajuan && isNotRejected; + }, [selectedSingleRow, isApproveLoading]); + return ( <>
@@ -617,9 +634,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { { action: 'DELETE', icon: 'material-symbols:delete-outline-rounded', - label: `Hapus ${selectedRowIds.length} data`, + label: `Hapus data`, + hidden: selectedRowIds.length !== 1, onClick: () => { - toast.error(`Konfirmasi hapus ${selectedRowIds.length} data.`); + deleteModal.openModal(); }, }, ]} @@ -632,6 +650,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setApprovalAction('APPROVED'); confirmModal.openModal(); }, + disabled: !canApprove, }, { icon: 'mdi:times', diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index a078ed85..a12e7369 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -102,7 +102,7 @@ const ProjectFlockClosingForm = ({ className={{ badge: 'rounded-lg px-2' }} > - {` Kapasitas ${formatNumber(projectFlockKandang.kandang.capacity)} Ekor`} + {` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`}
diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 17272d20..e2d8018f 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -34,6 +34,10 @@ const ProjectFlockDetail = ({ null ); + const selectedKandang = projectFlock.kandangs.find( + (kandang) => kandang.id === Number(selectedKandangId) + ); + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); const deleteProjectFlockRes = await ProjectFlockApi.delete( @@ -328,16 +332,21 @@ const ProjectFlockDetail = ({ value={selectedKandangId?.toString()} size='md' color='neutral' + disabled={projectFlock.approval.step_number == 1} > {projectFlock.kandangs.map((kandang) => (
setSelectedKamdangId(kandang.id.toString())} + onClick={() => + projectFlock.approval.step_number > 1 && + setSelectedKamdangId(kandang.id.toString()) + } >
@@ -374,7 +385,9 @@ const ProjectFlockDetail = ({ className='w-full px-2 py-1 text-sm' variant='outline' color='error' - disabled={!selectedKandangId} + disabled={ + !selectedKandangId || projectFlock.approval.step_number == 1 + } > Close From 4fe53f364a1756a1b91e3d89b3b4955e52907476 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:54:12 +0700 Subject: [PATCH 040/105] refactor(FE-326): Remove Tabs wrapper from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index e2956bb6..7213a621 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,5 @@ 'use client'; -import Tabs from '@/components/Tabs'; import React, { useState, useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; @@ -365,81 +364,68 @@ const SalesReportTable = ({ return ( <>
- -

Penjualan

- - 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - ), - }, - ]} - variant='lifted' - /> +
+

Penjualan

+ +
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPriceAct)} - - {formatCurrency(totals.totalAct)} -
0} + footerContent={ + + + + + + + + + + + + + + + + + + + } + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'hidden', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + + ); From 4ff16499918b0483ebd0c48d19da14a88392fa96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 08:56:14 +0700 Subject: [PATCH 041/105] chore(FE-327): Remove unused state from SalesReportTable --- .../pages/closing/sale/SalesReportTable.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 7213a621..a62c20a6 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; import Table, { CustomHeaderRow } from '@/components/Table'; import Card from '@/components/Card'; @@ -115,14 +115,9 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const [activeTabId, setActiveTabId] = useState('penjualan'); - const salesBroilerData: BaseSales[] = useMemo(() => { - if (activeTabId === 'penjualan' && initialValues && initialValues.sales) { - return initialValues.sales; - } - return []; - }, [initialValues, activeTabId]); + return initialValues?.sales || []; + }, [initialValues]); const totals = useMemo(() => { if (salesBroilerData.length === 0) { From ff1493b520b00b4e4e35320b9add9154bf4c5816 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:09:41 +0700 Subject: [PATCH 042/105] refactor(FE-326): Remove avgPriceAct/totalAct and use partner totals, fix badge case --- .../pages/closing/sale/SalesReportTable.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a62c20a6..dedcb498 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -127,8 +127,6 @@ const SalesReportTable = ({ avgWeight: 0, avgPricePartner: 0, totalPartner: 0, - avgPriceAct: 0, - totalAct: 0, }; } @@ -156,17 +154,12 @@ const SalesReportTable = ({ 0 ); - const avgPriceAct = avgPricePartner; - const totalAct = totalPartner; - return { totalQuantity, totalWeight, avgWeight, avgPricePartner, totalPartner, - avgPriceAct, - totalAct, }; }, [salesBroilerData]); @@ -307,12 +300,10 @@ const SalesReportTable = ({ const getStatusColor = (status: string) => { if (!status) return 'neutral'; switch (status.toLowerCase()) { - case 'lunas': + case 'paid': return 'success'; - case 'pending': + case 'tempo': return 'warning'; - case 'belum lunas': - return 'error'; default: return 'neutral'; } @@ -399,10 +390,10 @@ const SalesReportTable = ({ {formatCurrency(totals.totalPartner)} From aad24c3c58e97527b95e83bb212befe74de3ba54 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:12:02 +0700 Subject: [PATCH 043/105] refactor(FE-327): Rename salesBroilerData to salesData --- .../pages/closing/sale/SalesReportTable.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index dedcb498..a473dd14 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -115,12 +115,12 @@ const SalesReportTable = ({ type = 'detail', initialValues, }: SalesReportTableProps) => { - const salesBroilerData: BaseSales[] = useMemo(() => { + const salesData: BaseSales[] = useMemo(() => { return initialValues?.sales || []; }, [initialValues]); const totals = useMemo(() => { - if (salesBroilerData.length === 0) { + if (salesData.length === 0) { return { totalQuantity: 0, totalWeight: 0, @@ -130,17 +130,17 @@ const SalesReportTable = ({ }; } - const totalQuantity = salesBroilerData.reduce( + const totalQuantity = salesData.reduce( (sum, item) => sum + (item.qty || 0), 0 ); - const totalWeight = salesBroilerData.reduce( + const totalWeight = salesData.reduce( (sum, item) => sum + (item.weight || 0), 0 ); const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; - const validPriceItems = salesBroilerData.filter( + const validPriceItems = salesData.filter( (item) => item.price != null && item.price > 0 ); const avgPricePartner = @@ -149,7 +149,7 @@ const SalesReportTable = ({ validPriceItems.length : 0; - const totalPartner = salesBroilerData.reduce( + const totalPartner = salesData.reduce( (sum, item) => sum + (item.total_price || 0), 0 ); @@ -161,7 +161,7 @@ const SalesReportTable = ({ avgPricePartner, totalPartner, }; - }, [salesBroilerData]); + }, [salesData]); const salesColumns: ColumnDef[] = useMemo( () => [ @@ -359,11 +359,11 @@ const SalesReportTable = ({ }} >
+ Total Penjualan + + {formatNumber(totals.totalQuantity)} + + {formatNumber(totals.totalWeight)} + + {formatNumber(totals.avgWeight)} + + {formatCurrency(totals.avgPricePartner)} + + {formatCurrency(totals.totalPartner)} + + {formatCurrency(totals.avgPriceAct)} + + {formatCurrency(totals.totalAct)} +
- {formatCurrency(totals.avgPriceAct)} + {formatCurrency(totals.avgPricePartner)} - {formatCurrency(totals.totalAct)} + {formatCurrency(totals.totalPartner)}
0} + renderFooter={salesData.length > 0} footerContent={ From c9552dec2db111b47ed73f66fa46987d19f87ea7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:47:38 +0700 Subject: [PATCH 044/105] refactor(FE-326): Remove custom header rows and simplify Table --- src/components/Table.tsx | 208 ++++-------------- .../pages/closing/sale/SalesReportTable.tsx | 138 +----------- 2 files changed, 53 insertions(+), 293 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 69406220..b5148fea 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,8 +14,6 @@ import { SortingState, OnChangeFn, Row, - HeaderGroup, - Column, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -34,21 +32,6 @@ interface TableClassNames { bodyRowClassName?: string; bodyColumnClassName?: string; paginationClassName?: string; - customHeaderRowClassName?: string; - customHeaderCellClassName?: string; -} - -export interface CustomHeaderRow { - id: string; - cells: Array<{ - id: string; - content: ReactNode; - colSpan?: number; - rowSpan?: number; - className?: string; - field?: string; - }>; - className?: string; } export interface TableProps { @@ -69,13 +52,6 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); - customHeaderRows?: CustomHeaderRow[]; - renderCustomHeaders?: boolean; - onCustomHeaderCellRender?: ( - cell: ReactNode, - column: Column, - headerGroup: HeaderGroup - ) => ReactNode; renderFooter?: boolean; footerContent?: ReactNode; } @@ -111,8 +87,6 @@ const Table = ({ bodyRowClassName: '', bodyColumnClassName: '', paginationClassName: '', - customHeaderRowClassName: '', - customHeaderCellClassName: '', }, emptyContent = emptyContentDefaultValue, sorting, @@ -121,9 +95,6 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, - customHeaderRows = [], - renderCustomHeaders = false, - onCustomHeaderCellRender, renderFooter = false, footerContent, }: TableProps) => { @@ -228,143 +199,55 @@ const Table = ({
- {renderCustomHeaders && - customHeaderRows.length > 0 && - customHeaderRows.map((headerRow) => ( - - {headerRow.cells.map((cell) => { - const column = table - .getAllColumns() - .find((col) => col.id === cell.field); - - const canSort = column?.getCanSort(); - const sortingState = column?.getIsSorted(); - - return ( - - ); - })} - - ))} - {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - let cellContent = flexRender( - header.column.columnDef.header, - header.getContext() - ); - - if (onCustomHeaderCellRender) { - cellContent = onCustomHeaderCellRender( - cellContent, - header.column, - headerGroup - ); - } - - return ( - - ); - })} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ))} ))} @@ -383,7 +266,6 @@ const Table = ({ ))} - {renderFooter && footerContent}
-
- {cell.content} - - {canSort && ( -
- - -
- )} -
-
( + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() )} - > -
- {cellContent} - {header.column.getCanSort() && ( -
- - -
- )} -
-
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index a473dd14..3218d1d8 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { ColumnDef } from '@tanstack/react-table'; -import Table, { CustomHeaderRow } from '@/components/Table'; +import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; @@ -16,101 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface HeaderCell { - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - field?: string; -} - -const generateCustomHeaders = (template: { - groups: Array<{ - label: string; - field?: string; - rowSpan?: number; - colSpan?: number; - subLabels?: string[]; - }>; -}): CustomHeaderRow[] => { - const mainRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - const subRow: Array<{ - id: string; - content: React.ReactNode; - colSpan?: number; - rowSpan?: number; - className: string; - }> = []; - let subColumnIndex = 0; - - template.groups.forEach((group) => { - if (group.subLabels) { - const mainCell: HeaderCell = { - id: `${group.field || 'group'}-${subColumnIndex}`, - content: group.label, - colSpan: group.colSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200', - }; - - mainRow.push(mainCell); - - group.subLabels.forEach((subLabel) => { - const subCell: HeaderCell = { - id: `sub-${subColumnIndex}`, - content: subLabel, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - if (group.label === 'Jumlah') { - subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight'; - } - - subRow.push(subCell); - subColumnIndex++; - }); - } else { - const mainCell: HeaderCell = { - id: `${group.field}-header`, - content: group.label, - rowSpan: group.rowSpan, - className: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200', - }; - - mainCell.field = group.field; - - mainRow.push(mainCell); - } - }); - - const rows: CustomHeaderRow[] = [ - { - id: 'main-header', - cells: mainRow, - className: 'bg-gray-50', - }, - ]; - - if (subRow.length > 0) { - rows.push({ - id: 'sub-header', - cells: subRow, - className: 'bg-gray-50', - }); - } - - return rows; -}; - const SalesReportTable = ({ type = 'detail', initialValues, @@ -310,7 +215,7 @@ const SalesReportTable = ({ }; return ( - + {status || '-'} ); @@ -320,33 +225,6 @@ const SalesReportTable = ({ [] ); - const headerTemplate = { - groups: [ - { label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 }, - { label: 'Umur', field: 'age', rowSpan: 2 }, - { label: 'No. DO', field: 'do_number', rowSpan: 2 }, - { label: 'Produk', field: 'product', rowSpan: 2 }, - { label: 'Customer', field: 'customer', rowSpan: 2 }, - { - label: 'Jumlah', - colSpan: 2, - subLabels: ['Qty', 'Kg'], - }, - { label: 'AVG (Kg)', field: 'avg_weight', rowSpan: 2 }, - { label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 }, - { label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 }, - { label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 }, - { label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 }, - { label: 'Kandang', field: 'kandang', rowSpan: 2 }, - { label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 }, - ], - }; - - const salesCustomHeaderRows = useMemo( - () => generateCustomHeaders(headerTemplate), - [] - ); - return ( <>
@@ -361,8 +239,6 @@ const SalesReportTable = ({ 0} footerContent={ @@ -374,13 +250,13 @@ const SalesReportTable = ({ - - - ))} - {renderFooter && footerContent} + + {renderFooter && + (footerData && footerData.length > 0 + ? footerTable.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + : footerContent)} +
+ {formatNumber(totals.totalQuantity)} + {formatNumber(totals.totalWeight)} + {formatNumber(totals.avgWeight)} @@ -403,7 +279,9 @@ const SalesReportTable = ({ className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'hidden', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', bodyRowClassName: 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: From 27c867036f1d0ce3f1e9be7b0aeb935b2d973b99 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:51:40 +0700 Subject: [PATCH 045/105] refactor(FE-327): Update import paths for consistency in SalesReportTable --- src/components/pages/closing/sale/SalesReportTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 3218d1d8..1330b7f0 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -7,9 +7,9 @@ import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; -import { Product } from '@type/api/master-data/product'; -import { Customer } from '@type/api/master-data/customer'; -import { Kandang } from '@type/api/master-data/kandang'; +import { Product } from '@/types/api/master-data/product'; +import { Customer } from '@/types/api/master-data/customer'; +import { Kandang } from '@/types/api/master-data/kandang'; interface SalesReportTableProps { type?: 'detail'; From 99b9df27a78fa4f029615d5ff487b9cc1e72a22e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 09:58:38 +0700 Subject: [PATCH 046/105] refactor(FE-326): Comment _closing for copy-paste function --- src/app/{closing => _closing}/detail/layout.tsx | 0 src/app/{closing => _closing}/detail/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/{closing => _closing}/detail/layout.tsx (100%) rename src/app/{closing => _closing}/detail/page.tsx (97%) diff --git a/src/app/closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx similarity index 100% rename from src/app/closing/detail/layout.tsx rename to src/app/_closing/detail/layout.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/_closing/detail/page.tsx similarity index 97% rename from src/app/closing/detail/page.tsx rename to src/app/_closing/detail/page.tsx index 43a8469a..038e5072 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/_closing/detail/page.tsx @@ -34,7 +34,7 @@ const ClosingDetailPage = () => { } if (!isLoadingClosing && (!closing || isResponseError(closing))) { - // router.replace('/404'); + router.replace('/404'); return; } From 341cb4245214383b396cdd8bf56776a003431f6a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 10:05:10 +0700 Subject: [PATCH 047/105] feat(FE): adding temporary perhitungan sapronak --- src/app/us-284/page.tsx | 11 + .../detail/ProjectFlockDetail.tsx | 25 +- .../us-284/DummyDataSapronakCalculation.ts | 298 ++++++++++++++++++ .../pages/us-284/SapronakCalculationTable.tsx | 204 ++++++++++++ src/config/constant.ts | 6 + 5 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 src/app/us-284/page.tsx create mode 100644 src/components/pages/us-284/DummyDataSapronakCalculation.ts create mode 100644 src/components/pages/us-284/SapronakCalculationTable.tsx diff --git a/src/app/us-284/page.tsx b/src/app/us-284/page.tsx new file mode 100644 index 00000000..49764069 --- /dev/null +++ b/src/app/us-284/page.tsx @@ -0,0 +1,11 @@ +import SapronakCalculationTable from '@/components/pages/us-284/SapronakCalculationTable'; + +const PerhitunganSapronak = () => { + return ( +
+ +
+ ); +}; + +export default PerhitunganSapronak; diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index e2d8018f..5b54b10e 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -20,6 +20,10 @@ 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'; +import ApprovalSteps, { + useApprovalSteps, +} from '@/components/pages/ApprovalSteps'; +import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line'; const ProjectFlockDetail = ({ projectFlock, @@ -38,6 +42,17 @@ const ProjectFlockDetail = ({ (kandang) => kandang.id === Number(selectedKandangId) ); + const { + approvals, + isLoading: approvalsLoading, + refresh: refreshApprovals, + } = useApprovalSteps({ + latestApproval: projectFlock?.approval, + approvalLines: PROJECT_FLOCK_APPROVAL_LINE, + moduleName: 'PROJECT_FLOCKS', + moduleId: projectFlock?.id.toString() ?? '', + }); + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); const deleteProjectFlockRes = await ProjectFlockApi.delete( @@ -90,6 +105,12 @@ const ProjectFlockDetail = ({

Informasi Umum

+ {/* Status Approval */} + {approvals && !approvalsLoading && ( +
+ +
+ )} {/* Badge Row */}
-
+ {/*
History
@@ -163,7 +184,7 @@ const ProjectFlockDetail = ({ height={11} /> -
+
*/} {/* BARIS 1 */}
{ + constructor(basePath: string = '') { + super(basePath); + } + + async getPerhitunganSapronak( + projectFlockId: number + ): Promise | undefined> { + // Dummy implementation - simulate API call with delay + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + status: 'success', + message: 'Retrieved sapronak calculation successfully', + data: DUMMY_SAPRONAK_CALCULATION, + }); + }, 500); // Simulate 500ms network delay + }); + + /* + // Real API implementation - uncomment when backend is ready + try { + const path = `${this.basePath}/project-flock/${projectFlockId}/sapronak-calculation`; + + return await httpClient>(path, { + method: 'GET', + }); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + */ + } +} + +export const ClosingApi = new ClosingService(`/closing`); diff --git a/src/components/pages/us-284/SapronakCalculationTable.tsx b/src/components/pages/us-284/SapronakCalculationTable.tsx new file mode 100644 index 00000000..dea01068 --- /dev/null +++ b/src/components/pages/us-284/SapronakCalculationTable.tsx @@ -0,0 +1,204 @@ +'use client'; + +import Card from '@/components/Card'; +import { + ClosingApi, + RowSapronakCalculation, +} from '@/components/pages/us-284/DummyDataSapronakCalculation'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; + +const SapronakCalculationTable = ({ + projectFlockId, +}: { + projectFlockId: number; +}) => { + const { data: sapronakCalculation, isLoading } = useSWR( + `/sapronak-calculation`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId) + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => { + const value = props.getValue() as string; + // Data already in DD-MMM-YYYY format, just display it + return value || '-'; + }, + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => { + const value = props.getValue() as number; + return formatNumber(value); + }, + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => { + const value = props.getValue() as number; + return formatNumber(value); + }, + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => { + const value = props.getValue() as number; + return formatNumber(value); + }, + }, + { + header: 'Uraian', + accessorKey: 'uraian', + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => { + const value = props.getValue() as number; + return formatCurrency(value); + }, + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => { + const value = props.getValue() as number; + return formatCurrency(value); + }, + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => { + const value = props.getValue() as string; + return value || '-'; + }, + }, + ], + [] + ); + + return ( +
+ {isLoading && ( +
+ +
+ )} + {isResponseSuccess(sapronakCalculation) && ( + <> + + + data={sapronakCalculation.data.doc_broiler.rows} + columns={columns} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(sapronakCalculation) && + sapronakCalculation?.data.doc_broiler.rows.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', + }} + /> + + + + + data={sapronakCalculation.data.ovk.rows} + columns={columns} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(sapronakCalculation) && + sapronakCalculation?.data.ovk.rows.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', + }} + /> + + + + + data={sapronakCalculation.data.pakan.rows} + columns={columns} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(sapronakCalculation) && + sapronakCalculation?.data.pakan.rows.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 SapronakCalculationTable; diff --git a/src/config/constant.ts b/src/config/constant.ts index 8af85bc1..4611bb90 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -58,6 +58,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ icon: 'uil:wallet', }, + { + title: 'Perhitungan Sapronak', + link: '/us-284', + icon: 'uil:calculator', + }, + { title: 'Persediaan', link: '/inventory', From e407410c4ab25c8e26f5eb4d0f055ba2331ea3a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:25:40 +0700 Subject: [PATCH 048/105] feat(FE-Storyless): Add footer support to Table component --- src/components/Table.tsx | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b5148fea..5c76f44e 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -31,6 +31,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -54,6 +57,7 @@ export interface TableProps { enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; footerContent?: ReactNode; + footerData?: TData[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -86,6 +90,9 @@ const Table = ({ tableBodyClassName: '', bodyRowClassName: '', bodyColumnClassName: '', + tableFooterClassName: '', + footerRowClassName: '', + footerColumnClassName: '', paginationClassName: '', }, emptyContent = emptyContentDefaultValue, @@ -97,6 +104,7 @@ const Table = ({ enableRowSelection, renderFooter = false, footerContent, + footerData = [], }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -164,6 +172,14 @@ const Table = ({ const table = useReactTable(tableOptions); const { setPageSize } = table; + const footerTableOptions: TableOptions = { + columns, + data: footerData, + getCoreRowModel: getCoreRowModel(), + }; + + const footerTable = useReactTable(footerTableOptions); + const prevPageClickHandler = () => { table.previousPage(); @@ -266,7 +282,26 @@ const Table = ({
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
From b3f7b8a3c530c0d782576e2d85f64d21e002fdf3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 6 Dec 2025 10:26:26 +0700 Subject: [PATCH 049/105] feat(FE-326): Add totals footer row to sales report table --- .../pages/closing/sale/SalesReportTable.tsx | 172 +++++++++++++----- 1 file changed, 124 insertions(+), 48 deletions(-) diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 1330b7f0..f0810f15 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -16,6 +16,10 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } +interface FooterSalesRow extends BaseSales { + _isFooter: true; +} + const SalesReportTable = ({ type = 'detail', initialValues, @@ -68,6 +72,29 @@ const SalesReportTable = ({ }; }, [salesData]); + const footerData = useMemo((): FooterSalesRow[] => { + if (salesData.length === 0) return []; + + const footerRow: FooterSalesRow = { + id: -999, + realization_date: 'Total Penjualan', + age: 0, + do_number: '', + product: {} as Product, + customer: {} as Customer, + qty: totals.totalQuantity, + weight: totals.totalWeight, + avg_weight: totals.avgWeight, + price: totals.avgPricePartner, + total_price: totals.totalPartner, + kandang: {} as Kandang, + payment_status: '', + _isFooter: true, + }; + + return [footerRow]; + }, [salesData, totals]); + const salesColumns: ColumnDef[] = useMemo( () => [ { @@ -75,6 +102,14 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) { + return ( +
+ {props.row.original.realization_date} +
+ ); + } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, @@ -83,19 +118,27 @@ const SalesReportTable = ({ id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => props.getValue() || '-', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + return isFooter ? null : props.getValue() || '-'; + }, }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -105,6 +148,8 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, @@ -115,9 +160,13 @@ const SalesReportTable = ({ header: 'Kuantitas', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -129,9 +178,13 @@ const SalesReportTable = ({ header: 'Kg', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -143,9 +196,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isSummary = props.row.id === 'summary'; + const isFooter = '_isFooter' in props.row.original; return ( -
+
{formatNumber(value)}
); @@ -157,7 +214,18 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -166,7 +234,18 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -175,7 +254,18 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -184,7 +274,18 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - return
{formatCurrency(value)}
; + const isFooter = '_isFooter' in props.row.original; + return ( +
+ {formatCurrency(value)} +
+ ); }, }, { @@ -192,6 +293,8 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -201,6 +304,9 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -239,43 +345,8 @@ const SalesReportTable = ({ 0} - footerContent={ - - - - - - - - - - - - - - - - - - - } className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', @@ -286,6 +357,11 @@ const SalesReportTable = ({ 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> From ffbf88671855535e16d5b2b9c0901520ef1e21c6 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 11:38:28 +0700 Subject: [PATCH 050/105] fix(FE): adjust chickin and closing after submit --- .../pages/production/chickin/form/ChickinForm.tsx | 3 +-- .../closing/ProjectFlockClosingForm.tsx | 14 +++++++++----- .../project-flock/form/ProjectFlockForm.tsx | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index d7210c8b..84c5b5a5 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -26,7 +26,6 @@ const ChickinFormKandang = ({ initialValues: ProjectFlockKandang; afterSubmit?: () => void; }) => { - const [activeTabId, setActiveTabId] = useState('formChickIn'); const [openChickin, setOpenChickin] = useState(false); const { @@ -41,7 +40,7 @@ const ChickinFormKandang = ({ }); const afterSubmitFormChickin = () => { - setActiveTabId('logsChickIn'); + setOpenChickin(true); afterSubmit && afterSubmit(); refreshApprovals(); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index a12e7369..7d5f1f9b 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -28,7 +28,7 @@ const ProjectFlockClosingForm = ({ projectFlockKandang: ProjectFlockKandang; }) => { const closeModal = useModal(); - const isCanClose = projectFlock.approval.step_number <= 2; + const isCanClose = projectFlock.approval?.step_number <= 1; const [isClosingLoading, setIsClosingLoading] = useState(false); const { data: closingData, isLoading } = useSWR( @@ -116,7 +116,7 @@ const ProjectFlockClosingForm = ({ > Area -
{projectFlock.area.name}
+
{projectFlock.area?.name}
{/* Lokasi */}
Lokasi
-
{projectFlock.location.name}
+
{projectFlock.location?.name}
{/* Kandang */}
Kandang
-
{projectFlockKandang.kandang.name}
+
{projectFlockKandang.kandang?.name}
{/* Jumlah DOC */}
@@ -279,7 +279,11 @@ const ProjectFlockClosingForm = ({

- Estimasi Aggaran Per Flock + Estimasi Anggaran Per Flock

{formik.values.project_budgets && From b97cc398549d4253d564aa175c2553d233fdd0e7 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 13:10:03 +0700 Subject: [PATCH 051/105] fix(FE): revert RequireAuth component and closing logic --- src/app/us-284/page.tsx | 11 - src/components/helper/RequireAuth.tsx | 199 ++---------- .../closing/ProjectFlockClosingForm.tsx | 11 +- .../us-284/DummyDataSapronakCalculation.ts | 298 ------------------ .../pages/us-284/SapronakCalculationTable.tsx | 204 ------------ 5 files changed, 40 insertions(+), 683 deletions(-) delete mode 100644 src/app/us-284/page.tsx delete mode 100644 src/components/pages/us-284/DummyDataSapronakCalculation.ts delete mode 100644 src/components/pages/us-284/SapronakCalculationTable.tsx diff --git a/src/app/us-284/page.tsx b/src/app/us-284/page.tsx deleted file mode 100644 index 49764069..00000000 --- a/src/app/us-284/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SapronakCalculationTable from '@/components/pages/us-284/SapronakCalculationTable'; - -const PerhitunganSapronak = () => { - return ( -
- -
- ); -}; - -export default PerhitunganSapronak; diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -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', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } 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}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 7d5f1f9b..bcfb7795 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -1,16 +1,15 @@ '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'; @@ -19,6 +18,8 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; +import { useRouter } from 'next/navigation'; +import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; const ProjectFlockClosingForm = ({ projectFlock, @@ -27,8 +28,9 @@ const ProjectFlockClosingForm = ({ projectFlock: ProjectFlock; projectFlockKandang: ProjectFlockKandang; }) => { + const router = useRouter(); const closeModal = useModal(); - const isCanClose = projectFlock.approval?.step_number <= 1; + const isCanClose = projectFlock.approval?.step_number <= 2; const [isClosingLoading, setIsClosingLoading] = useState(false); const { data: closingData, isLoading } = useSWR( @@ -48,6 +50,7 @@ const ProjectFlockClosingForm = ({ if (isResponseSuccess(deleteProjectFlockRes)) { toast.success(deleteProjectFlockRes?.message as string); + router.push(`/production/project-flock`); } if (isResponseError(deleteProjectFlockRes)) { toast.error(deleteProjectFlockRes?.message as string); @@ -280,7 +283,7 @@ const ProjectFlockClosingForm = ({ ref={closeModal.ref} type='error' text={ - projectFlock.approval?.step_number <= 1 + isCanClose ? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai' : 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif' } diff --git a/src/components/pages/us-284/DummyDataSapronakCalculation.ts b/src/components/pages/us-284/DummyDataSapronakCalculation.ts deleted file mode 100644 index 74a6313e..00000000 --- a/src/components/pages/us-284/DummyDataSapronakCalculation.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { BaseApiService } from '@/services/api/base'; -import { BaseApiResponse } from '@/types/api/api-general'; - -export type RowSapronakCalculation = { - id: number; - tanggal: string; - no_referensi: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - uraian: string; - kategori_produk: string; - harga_beli_per_qty: number; - total_harga: number; - keterangan: string; -}; - -export type TotalSapronakCalculation = { - label: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - harga_beli_per_qty: number; - total_harga: number; -}; - -export type SapronakCalculationItem = { - rows: RowSapronakCalculation[]; - total: TotalSapronakCalculation; -}; - -export type SapronakCalculation = { - doc_broiler: SapronakCalculationItem; - ovk: SapronakCalculationItem; - pakan: SapronakCalculationItem; -}; - -// Dummy data -const DUMMY_SAPRONAK_CALCULATION: SapronakCalculation = { - doc_broiler: { - rows: [ - { - id: 1, - tanggal: '11-Sep-2025', - no_referensi: 'PO-PULLET-388', - qty_masuk: 32800, - qty_keluar: 0, - qty_pakai: 32800, - uraian: 'PULLET LOHMANN (16 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 60136, - total_harga: 1972556800, - keterangan: '-', - }, - { - id: 2, - tanggal: '24-Sep-2025', - no_referensi: 'PO-PULLET-410', - qty_masuk: 14758, - qty_keluar: 0, - qty_pakai: 14758, - uraian: 'PULLET HY-LINE (17 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 65421, - total_harga: 965908998, - keterangan: '-', - }, - { - id: 3, - tanggal: '29-Sep-2025', - no_referensi: 'PO-PULLET-196', - qty_masuk: 35439, - qty_keluar: 0, - qty_pakai: 35439, - uraian: 'PULLET ISA BROWN (15 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 55909, - total_harga: 1981297351, - keterangan: '-', - }, - ], - total: { - label: 'TOTAL PULLET', - qty_masuk: 82997, - qty_keluar: 0, - qty_pakai: 82997, - harga_beli_per_qty: 59274.65, - total_harga: 4919963149, - }, - }, - ovk: { - rows: [ - { - id: 1, - tanggal: '28-Sep-2025', - no_referensi: 'PO-OVK-276', - qty_masuk: 52, - qty_keluar: 0, - qty_pakai: 52, - uraian: 'ND-IB VACCINE', - kategori_produk: 'OVK VAKSIN', - harga_beli_per_qty: 204652, - total_harga: 10641904, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 2, - tanggal: '26-Sep-2025', - no_referensi: 'PO-OVK-811', - qty_masuk: 43, - qty_keluar: 0, - qty_pakai: 43, - uraian: 'GUMBORO VACCINE', - kategori_produk: 'OVK VAKSIN', - harga_beli_per_qty: 298379, - total_harga: 12830297, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 3, - tanggal: '28-Sep-2025', - no_referensi: 'PO-OVK-879', - qty_masuk: 21, - qty_keluar: 0, - qty_pakai: 21, - uraian: 'AMOXITIN SOLUBLE', - kategori_produk: 'OVK OBAT', - harga_beli_per_qty: 145952, - total_harga: 3064992, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 4, - tanggal: '11-Okt-2025', - no_referensi: 'PO-OVK-340', - qty_masuk: 38, - qty_keluar: 0, - qty_pakai: 38, - uraian: 'TILOXIN SOLUBLE', - kategori_produk: 'OVK OBAT', - harga_beli_per_qty: 200424, - total_harga: 7616112, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 5, - tanggal: '27-Sep-2025', - no_referensi: 'PO-OVK-364', - qty_masuk: 7, - qty_keluar: 0, - qty_pakai: 7, - uraian: 'EGG STIMULANT', - kategori_produk: 'OVK VITAMIN', - harga_beli_per_qty: 115024, - total_harga: 805168, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 6, - tanggal: '16-Sep-2025', - no_referensi: 'PO-OVK-982', - qty_masuk: 57, - qty_keluar: 0, - qty_pakai: 57, - uraian: 'MULTIVIT-AMINO', - kategori_produk: 'OVK VITAMIN', - harga_beli_per_qty: 65123, - total_harga: 3712011, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 7, - tanggal: '04-Okt-2025', - no_referensi: 'PO-OVK-876', - qty_masuk: 4, - qty_keluar: 0, - qty_pakai: 4, - uraian: 'BKC DESINFEKTAN', - kategori_produk: 'OVK KIMIA', - harga_beli_per_qty: 105677, - total_harga: 422708, - keterangan: 'Program kesehatan & biosecurity', - }, - ], - total: { - label: 'TOTAL OVK', - qty_masuk: 222, - qty_keluar: 0, - qty_pakai: 222, - harga_beli_per_qty: 172965.92, - total_harga: 38481094, - }, - }, - pakan: { - rows: [ - { - id: 1, - tanggal: '13-Ags-2025', - no_referensi: 'PO-FEED-730', - qty_masuk: 4833, - qty_keluar: 0, - qty_pakai: 4833, - uraian: 'FEED PRE-LAY', - kategori_produk: 'PAKAN PRE-LAY', - harga_beli_per_qty: 7578, - total_harga: 36625874, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 2, - tanggal: '28-Jul-2025', - no_referensi: 'PO-FEED-555', - qty_masuk: 6500, - qty_keluar: 0, - qty_pakai: 6500, - uraian: 'FEED LAYER PHASE 1', - kategori_produk: 'PAKAN LAYER', - harga_beli_per_qty: 8116, - total_harga: 52754000, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 3, - tanggal: '24-Agu-2025', - no_referensi: 'PO-FEED-683', - qty_masuk: 8802, - qty_keluar: 0, - qty_pakai: 8802, - uraian: 'FEED LAYER PHASE 2', - kategori_produk: 'PAKAN LAYER', - harga_beli_per_qty: 8801, - total_harga: 77465402, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 4, - tanggal: '02-Sep-2025', - no_referensi: 'PO-FEED-448', - qty_masuk: 2185, - qty_keluar: 0, - qty_pakai: 2185, - uraian: 'JAGUNG GILING', - kategori_produk: 'PAKAN MIX', - harga_beli_per_qty: 5573, - total_harga: 12187705, - keterangan: 'Konsumsi pakan kandang layer', - }, - ], - total: { - label: 'TOTAL PAKAN', - qty_masuk: 22320, - qty_keluar: 0, - qty_pakai: 22320, - harga_beli_per_qty: 8092.39, - total_harga: 179032981, - }, - }, -}; - -export class ClosingService extends BaseApiService { - constructor(basePath: string = '') { - super(basePath); - } - - async getPerhitunganSapronak( - projectFlockId: number - ): Promise | undefined> { - // Dummy implementation - simulate API call with delay - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - code: 200, - status: 'success', - message: 'Retrieved sapronak calculation successfully', - data: DUMMY_SAPRONAK_CALCULATION, - }); - }, 500); // Simulate 500ms network delay - }); - - /* - // Real API implementation - uncomment when backend is ready - try { - const path = `${this.basePath}/project-flock/${projectFlockId}/sapronak-calculation`; - - return await httpClient>(path, { - method: 'GET', - }); - } catch (error: unknown) { - if (axios.isAxiosError>(error)) { - return error.response?.data; - } - return undefined; - } - */ - } -} - -export const ClosingApi = new ClosingService(`/closing`); diff --git a/src/components/pages/us-284/SapronakCalculationTable.tsx b/src/components/pages/us-284/SapronakCalculationTable.tsx deleted file mode 100644 index dea01068..00000000 --- a/src/components/pages/us-284/SapronakCalculationTable.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client'; - -import Card from '@/components/Card'; -import { - ClosingApi, - RowSapronakCalculation, -} from '@/components/pages/us-284/DummyDataSapronakCalculation'; -import Table from '@/components/Table'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatCurrency, formatNumber } from '@/lib/helper'; -import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import useSWR from 'swr'; - -const SapronakCalculationTable = ({ - projectFlockId, -}: { - projectFlockId: number; -}) => { - const { data: sapronakCalculation, isLoading } = useSWR( - `/sapronak-calculation`, - () => ClosingApi.getPerhitunganSapronak(projectFlockId) - ); - - const columns: ColumnDef[] = useMemo( - () => [ - { - header: 'Tanggal', - accessorKey: 'tanggal', - cell: (props) => { - const value = props.getValue() as string; - // Data already in DD-MMM-YYYY format, just display it - return value || '-'; - }, - }, - { - header: 'No. Referensi', - accessorKey: 'no_referensi', - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_masuk', - cell: (props) => { - const value = props.getValue() as number; - return formatNumber(value); - }, - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_keluar', - cell: (props) => { - const value = props.getValue() as number; - return formatNumber(value); - }, - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_pakai', - cell: (props) => { - const value = props.getValue() as number; - return formatNumber(value); - }, - }, - { - header: 'Uraian', - accessorKey: 'uraian', - }, - { - header: 'Kategori Produk', - accessorKey: 'kategori_produk', - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'harga_beli_per_qty', - cell: (props) => { - const value = props.getValue() as number; - return formatCurrency(value); - }, - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_harga', - cell: (props) => { - const value = props.getValue() as number; - return formatCurrency(value); - }, - }, - { - header: 'Keterangan', - accessorKey: 'keterangan', - cell: (props) => { - const value = props.getValue() as string; - return value || '-'; - }, - }, - ], - [] - ); - - return ( -
- {isLoading && ( -
- -
- )} - {isResponseSuccess(sapronakCalculation) && ( - <> - - - data={sapronakCalculation.data.doc_broiler.rows} - columns={columns} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(sapronakCalculation) && - sapronakCalculation?.data.doc_broiler.rows.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', - }} - /> - - - - - data={sapronakCalculation.data.ovk.rows} - columns={columns} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(sapronakCalculation) && - sapronakCalculation?.data.ovk.rows.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', - }} - /> - - - - - data={sapronakCalculation.data.pakan.rows} - columns={columns} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(sapronakCalculation) && - sapronakCalculation?.data.pakan.rows.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 SapronakCalculationTable; From 2d1cabb86b61def72179d476554db95b6798adfb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sun, 7 Dec 2025 14:59:25 +0700 Subject: [PATCH 052/105] refactor: update CreateExpensePayload, UpdateExpensePayload, and CreateExpenseRealizationPayload types --- .../expense/ExpenseRealizationContent.tsx | 8 +-- .../pages/expense/ExpenseRequestContent.tsx | 13 ++--- .../form/ExpenseRealizationForm.schema.ts | 8 +-- .../expense/form/ExpenseRealizationForm.tsx | 9 +--- ...ExpenseRealizationKandangDetailExpense.tsx | 12 ++--- .../expense/form/ExpenseRequestForm.schema.ts | 16 +++--- .../pages/expense/form/ExpenseRequestForm.tsx | 53 ++++++++++--------- .../ExpenseRequestKandangDetailExpense.tsx | 49 +++++++++-------- .../pages/expense/pdf/ExpensePDF.tsx | 14 ++--- src/services/api/expense.ts | 8 +-- src/types/api/expense.d.ts | 34 +++++------- 11 files changed, 107 insertions(+), 117 deletions(-) diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index 478cdadf..2b5b0a0a 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -207,7 +207,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -238,7 +238,7 @@ const ExpenseRealizationContent = ({
- + ) @@ -269,7 +269,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -300,7 +300,7 @@ const ExpenseRealizationContent = ({ - + ) diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index af8ceddc..0d7d959d 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -402,7 +402,10 @@ const ExpenseRequestContent = ({ @@ -529,7 +532,7 @@ const ExpenseRequestContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -550,7 +553,7 @@ const ExpenseRequestContent = ({ - + @@ -560,9 +563,7 @@ const ExpenseRequestContent = ({ - + diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts index 863238b9..77db761c 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts @@ -27,7 +27,7 @@ type ExpenseRealizationFormSchemaType = { label: string; }; quantity?: number; - total_cost?: number; + price?: number; notes?: string; }[]; }[]; @@ -82,7 +82,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema { realization.cost_items.forEach((costItem) => { - const unitPrice = - parseFloat(String(costItem.total_cost)) / - parseFloat(String(costItem.quantity)); - const realizationItem = { expense_nonstock_id: costItem.nonstock?.value as number, qty: parseFloat(String(costItem.quantity)) as number, - unit_price: unitPrice, - total_price: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', }; @@ -177,7 +172,7 @@ const ExpenseRealizationForm = ({ { nonstock: undefined, quantity: undefined, - total_cost: undefined, + price: undefined, notes: '', }, ], diff --git a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx index 8b889c5b..017a733e 100644 --- a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx @@ -48,7 +48,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC< }; const isExpenseRepeaterInputError = ( - column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', + column: 'nonstock' | 'quantity' | 'price' | 'notes', kandangExpenseIdx: number, expenseIdx: number ) => { @@ -112,7 +112,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC< - + @@ -163,17 +163,17 @@ const ExpenseRealizationKandangDetailExpense: React.FC< - + {type !== 'detail' && } @@ -178,10 +178,10 @@ const ExpenseRequestKandangDetailExpense: React.FC< {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - ); })} diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 96836bc6..c7da956d 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -23,10 +23,12 @@ type PurchaseRequestStaffApprovalFormSchemaType = { }; type PurchaseRequestManagerApprovalFormSchemaType = { + action: 'APPROVED' | 'REJECTED'; notes: string | null; }; type PurchaseRequestAcceptApprovalFormSchemaType = { + action: 'APPROVED' | 'REJECTED'; notes: string | null; items: { purchase_item?: { @@ -45,7 +47,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = { expedition_vendor_id: number; received_qty: number | string; transport_per_item: number | string; - transport_total: number | string; }[]; }; @@ -83,7 +84,6 @@ export type PurchaseAcceptApprovalItemSchema = { expedition_vendor_id: number; received_qty: number | string; transport_per_item: number | string; - transport_total: number | string; }; export type PurchaseDeleteItemsSchema = { @@ -152,6 +152,10 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema = Yup.object({ + action: Yup.mixed<'APPROVED' | 'REJECTED'>() + .oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED') + .required('Action wajib diisi!') + .default('APPROVED'), notes: Yup.string().nullable().default(null), }); @@ -230,20 +234,6 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema() - .required('Total biaya transport wajib diisi!') - .test( - 'is-valid-transport-total', - 'Total biaya transport harus berupa angka lebih dari atau sama dengan 0!', - function (value) { - if (value === '' || value === null || value === undefined) - return false; - const numValue = - typeof value === 'string' ? parseFloat(value) : value; - return !isNaN(numValue) && numValue >= 0; - } - ) - .typeError('Total biaya transport harus berupa angka!'), }); export const PurchaseRequestStaffApprovalFormSchema: Yup.ObjectSchema = @@ -368,6 +358,7 @@ export const PurchaseRequestManagerApprovalFormDefaultValues = ( purchase?: Purchase ): PurchaseRequestManagerApprovalFormSchemaType => { return { + action: 'APPROVED', notes: purchase?.notes ?? null, }; }; @@ -378,6 +369,10 @@ export type PurchaseRequestManagerApprovalFormValues = Yup.InferType< export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema = Yup.object({ + action: Yup.mixed<'APPROVED' | 'REJECTED'>() + .oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED') + .required('Action wajib diisi!') + .default('APPROVED'), notes: Yup.string().nullable().default(null), items: Yup.array() .of(PurchaseAcceptApprovalItemObjectSchema) @@ -388,6 +383,7 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema { return { + action: 'APPROVED', notes: purchase?.notes ?? null, items: purchase?.items ? purchase.items.map((item) => ({ @@ -419,7 +415,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( expedition_vendor_id: 0, received_qty: '', transport_per_item: '', - transport_total: '', })) : [ { @@ -431,7 +426,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( expedition_vendor_id: 0, received_qty: '', transport_per_item: '', - transport_total: '', }, ], }; diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index 2f3bbfb0..194e9534 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -925,6 +925,7 @@ const PurchaseOrderDetail = ({ color: 'success', onClick: async (notes) => { const payload: CreateManagerApprovalRequestPayload = { + action: 'APPROVED', notes: notes || null, }; diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 56cbd810..d075f6fe 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -42,7 +42,6 @@ export type PurchaseItem = { expedition_vendor_name?: string | null; received_qty?: number | null; transport_per_item?: number | null; - transport_total?: number | null; }; export type BasePurchase = { @@ -103,10 +102,12 @@ export type UpdateStaffApprovalRequestPayload = { }; export type CreateManagerApprovalRequestPayload = { + action: 'APPROVED' | 'REJECTED'; notes?: string | null; }; export type CreateAcceptApprovalRequestPayload = { + action: 'APPROVED' | 'REJECTED'; notes?: string; items: { purchase_item_id: number; @@ -117,7 +118,6 @@ export type CreateAcceptApprovalRequestPayload = { expedition_vendor_id: number; received_qty: number; transport_per_item: number; - transport_total: number; }[]; }; From f5663b82aae925b8b6e7225a28b3729a7fb5e335 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 14:43:53 +0700 Subject: [PATCH 061/105] refactor(ci): clean up .gitlab-ci.yml by removing unnecessary whitespace --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - From 68874a1c14afb00dbef269b7e170edfc184a1ead Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 17:42:23 +0700 Subject: [PATCH 062/105] feat(FE-311): Use latest_approval for purchase approvals --- .../purchase/order/PurchaseOrderDetail.tsx | 26 +++++++++---------- src/types/api/purchase/purchase.d.ts | 1 + 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index 194e9534..c2310e42 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -156,9 +156,9 @@ const PurchaseOrderDetail = ({ }, [goodsReceiptItems]); const approvalStep = useMemo(() => { - if (!initialValues?.approval) return null; - return initialValues.approval.step_number; - }, [initialValues?.approval]); + if (!initialValues?.latest_approval) return null; + return initialValues.latest_approval.step_number; + }, [initialValues?.latest_approval]); const { approvals, @@ -166,7 +166,7 @@ const PurchaseOrderDetail = ({ rawDataApprovals, refresh: refreshApprovals, } = useApprovalSteps({ - latestApproval: initialValues?.approval, + latestApproval: initialValues?.latest_approval, approvalLines: PURCHASE_ORDER_APPROVAL_LINE, moduleName: 'PURCHASES', moduleId: initialValues?.id?.toString() ?? '', @@ -180,16 +180,16 @@ const PurchaseOrderDetail = ({ approvalStep !== null && approvalStep >= 1 && approvalStep <= 3; const canDeleteItems = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; const hasReachedStep5 = rawDataApprovals?.some( (approval) => approval.step_number === 5 ); return currentStep === 3 && !hasReachedStep5; - }, [initialValues?.approval, rawDataApprovals]); + }, [initialValues?.latest_approval, rawDataApprovals]); const handleApprovalClick = () => { if (!approvalStep) return; @@ -222,18 +222,18 @@ const PurchaseOrderDetail = ({ }; const canShowPurchaseOrderInvoice = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep >= 3; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const canShowPenerimaanBarang = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep === 5; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const totalBeforeTax = useMemo(() => { return purchaseOrderItems.reduce( diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d075f6fe..94611eff 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -62,6 +62,7 @@ export type BasePurchase = { warehouse?: Warehouse; items?: PurchaseItem[]; approval?: BaseApproval; + latest_approval?: BaseApproval; }; export type Purchase = BaseMetadata & BasePurchase; From c7911f01f2f61c3fa8f50c9bbd0e43fd16f64b1d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 17:43:44 +0700 Subject: [PATCH 063/105] refactor(FE-311): Remove Total Transport header from approval form --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 0a10b1cd..d610acfe 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -361,10 +361,6 @@ const PurchaseOrderAcceptApprovalForm = ({ Transport/Item * - From ce75eb25d753a6836304f0341727ef23382931f0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 17:55:22 +0700 Subject: [PATCH 064/105] refactor(FE-311): Show previous values only in edit mode --- .../order/PurchaseOrderStaffApprovalForm.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 791e2592..f0519381 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -719,7 +719,10 @@ const PurchaseOrderStaffApprovalForm = ({ 'min-w-52 md:min-w-72 lg:min-w-80', }} bottomLabel={ - 'Previous: ' + purchaseItem.product.name + type === 'edit' + ? 'Previous: ' + + purchaseItem.product.name + : undefined } /> @@ -819,7 +822,11 @@ const PurchaseOrderStaffApprovalForm = ({ thousandSeparator=',' decimalSeparator='.' inputPrefix={'Rp'} - bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`} + bottomLabel={ + type === 'edit' + ? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}` + : undefined + } isError={ isRepeaterInputError( formItemIndex, @@ -857,7 +864,11 @@ const PurchaseOrderStaffApprovalForm = ({ thousandSeparator=',' decimalSeparator='.' inputPrefix={'Rp'} - bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`} + bottomLabel={ + type === 'edit' + ? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}` + : undefined + } isError={ isRepeaterInputError( formItemIndex, From a7d884b5f007e4d4ff29259ead7100cd79049fa3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 18:14:38 +0700 Subject: [PATCH 065/105] refactor(FE-311): Use latest_approval instead of approval --- .../order/PurchaseOrderStaffApprovalForm.tsx | 16 ++++++++-------- src/types/api/purchase/purchase.d.ts | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index f0519381..bc718eb6 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -61,7 +61,7 @@ const PurchaseOrderStaffApprovalForm = ({ return 'add'; } - const currentStep = initialValues?.approval?.step_number || 1; + const currentStep = initialValues?.latest_approval?.step_number || 1; switch (currentStep) { case 1: @@ -77,7 +77,7 @@ const PurchaseOrderStaffApprovalForm = ({ // Step 4+ (Penerimaan Barang dan selesai), tidak boleh edit kalau sudah disetujui return 'edit'; } - }, [rawDataApprovals, propType, initialValues?.approval?.step_number]); + }, [rawDataApprovals, propType, initialValues?.latest_approval?.step_number]); const router = useRouter(); const searchParams = useSearchParams(); @@ -93,16 +93,16 @@ const PurchaseOrderStaffApprovalForm = ({ // ===== UTILITY FUNCTIONS ===== const canUpdatePurchaseItems = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; return currentStep >= 3; - }, [initialValues?.approval]); + }, [initialValues?.latest_approval]); const canShowDeleteAddButtons = useMemo(() => { - if (!initialValues?.approval) return false; + if (!initialValues?.latest_approval) return false; - const currentStep = initialValues.approval.step_number; + const currentStep = initialValues.latest_approval.step_number; // Step 2 (Staff Purchase) dengan mode 'add' tidak boleh add/delete items // User hanya boleh input harga dan total harga untuk items yang sudah ada @@ -112,7 +112,7 @@ const PurchaseOrderStaffApprovalForm = ({ // Step 3 (Manager Purchase) boleh add/delete items return currentStep === 3; - }, [initialValues?.approval, type]); + }, [initialValues?.latest_approval, type]); const isRepeaterInputError = ( idx: number, diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 94611eff..93d6e610 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -61,7 +61,6 @@ export type BasePurchase = { location?: Location; warehouse?: Warehouse; items?: PurchaseItem[]; - approval?: BaseApproval; latest_approval?: BaseApproval; }; From 512ad5175eb9d981d1400c74a10e7a19615754ff Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 18:37:58 +0700 Subject: [PATCH 066/105] refactor(FE-311): Default received_qty and remove transport_total --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 2 +- src/components/pages/purchase/order/PurchaseOrderDetail.tsx | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d610acfe..ab2b373a 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -235,7 +235,7 @@ const PurchaseOrderAcceptApprovalForm = ({ vehicle_number: item.vehicle_number || '', expedition_vendor: null, expedition_vendor_id: 0, - received_qty: '', + received_qty: item.total_qty || '', transport_per_item: '', }; }); diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index c2310e42..2ca16480 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -544,11 +544,6 @@ const PurchaseOrderDetail = ({ accessorKey: 'transport_per_item', cell: (props) => formatCurrency(props.getValue() as number), }, - { - header: 'Transport Total', - accessorKey: 'transport_total', - cell: (props) => formatCurrency(props.getValue() as number), - }, ]; const summaryData = [ From b464432581a70e978e6f6c71addf615643cb8deb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 18:49:17 +0700 Subject: [PATCH 067/105] chore(FE): Add .claude to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d86875dd..e47b8ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude From 5deca5739fffed9f15a668cbb4c4dfde132945d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 20:08:35 +0700 Subject: [PATCH 068/105] refactor(FE-318): Add egg weight column and separate inputs --- .../recording/form/RecordingForm.tsx | 114 +++++++++--------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 582e8e78..f9314a9d 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2599,6 +2599,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { * + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( )} @@ -2674,58 +2683,55 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { /> + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - ))} + {header.column.getCanSort() && ( +
+ + +
+ )} + + + ); + })} ))} @@ -290,6 +324,28 @@ const Table = ({ ))} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + ))} + + )} +
- Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} -
{pengajuanItem.nonstock.name} {pengajuanItem.qty}{formatCurrency(pengajuanItem.total_price)}{formatCurrency(pengajuanItem.price)} {pengajuanItem.note ?? '-'}
{realisasiItem.nonstock.name} {realisasiItem.qty}{formatCurrency(realisasiItem.total_price)}{formatCurrency(realisasiItem.price)} {realisasiItem.note ?? '-'}
Tanggal Transaksi : - {formatDate(initialValues?.expense_date, 'DD MMMM YYYY')} + {formatDate( + initialValues?.transaction_date, + 'DD MMMM YYYY' + )}
Nonstock Total KuantitasTotal BiayaHarga Satuan Catatan
{pengajuanItem.nonstock.name} {pengajuanItem.qty} - {formatCurrency(pengajuanItem.total_price)} - {formatCurrency(pengajuanItem.price)} {pengajuanItem.note ?? '-'}
Nonstock Total KuantitasTotal BiayaHarga Satuan Catatan
= documents: Yup.array().of(Yup.mixed().required()).optional(), - cost_per_kandangs: Yup.array() + expense_nonstocks: Yup.array() .of( Yup.object({ kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(), @@ -86,7 +86,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = label: Yup.string().required(), }).required('Nonstock wajib diisi!'), quantity: Yup.number().required('Total kuantitas wajib diisi!'), - total_cost: Yup.number().required('Total biaya wajib diisi!'), + price: Yup.number().required('Harga satuan wajib diisi!'), notes: Yup.string(), }) ) @@ -128,8 +128,8 @@ export const getExpenseFormInitialValues = ( label: initialValues.location.name, } : undefined, - transaction_date: initialValues?.expense_date - ? formatDate(initialValues.expense_date, 'YYYY-MM-DD') + transaction_date: initialValues?.transaction_date + ? formatDate(initialValues.transaction_date, 'YYYY-MM-DD') : undefined, kandangs: initialValues?.kandangs.map((kandang) => ({ id: kandang.kandang_id, @@ -148,7 +148,7 @@ export const getExpenseFormInitialValues = ( })), deleted_documents: [], documents: [], - cost_per_kandangs: initialValues?.kandangs + expense_nonstocks: initialValues?.kandangs ? initialValues.kandangs.map((kandangExpense) => ({ kandang_id: kandangExpense.kandang_id, cost_items: kandangExpense.pengajuans @@ -158,7 +158,7 @@ export const getExpenseFormInitialValues = ( label: expenseItem.nonstock.name, }, quantity: expenseItem.qty, - total_cost: expenseItem.total_price, + price: expenseItem.price, notes: expenseItem.note, })) : [], diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index e47f2f76..d52bde0d 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -110,12 +110,12 @@ const ExpenseRequestForm = ({ transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], - cost_per_kandangs: values.cost_per_kandangs.map((costPerKandang) => ({ - kandang_id: costPerKandang.kandang_id, - cost_items: costPerKandang.cost_items.map((costItem) => ({ + expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({ + kandang_id: expenseNonstock.kandang_id, + cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, - total_cost: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), })), @@ -132,13 +132,13 @@ const ExpenseRequestForm = ({ transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], - cost_per_kandang: values.cost_per_kandangs.map( - (costPerKandang) => ({ - kandang_id: costPerKandang.kandang_id, - cost_items: costPerKandang.cost_items.map((costItem) => ({ + expense_nonstocks: values.expense_nonstocks.map( + (expenseNonstock) => ({ + kandang_id: expenseNonstock.kandang_id, + cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, - total_cost: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), }) @@ -179,53 +179,54 @@ const ExpenseRequestForm = ({ formik.setFieldValue('location', val); formik.setFieldValue('kandangs', []); - formik.setFieldValue('cost_per_kandangs', []); + formik.setFieldValue('expense_nonstocks', []); }; const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { formik.setFieldTouched('kandangs', true); formik.setFieldValue('kandangs', kandangs); - const newCostPerKandangs = [...(formik.values.cost_per_kandangs ?? [])]; + const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])]; - // add new cost_per_kandangs + // add new expense_nonstocks kandangs.forEach((kandangItem) => { - const isKandangExistInCostPerKandangs = newCostPerKandangs.find( - (costPerKandangItem) => costPerKandangItem.kandang_id === kandangItem.id + const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find( + (expenseNonstockItem) => + expenseNonstockItem.kandang_id === kandangItem.id ); - if (isKandangExistInCostPerKandangs) return; + if (isKandangExistInExpenseNonstocks) return; - newCostPerKandangs.push({ + newExpenseNonstocks.push({ kandang_id: kandangItem.id, cost_items: [ { nonstock: undefined, quantity: undefined, - total_cost: undefined, + price: undefined, notes: '', }, ], }); }); - // prune cost_per_kandangs + // prune expense_nonstocks const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); - const deletedCostPerKandangsIdx: number[] = []; + const deletedExpenseNonstocksIdx: number[] = []; - newCostPerKandangs.forEach((costPerKandang, idx) => { - const isCostPerKandangValid = kandangIds.has(costPerKandang.kandang_id); + newExpenseNonstocks.forEach((expenseNonstock, idx) => { + const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id); - if (!isCostPerKandangValid) { - deletedCostPerKandangsIdx.push(idx); + if (!isExpenseNonstockValid) { + deletedExpenseNonstocksIdx.push(idx); } }); - deletedCostPerKandangsIdx.forEach((deletedCostPerKandangIdx) => { - newCostPerKandangs.splice(deletedCostPerKandangIdx, 1); + deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => { + newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1); }); - formik.setFieldValue('cost_per_kandangs', newCostPerKandangs); + formik.setFieldValue('expense_nonstocks', newExpenseNonstocks); }; const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { diff --git a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx index 73e6c9b7..11f54585 100644 --- a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx @@ -41,28 +41,28 @@ const ExpenseRequestKandangDetailExpense: React.FC< val: OptionType | OptionType[] | null ) => { formik.setFieldTouched( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, true ); formik.setFieldValue( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, val ); }; const addExpenseItemHandler = (kandangExpenseIdx: number) => { const newExpensesValue = [ - ...formik.values.cost_per_kandangs[kandangExpenseIdx].cost_items, + ...formik.values.expense_nonstocks[kandangExpenseIdx].cost_items, { nonstock: undefined, - total_cost: undefined, + price: undefined, quantity: undefined, notes: '', }, ]; formik.setFieldValue( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items`, newExpensesValue ); }; @@ -71,28 +71,28 @@ const ExpenseRequestKandangDetailExpense: React.FC< kandangExpenseIdx: number, expenseIdx: number ) => { - const path = `cost_per_kandangs[${kandangExpenseIdx}].cost_items`; + const path = `expense_nonstocks[${kandangExpenseIdx}].cost_items`; // trims values, errors, and touched at expenseIdx removeArrayItemAndSync(formik, path, expenseIdx); }; const isExpenseRepeaterInputError = ( - column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', + column: 'nonstock' | 'quantity' | 'price' | 'notes', kandangExpenseIdx: number, expenseIdx: number ) => { return ( - formik.touched.cost_per_kandangs?.[kandangExpenseIdx]?.cost_items?.[ + formik.touched.expense_nonstocks?.[kandangExpenseIdx]?.cost_items?.[ expenseIdx ]?.[column] && Boolean( - formik.errors.cost_per_kandangs?.[kandangExpenseIdx] instanceof + formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof Object && - formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ + formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ expenseIdx ] instanceof Object && - formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ + formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ expenseIdx ]?.[column] ) @@ -113,7 +113,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
- {(formik.values.cost_per_kandangs.length === 0 || + {(formik.values.expense_nonstocks.length === 0 || !formik.values.supplier?.value) && (

@@ -122,9 +122,9 @@ const ExpenseRequestKandangDetailExpense: React.FC<

)} - {formik.values.cost_per_kandangs.length > 0 && + {formik.values.expense_nonstocks.length > 0 && formik.values.supplier?.value && - formik.values.cost_per_kandangs.map( + formik.values.expense_nonstocks.map( (kandangExpense, kandangExpenseIdx) => { const kandangName = formik.values.kandangs?.find( (kandang) => kandang.id === kandangExpense.kandang_id @@ -147,7 +147,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
Nonstock Total KuantitasTotal BiayaHarga Satuan CatatanAksi
{ { label: 'Vendor', value: expense?.supplier.name }, { label: 'Tanggal Transaksi', - value: formatDate(expense?.expense_date, 'DD MMMM YYYY'), + value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), }, { label: 'Tanggal Realisasi', @@ -326,7 +326,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { let expenseRequestTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseRequestTotal += item.total_price) + (item) => (expenseRequestTotal += item.price) ); return ( @@ -374,7 +374,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { - Total Biaya + Harga Satuan { ]} > - {formatCurrency(pengajuan.total_price)} + {formatCurrency(pengajuan.price)} { let expenseRealizationTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseRealizationTotal += item.total_price) + (item) => (expenseRealizationTotal += item.price) ); return ( @@ -532,7 +532,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { - Total Biaya + Harga Satuan { ]} > - {formatCurrency(realisasi.total_price)} + {formatCurrency(realisasi.price)} ; - project_flock_kandang: { - id: number; - kandang_id: number; - }; + created_at: string; }[]; realisasi?: { id: number; + expense_nonstock_id: number; qty: number; - unit_price: number; - total_price: number; - date: string; + price: number; note?: string; nonstock: Pick; - project_flock_kandang: { - id: number; - kandang_id: number; - }; + created_at: string; }[]; }[]; total_pengajuan: number; @@ -65,12 +60,12 @@ export type CreateExpensePayload = { transaction_date: string; supplier_id: number; documents: File[]; - cost_per_kandangs: { + expense_nonstocks: { kandang_id: number; cost_items: { nonstock_id: number; quantity: number; - total_cost: number; + price: number; notes: string; }[]; }[]; @@ -81,12 +76,12 @@ export type UpdateExpensePayload = { transaction_date: string; supplier_id: number; documents: File[]; - cost_per_kandang: { + expense_nonstocks: { kandang_id: number; cost_items: { nonstock_id: number; quantity: number; - total_cost: number; + price: number; notes: string; }[]; }[]; @@ -98,8 +93,7 @@ export type CreateExpenseRealizationPayload = { realizations: { expense_nonstock_id: number; qty: number; - unit_price: number; - total_price: number; + price: number; notes: string; }[]; }; From 5782abb531d974ec05eb18a98fd005eb6ceabf4b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sun, 7 Dec 2025 14:59:51 +0700 Subject: [PATCH 053/105] refactor: change expense_date to transaction_date --- src/components/pages/expense/ExpensesTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 3a50f233..bbcb6c4e 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -263,11 +263,11 @@ const ExpensesTable = () => { }, }, { - accessorKey: 'expense_date', + accessorKey: 'transaction_date', header: 'Tanggal Pengajuan', cell: (props) => - props.row.original.expense_date - ? formatDate(props.row.original.expense_date, 'DD MMM YYYY') + props.row.original.transaction_date + ? formatDate(props.row.original.transaction_date, 'DD MMM YYYY') : '-', }, { From dc0fd7a3ed121ee43588b4fcf7ec116abd181cfd Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sun, 7 Dec 2025 15:00:25 +0700 Subject: [PATCH 054/105] chore: format code --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - From c3c1bbbe9678d69e2b4b1acbc85eb73c32b47934 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 08:57:57 +0700 Subject: [PATCH 055/105] feat(FE-326): Add egg weight field to recording forms --- .../recording/form/RecordingForm.schema.ts | 8 ++++ .../recording/form/RecordingForm.tsx | 40 ++++++++++++++++++- src/types/api/production/recording.d.ts | 2 + 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 4d72e053..99496843 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -32,6 +32,7 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & { eggs: { product_warehouse_id: number; qty: number | string; + weight: number | string; }[]; }; @@ -62,6 +63,7 @@ export type DepletionSchema = { export type EggSchema = { product_warehouse_id: number; qty: number | string; + weight: number | string; }; const BodyWeightObjectSchema: Yup.ObjectSchema = Yup.object({ @@ -109,6 +111,10 @@ const EggObjectSchema: Yup.ObjectSchema = Yup.object({ .required('Jumlah telur wajib diisi!') .min(1, 'Jumlah telur tidak boleh 0!') .typeError('Jumlah telur harus berupa angka!'), + weight: Yup.number() + .required('Berat telur wajib diisi!') + .min(1, 'Berat telur minimal 1 gram!') + .typeError('Berat telur harus berupa angka!'), }); export const RecordingGrowingFormSchema: Yup.ObjectSchema = @@ -295,10 +301,12 @@ export const getRecordingLayingFormInitialValues = ( eggs: initialValues?.eggs?.map((egg: CreateEggPayload) => ({ product_warehouse_id: egg.product_warehouse_id, qty: egg.qty, + weight: egg.weight, })) ?? [ { product_warehouse_id: 0, qty: '', + weight: '', }, ], }); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 43ffc98b..582e8e78 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -181,6 +181,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { eggs: (values.eggs ?? []).map((egg) => ({ product_warehouse_id: egg.product_warehouse_id, qty: Number(egg.qty) || 0, + weight: + typeof egg.weight === 'number' + ? egg.weight + : parseFloat(String(egg.weight)) || 0, })), }; }, @@ -1148,7 +1152,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (hasSameDayRecording) { toast.error( - `Recording untuk hari ${nextDayRecording.next_day} sudah ada. + `Recording untuk hari ${nextDayRecording.next_day} sudah ada. Tidak bisa membuat recording duplikat, mohon perbarui recording yang sudah ada terlebih dahulu.` ); return; @@ -1485,6 +1489,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { [formik] ); + const handleEggWeightChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`eggs.${idx}.weight`, value); + }, + [formik] + ); + const removeEgg = (idx: number) => { const updatedEggs = ( formik.values as RecordingLayingFormValues @@ -2688,6 +2700,32 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { placeholder='Masukkan jumlah telur' /> +
+ +
diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index e7b28f47..46579509 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -53,6 +53,7 @@ export type RecordingEgg = { recording_id: number; product_warehouse_id: number; qty: number; + weight: number; created_by: User; product_warehouse: ProductWarehouse; gradings?: { @@ -129,6 +130,7 @@ export type CreateGradingRecordingPayload = { export type CreateEggPayload = { product_warehouse_id: number; qty: number; + weight: number; }; export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { From df3f3422145677d8469d39a73a2097492d342aba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 10:07:00 +0700 Subject: [PATCH 056/105] chore(CVE): update Next.js version to ^15.5.7 in package.json and package-lock.json --- package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..535bb986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..85485ee3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "^15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 86a0faaa52f9464bb3033413ca3d70595dfdd983 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 10:09:21 +0700 Subject: [PATCH 057/105] chore(ci): clean up .gitlab-ci.yml by removing unnecessary whitespace --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - From 58fb9b0c082c21790bc18d6f7c28bb7d367c9b22 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 14:07:25 +0700 Subject: [PATCH 058/105] chore(CVE): Bump Next to 15.5.7 and ignore .claude --- .gitignore | 3 ++ package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index d86875dd..e47b8ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude diff --git a/package-lock.json b/package-lock.json index ec1316ae..d73a1b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..4b9fdac7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 32ffc1f14c9e8f16f276cf259275f72ffd5a4065 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 14:08:40 +0700 Subject: [PATCH 059/105] chore(prettier): Remove trailing whitespace in .gitlab-ci.yml --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - From 3a7f1f48121aba5955c270ea29d4c7324ff1a978 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 14:42:39 +0700 Subject: [PATCH 060/105] refactor(FE-311): remove transport_total field and update approval actions --- .gitignore | 3 + package-lock.json | 92 ++++++++++--------- package.json | 2 +- .../order/PurchaseOrderAcceptApprovalForm.tsx | 60 +----------- .../form/order/PurchaseOrderForm.schema.ts | 32 +++---- .../purchase/order/PurchaseOrderDetail.tsx | 1 + src/types/api/purchase/purchase.d.ts | 4 +- 7 files changed, 73 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index d86875dd..7d6264e6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ec1316ae..d73a1b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5678,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 7396d49d..4b9fdac7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 79762da9..0a10b1cd 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -64,7 +64,6 @@ const PurchaseOrderAcceptApprovalForm = ({ | 'expedition_vendor_id' | 'received_qty' | 'transport_per_item' - | 'transport_total' ): { isError: boolean; errorMessage: string } => { const touchedItem = formik.touched.items?.[idx]; const errorItem = formik.errors.items?.[idx] as @@ -163,6 +162,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validateOnBlur: true, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { + action: 'APPROVED', notes: values.notes || '', items: values.items?.map((formItem) => { @@ -181,10 +181,6 @@ const PurchaseOrderAcceptApprovalForm = ({ typeof formItem.transport_per_item === 'string' ? parseFloat(formItem.transport_per_item) || 0 : formItem.transport_per_item || 0, - transport_total: - typeof formItem.transport_total === 'string' - ? parseFloat(formItem.transport_total) || 0 - : formItem.transport_total || 0, }; }) || [], }; @@ -241,7 +237,6 @@ const PurchaseOrderAcceptApprovalForm = ({ expedition_vendor_id: 0, received_qty: '', transport_per_item: '', - transport_total: '', }; }); formik.setFieldValue('items', updatedItems); @@ -301,7 +296,7 @@ const PurchaseOrderAcceptApprovalForm = ({ // ===== PURCHASE ITEM OPERATIONS ===== const handlePurchaseItemChange = ( idx: number, - field: 'received_qty' | 'transport_per_item' | 'transport_total', + field: 'received_qty' | 'transport_per_item', value: string | number ) => { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; @@ -318,26 +313,6 @@ const PurchaseOrderAcceptApprovalForm = ({ : parseFloat( formik.values.items?.[idx]?.transport_per_item as string ) || 0; - - if (receivedQty > 0 && transportPerItem >= 0) { - const calculatedTransportTotal = receivedQty * transportPerItem; - formik.setFieldValue( - `items.${idx}.transport_total`, - calculatedTransportTotal - ); - } - } - - if (field === 'transport_total') { - const receivedQty = - parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0; - if (receivedQty > 0 && numValue >= 0) { - const calculatedTransportPerItem = numValue / receivedQty; - formik.setFieldValue( - `items.${idx}.transport_per_item`, - calculatedTransportPerItem - ); - } } }; @@ -657,37 +632,6 @@ const PurchaseOrderAcceptApprovalForm = ({ }} /> - - handlePurchaseItemChange( - idx, - 'transport_total', - e.target.value - ) - } - onBlur={formik.handleBlur} - placeholder='Masukkan total transport' - allowNegative={false} - decimalScale={2} - thousandSeparator=',' - decimalSeparator='.' - inputPrefix={'Rp'} - isError={ - isRepeaterInputError(idx, 'transport_total').isError - } - errorMessage={ - isRepeaterInputError(idx, 'transport_total') - .errorMessage - } - className={{ - wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', - }} - /> -
- Total Transport - * -
+ Berat (gram) + + * + + Action -
- -
-
- -
+ +
+ @@ -2908,7 +2914,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { > Submit - {isLayingCategory && ( + {/*{isLayingCategory && ( { Next Step: Grading - )} + )}*/} )} From 305b8e5005d47a1b8f86eb9eab9790db1a906d79 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 20:12:18 +0700 Subject: [PATCH 069/105] refactor(FE-319): Remove Grading-Telur step from RECORDINGS workflow --- src/config/approval-line.ts | 18 +++--------------- src/config/constant.ts | 4 ---- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/src/config/approval-line.ts b/src/config/approval-line.ts index 3af866c6..48b1268b 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -51,14 +51,10 @@ export const MARKETING_APPROVAL_LINE: ApprovalLine = [ export const RECORDING_APPROVAL_LINE: ApprovalLine = [ { step_number: 1, - step_name: 'Grading-Telur', - }, - { - step_number: 2, step_name: 'Pengajuan', }, { - step_number: 3, + step_number: 2, step_name: 'Disetujui', }, ] as const; @@ -66,14 +62,10 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [ { step_number: 1, - step_name: 'Grading-Telur', - }, - { - step_number: 2, step_name: 'Pengajuan', }, { - step_number: 3, + step_number: 2, step_name: 'Disetujui', }, ] as const; @@ -81,14 +73,10 @@ export const GROWING_RECORDING_APPROVAL_LINE: ApprovalLine = [ export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [ { step_number: 1, - step_name: 'Grading-Telur', - }, - { - step_number: 2, step_name: 'Pengajuan', }, { - step_number: 3, + step_number: 2, step_name: 'Disetujui', }, ] as const; diff --git a/src/config/constant.ts b/src/config/constant.ts index dc36025b..d4e08942 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -261,10 +261,6 @@ export const APPROVAL_WORKFLOWS = [ { key: 'RECORDINGS', steps: [ - { - step_number: 1, - step_name: 'Grading-Telur', - }, { step_number: 2, step_name: 'Pengajuan', From 2e6a724b2f2361268fb86cbe29e91edc19cc396b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 20:13:10 +0700 Subject: [PATCH 070/105] refactor(FE-319): Use approval step 2 and remove grading button --- .../recording/form/RecordingForm.tsx | 74 +------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index f9314a9d..f6058d70 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -112,7 +112,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return ( recording?.approval?.action === 'APPROVED' && recording?.approval?.step_name === 'Disetujui' && - recording?.approval?.step_number === 3 + recording?.approval?.step_number === 2 ); }, []); @@ -2914,78 +2914,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { > Submit - {/*{isLayingCategory && ( - - - - )}*/} )} From 545af8267a93cc3ad738dc6e5fb6dbb3d8ffed42 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 23:33:34 +0700 Subject: [PATCH 071/105] feat(FE-319): Refactor recording types and simplify payloads --- src/types/api/production/recording.d.ts | 46 +++---------------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 46579509..9bed7685 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -9,8 +9,7 @@ export type ProductionMetrics = { cum_intake: number; fcr_value: number; total_chick_qty: number; - daily_depletion_rate?: number; - cum_depletion?: number; + cum_depletion: number; }; export type BaseRecording = { @@ -18,43 +17,33 @@ export type BaseRecording = { project_flock_kandang_id: number; record_datetime: string; day: number; - created_by: User; + project_flock_category?: 'GROWING' | 'LAYING'; } & ProductionMetrics; export type RecordingBW = { - id: number; - recording_id: number; avg_weight: number; qty: number; total_weight: number; }; export type RecordingDepletion = { - id: number; - recording_id: number; product_warehouse_id: number; qty: number; product_warehouse: ProductWarehouse; }; export type RecordingStock = { - id: number; - recording_id: number; product_warehouse_id: number; usage_amount?: number; - usage_qty: number; - qty: number; pending_qty: number; product_warehouse: ProductWarehouse; }; export type RecordingEgg = { id: number; - recording_id: number; product_warehouse_id: number; qty: number; weight: number; - created_by: User; product_warehouse: ProductWarehouse; gradings?: { grade: string; @@ -72,19 +61,12 @@ export type GradingEgg = { export type Recording = BaseMetadata & BaseRecording & { - project_flock_category?: 'GROWING' | 'LAYING'; approval?: BaseApproval; - egg_grading_status?: string | null; - egg_grading_pending_qty?: number | null; - egg_grading_completed_qty?: number | null; + created_user: User; body_weights?: RecordingBW[]; depletions?: RecordingDepletion[]; stocks?: RecordingStock[]; eggs?: RecordingEgg[]; - recording_bws?: RecordingBW[]; - recording_depletions?: RecordingDepletion[]; - recording_stocks?: RecordingStock[]; - recording_eggs?: RecordingEgg[]; grading_eggs?: GradingEgg[]; }; @@ -109,24 +91,6 @@ export type CreateGrowingRecordingPayload = { }[]; }; -export type CreateGradingPayload = { - eggs_grading: { - recording_egg_id: number; - grade: string; - qty: number; - }[]; -}; - -export type UpdateGradingPayload = CreateGradingPayload; - -export type CreateGradingRecordingPayload = { - eggs_grading: { - recording_egg_id: number; - grade: string; - qty: number; - }[]; -}; - export type CreateEggPayload = { product_warehouse_id: number; qty: number; @@ -139,11 +103,9 @@ export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & { export type CreateRecordingPayload = | CreateGrowingRecordingPayload - | CreateLayingRecordingPayload - | CreateGradingRecordingPayload; + | CreateLayingRecordingPayload; export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; -export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload; From 7c4bd81364108bfce196bc1a7e0dc438295e41f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 23:34:01 +0700 Subject: [PATCH 072/105] feat(FE-319): Remove recording grading feature --- .../production/recording/grading/add/page.tsx | 49 - .../recording/grading/detail/edit/page.tsx | 53 - .../recording/grading/detail/page.tsx | 52 - .../production/recording/grading/layout.tsx | 11 - .../recording/grading/form/GradingForm.tsx | 1050 ----------------- src/services/api/production.ts | 24 - 6 files changed, 1239 deletions(-) delete mode 100644 src/app/production/recording/grading/add/page.tsx delete mode 100644 src/app/production/recording/grading/detail/edit/page.tsx delete mode 100644 src/app/production/recording/grading/detail/page.tsx delete mode 100644 src/app/production/recording/grading/layout.tsx delete mode 100644 src/components/pages/production/recording/grading/form/GradingForm.tsx diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx deleted file mode 100644 index 9b918d98..00000000 --- a/src/app/production/recording/grading/add/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const AddGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const recordingId = searchParams.get('recording_id'); - - const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId && recordingId !== 'new' ? [recordingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if ( - recordingId && - recordingId !== 'new' && - !isLoadingRecording && - (!recording || !isResponseSuccess(recording)) - ) { - router.replace('/404'); - return; - } - - return ( -
- {recordingId && recordingId !== 'new' && isLoadingRecording && ( - - )} - {(!recordingId || - recordingId === 'new' || - (!isLoadingRecording && recording && isResponseSuccess(recording))) && ( - - )} -
- ); -}; - -export default AddGrading; diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx deleted file mode 100644 index 0a65f528..00000000 --- a/src/app/production/recording/grading/detail/edit/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const EditGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const recordingId = searchParams.get('recordingId'); - const gradingId = searchParams.get('gradingId'); - - const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId ? [recordingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if (!recordingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingRecording && ( - - )} - {!isLoadingRecording && recording && isResponseSuccess(recording) && ( - egg.id === parseInt(gradingId || '0') - )} - /> - )} -
- ); -}; - -export default EditGrading; diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx deleted file mode 100644 index 6a5fbcba..00000000 --- a/src/app/production/recording/grading/detail/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; -import { RecordingApi } from '@/services/api/production'; -import { isResponseSuccess } from '@/lib/api-helper'; - -const DetailGrading = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const gradingId = searchParams.get('gradingId'); - - const { data: grading, isLoading: isLoadingGrading } = useSWR( - gradingId ? [gradingId] : null, - ([id]) => RecordingApi.getSingle(parseInt(id)) - ); - - if (!gradingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingGrading && ( - - )} - {!isLoadingGrading && grading && isResponseSuccess(grading) && ( - egg.id === parseInt(gradingId) - )} - /> - )} -
- ); -}; - -export default DetailGrading; diff --git a/src/app/production/recording/grading/layout.tsx b/src/app/production/recording/grading/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/production/recording/grading/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx deleted file mode 100644 index 417c6356..00000000 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ /dev/null @@ -1,1050 +0,0 @@ -'use client'; - -import { useMemo, useState, useEffect, useCallback } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useFormik } from 'formik'; -import { Icon } from '@iconify/react'; - -import Button from '@/components/Button'; -import NumberInput from '@/components/input/NumberInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; -import CheckboxInput from '@/components/input/CheckboxInput'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import Card from '@/components/Card'; -import Badge from '@/components/Badge'; - -import { - CreateGradingPayload, - UpdateGradingPayload, - RecordingEgg, - GradingEgg, - Recording, -} from '@/types/api/production/recording'; -import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { type BaseApiResponse } from '@/types/api/api-general'; - -import { - RecordingGradingFormSchema, - RecordingGradingFormValues, - UpdateRecordingGradingFormSchema, - getRecordingGradingFormInitialValues, -} from '@/components/pages/production/recording/form/RecordingForm.schema'; - -import { cn, formatDate } from '@/lib/helper'; -import toast from 'react-hot-toast'; -import { isResponseError } from '@/lib/api-helper'; - -import { - RecordingApi, - ProjectFlockKandangApi, -} from '@/services/api/production'; - -import { useModal } from '@/components/Modal'; -import useSWR from 'swr'; - -// INTERFACES & PROPS -interface GradingFormProps { - type?: 'add' | 'edit' | 'detail'; - initialValues?: RecordingEgg & { - grading_eggs?: GradingEgg[]; - gradings?: { grade: string; qty: number }[]; - }; -} - -const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { - // HOOKS & ROUTER - const router = useRouter(); - const searchParams = useSearchParams(); - const recordingId = searchParams.get('recording_id'); - - // STATE MANAGEMENT - const [selectedGradingItems, setSelectedGradingItems] = useState( - [] - ); - const [gradingFormErrorMessage, setGradingFormErrorMessage] = useState(''); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const deleteModal = useModal(); - - // API DATA FETCHING - const recordingUrl = useMemo(() => { - const recordingIdToUse = recordingId; - if (!recordingIdToUse) return null; - return `${RecordingApi.basePath}/${recordingIdToUse}`; - }, [recordingId]); - - const { data: recordingData } = useSWR( - recordingUrl, - recordingUrl ? RecordingApi.getAllFetcher : null - ); - - // DATA PROCESSING - const recording = - recordingData?.status === 'success' - ? (recordingData.data as unknown as Recording) - : undefined; - - const projectFlockKandangUrl = useMemo(() => { - if (!recording?.project_flock_kandang_id) return null; - return `${ProjectFlockKandangApi.basePath}/${recording.project_flock_kandang_id}`; - }, [recording?.project_flock_kandang_id]); - - const { data: projectFlockKandangData } = useSWR( - projectFlockKandangUrl, - projectFlockKandangUrl ? ProjectFlockKandangApi.getAllFetcher : null - ); - - const projectFlockKandang = - projectFlockKandangData?.status === 'success' - ? (projectFlockKandangData.data as unknown as ProjectFlockKandang) - : undefined; - - const konsumsiBaikEggData = useMemo(() => { - if (!recording?.eggs) return null; - - const konsumsiBaikEgg = recording.eggs.find((egg: RecordingEgg) => - egg.product_warehouse?.product?.name - ?.toLowerCase() - .includes('konsumsi baik') - ); - - return konsumsiBaikEgg || null; - }, [recording]); - - const totalKonsumsiBaikEggs = konsumsiBaikEggData?.qty || 0; - const konsumsiBaikEggId = konsumsiBaikEggData?.id; - - const isDataLoading = - !recording || - (totalKonsumsiBaikEggs === 0 && - recording?.project_flock_category === 'LAYING'); - - // FORM HANDLERS - const createGradingHandler = useCallback( - async (payload: CreateGradingPayload) => { - const res = (await RecordingApi.createGrading(payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to add Grading'); - return; - } - - toast.success(res?.message || 'Successfully added Grading!'); - router.push('/production/recording'); - }, - [router] - ); - - const updateGradingHandler = useCallback( - async (gradingId: number, payload: UpdateGradingPayload) => { - const res = (await RecordingApi.updateGrading(gradingId, payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to update Grading'); - return; - } - toast.success(res?.message || 'Successfully updated Grading!'); - router.refresh(); - router.push('/production/recording'); - }, - [router] - ); - - const deleteRecordingClickHandler = useCallback(() => { - deleteModal.openModal(); - }, [deleteModal]); - - const confirmationModalDeleteClickHandler = useCallback(async () => { - if (!initialValues?.id) return; - - setIsDeleteLoading(true); - try { - const res = (await RecordingApi.deleteGrading(initialValues.id)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setGradingFormErrorMessage(res?.message || 'Failed to delete Grading'); - return; - } - deleteModal.closeModal(); - toast.success(res?.message || 'Successfully delete Grading!'); - router.push('/production/recording'); - } catch { - setGradingFormErrorMessage('Failed to delete Grading'); - } finally { - setIsDeleteLoading(false); - } - }, [deleteModal, initialValues?.id, router]); - - // FORMIK SETUP - const formikInitialValues = useMemo(() => { - let recordingEggId: number | undefined = konsumsiBaikEggId; - - if (!recordingEggId && initialValues?.id) { - recordingEggId = initialValues.id; - } - - if (!recordingEggId) { - recordingEggId = parseInt(recordingId || '0') || 0; - } - - let gradingData: { - recording_egg_id: number; - grade: string; - qty: number; - }[] = []; - - if (initialValues?.grading_eggs && initialValues.grading_eggs.length > 0) { - gradingData = initialValues.grading_eggs.map((grading: GradingEgg) => ({ - recording_egg_id: recordingEggId, - grade: grading.grade, - qty: grading.qty, - })); - } else if (initialValues?.gradings && initialValues.gradings.length > 0) { - gradingData = initialValues.gradings.map( - (grading: { grade: string; qty: number }) => ({ - recording_egg_id: recordingEggId, - grade: grading.grade, - qty: grading.qty, - }) - ); - } - - return getRecordingGradingFormInitialValues({ - recording_egg_id: recordingEggId, - eggs_grading: gradingData, - }); - }, [initialValues, recordingId, konsumsiBaikEggId]); - - const formik = useFormik({ - initialValues: formikInitialValues, - enableReinitialize: true, - validationSchema: (() => { - return type === 'edit' - ? UpdateRecordingGradingFormSchema - : RecordingGradingFormSchema; - })(), - validateOnChange: true, - validateOnBlur: true, - onSubmit: async (values) => { - const gradingPayload = { - eggs_grading: (values.eggs_grading ?? []).map((grading) => ({ - recording_egg_id: grading.recording_egg_id, - grade: grading.grade, - qty: grading.qty || 0, - })), - }; - - switch (type) { - case 'add': - await createGradingHandler(gradingPayload as CreateGradingPayload); - break; - case 'edit': - await updateGradingHandler( - initialValues?.id as number, - gradingPayload as UpdateGradingPayload - ); - break; - } - }, - }); - - const currentGradingTotal = useMemo(() => { - return (formik.values.eggs_grading || []).reduce((total, grading) => { - return total + (Number(grading.qty) || 0); - }, 0); - }, [formik.values.eggs_grading]); - - const isGradingExceedsAvailable = currentGradingTotal > totalKonsumsiBaikEggs; - const isGradingIncomplete = - currentGradingTotal < totalKonsumsiBaikEggs && totalKonsumsiBaikEggs > 0; - const hasUserStartedGrading = currentGradingTotal > 0; - - // GRADING HANDLERS - const addGrading = () => { - let recordingEggId: number | undefined = konsumsiBaikEggId; - - if (!recordingEggId && initialValues?.id) { - recordingEggId = initialValues.id; - } - - if (!recordingEggId) { - recordingEggId = parseInt(recordingId || '0') || 0; - } - - const newGrading = [ - ...(formik.values.eggs_grading || []), - { - recording_egg_id: recordingEggId, - grade: '', - qty: '', - }, - ]; - formik.setFieldValue('eggs_grading', newGrading); - }; - - const handleGradingGradeChangeWrapper = useCallback( - (idx: number) => (selectedOption: OptionType | OptionType[] | null) => { - const option = selectedOption as OptionType | null; - formik.setFieldValue(`eggs_grading.${idx}.grade`, option?.label || ''); - }, - [formik] - ); - - const handleGradingQtyChangeWrapper = useCallback( - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - formik.setFieldValue(`eggs_grading.${idx}.qty`, value); - }, - [formik] - ); - - const removeGrading = (idx: number) => { - const updatedGrading = formik.values.eggs_grading?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('eggs_grading', updatedGrading); - }; - - const removeSelectedGrading = () => { - const updatedGrading = formik.values.eggs_grading?.filter( - (_, idx) => !selectedGradingItems.includes(idx) - ); - formik.setFieldValue('eggs_grading', updatedGrading); - setSelectedGradingItems([]); - }; - - // VALIDATION HELPERS - const isRepeaterInputError = ( - arrayName: 'eggs_grading', - column: string, - idx: number - ) => { - const touched = formik.touched as Record; - const errors = formik.errors as Record; - - if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { - return { - isError: false, - errorMessage: '', - }; - } - - const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< - string, - unknown - >; - const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< - string, - unknown - >; - - return { - isError: touchedField && Boolean(errorField?.[column]), - errorMessage: - touchedField && errorField?.[column] - ? (errorField[column] as string) - : '', - }; - }; - - // EFFECTS - useEffect(() => { - if (isDataLoading) { - toast.dismiss('grading-exceeds'); - toast.dismiss('grading-incomplete'); - return; - } - - if (isGradingExceedsAvailable && currentGradingTotal > 0) { - toast.error( - `Total grading (${currentGradingTotal}) melebihi telur yang tersedia (${totalKonsumsiBaikEggs})!`, - { - id: 'grading-exceeds', - duration: 3000, - } - ); - toast.dismiss('grading-incomplete'); - } else if (isGradingIncomplete && hasUserStartedGrading) { - toast.error( - `Total grading (${currentGradingTotal}) tidak sama dengan total telur konsumsi baik yang tersedia (${totalKonsumsiBaikEggs})! Semua telur harus digrading.`, - { - id: 'grading-incomplete', - duration: 3000, - } - ); - toast.dismiss('grading-exceeds'); - } else { - toast.dismiss('grading-exceeds'); - toast.dismiss('grading-incomplete'); - } - }, [ - isDataLoading, - isGradingExceedsAvailable, - isGradingIncomplete, - hasUserStartedGrading, - currentGradingTotal, - totalKonsumsiBaikEggs, - ]); - - useEffect(() => { - if ( - konsumsiBaikEggId && - formik.values.eggs_grading && - formik.values.eggs_grading.length === 0 - ) { - formik.setFieldValue('eggs_grading', [ - { recording_egg_id: konsumsiBaikEggId, grade: '', qty: '' }, - ]); - } - }, [konsumsiBaikEggId, formik.values.eggs_grading.length]); - - return ( - <> -
-
- -

- {type === 'add' && 'Tambah Grading'} - {type === 'edit' && 'Edit Grading'} - {type === 'detail' && 'Detail Grading'} -

-
- - - {/* Basic Info Card */} - -
- {/* Status Approval */} - {recording?.approval && ( -
- Status Approval -
- - {(() => { - const actionText = (() => { - switch (recording.approval.action) { - case 'APPROVED': - return 'Disetujui'; - case 'REJECTED': - return 'Ditolak'; - case 'CREATED': - return 'Dibuat'; - case 'UPDATED': - return 'Diperbarui'; - default: - return recording.approval.action; - } - })(); - - const stepName = recording.approval.step_name; - - if (stepName === actionText) { - return stepName; - } - - return `${stepName} - ${actionText}`; - })()} - -
-
- )} - {/* Recording Info */} -
- Lokasi -

- {projectFlockKandang?.project_flock?.location?.name || '-'} -

-
-
- Project Flock -

- {projectFlockKandang?.project_flock?.flock_name || '-'} -

-
-
- Kandang -

- {projectFlockKandang?.kandang?.name || '-'} -

-
-
- Tanggal Recording -

- {recording - ? formatDate(recording.record_datetime, 'DD MMMM YYYY') - : '-'} -

-
-
- Hari -

Hari ke-{recording?.day || '-'}

-
-
- Kategori -

- - {recording?.project_flock_category || '-'} - -

-
-
- Periode -

- - Periode {projectFlockKandang?.project_flock?.period || '-'} - -

-
-
- -
- {/* Additional Recording Info */} -
-
-
- -
- - Detail Recording - -
-
-
-

Area

-

- {projectFlockKandang?.project_flock?.area?.name || '-'} -

-
-
-

Status Kandang

-

- {projectFlockKandang?.kandang?.status || '-'} -

-
-
-
- - {/* Total Telur Konsumsi Baik Info */} -
-
-
-

- Total Telur Konsumsi Baik -

-
-

- {isDataLoading ? ( - - ) : ( - totalKonsumsiBaikEggs - )}{' '} - - telur - -

-
-
-
- -
-
- - {/* Progress Bar */} -
-
- Total yang digrading: - - {isDataLoading ? ( - - ) : ( - `${currentGradingTotal} / ${totalKonsumsiBaikEggs}` - )} - -
-
-
-
- {!isDataLoading && isGradingExceedsAvailable && ( -
- - Melebihi batas tersedia -
- )} - {!isDataLoading && - isGradingIncomplete && - hasUserStartedGrading && ( -
- - - Grading belum lengkap, semua telur harus digrading - -
- )} - {isDataLoading && ( -
- - Memuat data telur konsumsi baik... -
- )} -
-
-
- - - {/* Grading Table */} - -
- - - - {type !== 'detail' && ( - - )} - - - {type !== 'detail' && } - - - - {formik.values.eggs_grading?.map((grading, idx) => ( - - {type !== 'detail' && ( - - )} - - - {type !== 'detail' && ( - - )} - - ))} - -
- 0 - } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedGradingItems( - formik.values.eggs_grading?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedGradingItems([]); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - Grade - - * - - - Jumlah - - * - - Action
- - ) => { - if (e.target.checked) { - setSelectedGradingItems([ - ...selectedGradingItems, - idx, - ]); - } else { - setSelectedGradingItems( - selectedGradingItems.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - - - - -
- -
-
-
- {type !== 'detail' && ( -
- {selectedGradingItems.length > 0 && ( - - )} - -
- )} -
- - {/* Action buttons */} -
- {type !== 'add' && ( -
- {deleteRecordingClickHandler && ( - - )} - {type !== 'edit' && initialValues && ( - - )} -
- )} - {type !== 'detail' && ( -
- - -
- )} -
- {gradingFormErrorMessage && ( -
- - {gradingFormErrorMessage} -
- )} - -
- - {/* ===== MODALS ===== */} - {type !== 'add' && ( - <> - - - )} - - ); -}; - -export default GradingForm; diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 4266f6b7..ea06615a 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -9,8 +9,6 @@ import { CreateRecordingPayload, Recording, UpdateRecordingPayload, - CreateGradingPayload, - UpdateGradingPayload, NextDayRecording, } from '@/types/api/production/recording'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; @@ -64,28 +62,6 @@ export class RecordingService extends BaseApiService< }); } - async createGrading( - payload: CreateGradingPayload - ): Promise | undefined> { - return await this.customRequest>('gradings', { - method: 'POST', - payload, - }); - } - - async updateGrading( - gradingId: number, - payload: UpdateGradingPayload - ): Promise | undefined> { - return await this.customRequest>( - `gradings/${gradingId}`, - { - method: 'PUT', - payload, - } - ); - } - async deleteGrading( gradingId: number ): Promise | undefined> { From c3835d51286f370142327459931f324e0595af94 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 23:35:12 +0700 Subject: [PATCH 073/105] refactor(FE-319): Renumber RECORDINGS approval workflow steps --- src/config/constant.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index d4e08942..2786e951 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -262,11 +262,11 @@ export const APPROVAL_WORKFLOWS = [ key: 'RECORDINGS', steps: [ { - step_number: 2, + step_number: 1, step_name: 'Pengajuan', }, { - step_number: 3, + step_number: 2, step_name: 'Disetujui', }, ], From 012fe800bcdf141f0c18e71a9c8f60bf7aa469f5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 23:35:55 +0700 Subject: [PATCH 074/105] refactor(FE-318,319): Remove laying grading checks and simplify approval --- .../production/recording/RecordingTable.tsx | 133 ++++-------------- 1 file changed, 27 insertions(+), 106 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6cf254e7..27b2d5c6 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -35,28 +35,22 @@ const RowOptionsMenu = ({ deleteClickHandler, approveClickHandler, rejectClickHandler, - isGradingCompleted, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; approveClickHandler: () => void; rejectClickHandler: () => void; - isGradingCompleted: (recording: Recording) => boolean; }) => { - const isLayingCategory = - props.row.original.project_flock_category === 'LAYING'; - const isRecordingApproved = (recording: Recording) => { return ( recording.approval?.action === 'APPROVED' && - recording.approval?.step_name === 'Disetujui' && - recording.approval?.step_number === 3 + recording.approval?.step_number === 2 && + recording.approval?.step_name === 'Disetujui' ); }; const isApproved = isRecordingApproved(props.row.original); - const isGradingDone = isGradingCompleted(props.row.original); return ( @@ -78,7 +72,7 @@ const RowOptionsMenu = ({ Edit - {!isApproved && !(isLayingCategory && !isGradingDone) && ( + {!isApproved && ( - {type === 'detail' && - !isRecordingApproved(initialValues) && - (!isLayingCategory || hasGradingData(initialValues)) && ( -
- + {type === 'detail' && !isRecordingApproved(initialValues) && ( +
+ - -
- )} + +
+ )}

@@ -1928,7 +1820,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {formik.values.body_weights?.map((bw, idx) => (

+ { {formik.values.stocks?.map((stock, idx) => (
+ { {formik.values.depletions?.map((depletion, idx) => (
+ { (egg, idx) => (
+ { {/* Right side actions */}
- {type === 'detail' && isLayingCategory && ( - - - - )} - {type === 'edit' && (
-
- {flexRender( - header.column.columnDef.header, - header.getContext() + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( +
1, + }, + tableClassNames.headerColumnClassName )} + > +
1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
- - -
- )} -
-
+ {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} +
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index f0810f15..e509eb7d 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -6,7 +6,7 @@ import Table from '@/components/Table'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; +import { BaseClosingSales, BaseSales } from '@/types/api/closing'; import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; @@ -16,10 +16,6 @@ interface SalesReportTableProps { initialValues?: BaseClosingSales; } -interface FooterSalesRow extends BaseSales { - _isFooter: true; -} - const SalesReportTable = ({ type = 'detail', initialValues, @@ -72,29 +68,6 @@ const SalesReportTable = ({ }; }, [salesData]); - const footerData = useMemo((): FooterSalesRow[] => { - if (salesData.length === 0) return []; - - const footerRow: FooterSalesRow = { - id: -999, - realization_date: 'Total Penjualan', - age: 0, - do_number: '', - product: {} as Product, - customer: {} as Customer, - qty: totals.totalQuantity, - weight: totals.totalWeight, - avg_weight: totals.avgWeight, - price: totals.avgPricePartner, - total_price: totals.totalPartner, - kandang: {} as Kandang, - payment_status: '', - _isFooter: true, - }; - - return [footerRow]; - }, [salesData, totals]); - const salesColumns: ColumnDef[] = useMemo( () => [ { @@ -102,43 +75,30 @@ const SalesReportTable = ({ accessorKey: 'realization_date', header: 'Tanggal Realisasi', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
- {props.row.original.realization_date} -
- ); - } const date = props.row.original.realization_date; return date ? formatDate(date, 'DD MMM YYYY') : '-'; }, + footer: () => ( +
Total Penjualan
+ ), }, { id: 'age', accessorKey: 'age', header: 'Umur', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'do_number', accessorKey: 'do_number', header: 'No. DO', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, + cell: (props) => props.getValue() || '-', }, { id: 'product', accessorKey: 'product', header: 'Produk', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const product = props.getValue() as Product; return product?.name || '-'; }, @@ -148,47 +108,43 @@ const SalesReportTable = ({ accessorKey: 'customer', header: 'Customer', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const customer = props.getValue() as Customer; return customer?.name || '-'; }, }, { - id: 'qty', - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, - }, - { - id: 'weight', - accessorKey: 'weight', - header: 'Kg', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); - }, + id: 'jumlah', + header: 'Jumlah', + columns: [ + { + id: 'qty', + accessorKey: 'qty', + header: 'Kuantitas', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalQuantity)} +
+ ), + }, + { + id: 'weight', + accessorKey: 'weight', + header: 'Kg', + cell: (props) => { + const value = props.getValue() as number; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(totals.totalWeight)} +
+ ), + }, + ], }, { id: 'avg_weight', @@ -196,17 +152,13 @@ const SalesReportTable = ({ header: 'AVG (Kg)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatNumber(value)} -
- ); + return
{formatNumber(value)}
; }, + footer: () => ( +
+ {formatNumber(totals.avgWeight)} +
+ ), }, { id: 'price_partner', @@ -214,19 +166,13 @@ const SalesReportTable = ({ header: 'Harga Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.avgPricePartner)} +
+ ), }, { id: 'total_mitra', @@ -234,19 +180,13 @@ const SalesReportTable = ({ header: 'Total Mitra (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(totals.totalPartner)} +
+ ), }, { id: 'price_act', @@ -254,18 +194,7 @@ const SalesReportTable = ({ header: 'Harga Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -274,18 +203,7 @@ const SalesReportTable = ({ header: 'Total Act (Rp)', cell: (props) => { const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
- {formatCurrency(value)} -
- ); + return
{formatCurrency(value)}
; }, }, { @@ -293,8 +211,6 @@ const SalesReportTable = ({ accessorKey: 'kandang', header: 'Kandang', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; const kandang = props.getValue() as Kandang; return kandang?.name || '-'; }, @@ -304,9 +220,6 @@ const SalesReportTable = ({ accessorKey: 'payment_status', header: 'Status Pembayaran', cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const status = props.getValue() as string; const getStatusColor = (status: string) => { if (!status) return 'neutral'; @@ -345,16 +258,14 @@ const SalesReportTable = ({ 0} className={{ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', tableFooterClassName: diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 3f7ba816..95b2f57f 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,9 +1,34 @@ import { Area } from '@/types/api/master-data/area'; import { Fcr } from '@/types/api/master-data/fcr'; import { Flock } from '@/types/api/master-data/flock'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Location } from '@/types/api/master-data/location'; -import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseSales = { + id: number; + realization_date: string; + age: number; + do_number: string; + product: Product; + customer: Customer; + qty: number; + weight: number; + avg_weight: number; + price: number; + total_price: number; + kandang: Kandang; + payment_status: string; +}; + +export type BaseClosingSales = { + project_type: string; + flock_id: number; + period: number; + sales: BaseSales[]; +}; export type BaseClosing = { id: number; @@ -53,3 +78,4 @@ export type ClosingIncomingSapronak = { }; export type ClosingOutgoingSapronak = ClosingIncomingSapronak; +export type ClosingSales = BaseMetadata & BaseClosingSales; From 7e999b2e347cea4382eb360270f6b456b7ee37b4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:53:47 +0700 Subject: [PATCH 079/105] feat(FE): Show sales report on closing detail page --- src/app/closing/detail/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 6225b8dd..487533be 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import ClosingDetail from '@/components/pages/closing/ClosingDetail'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -19,6 +20,11 @@ const ClosingDetailPage = () => { (id: number) => ClosingApi.getGeneralInfo(id) ); + const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR( + closingId, + (id: number) => ClosingApi.getPenjualan(id) + ); + if (!closingId) { router.back(); @@ -43,6 +49,9 @@ const ClosingDetailPage = () => { {!isLoadingClosing && isResponseSuccess(closing) && ( )} + {!isLoadingSalesReport && isResponseSuccess(salesReport) && ( + + )} ); }; From eed142a85ffc38e29e06e49b75c13d51842dff2d Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:25:07 +0700 Subject: [PATCH 080/105] hotfix(FE): fixing dropdown logout and floating button max size --- src/app/production/project-flock/layout.tsx | 1 + src/components/FloatingActionsButton.tsx | 2 +- src/components/Navbar.tsx | 25 +-- src/components/dropdown/Dropdown.tsx | 116 ++++++++++++ src/components/dropdown/README.md | 83 ++++++++ src/components/helper/RequireAuth.tsx | 199 ++++++++++++++++---- src/services/api/closing.ts | 2 +- 7 files changed, 381 insertions(+), 47 deletions(-) create mode 100644 src/components/dropdown/Dropdown.tsx create mode 100644 src/components/dropdown/README.md diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index 698064cf..b74ef612 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -52,6 +52,7 @@ export default function ProjectFlockLayout({ closeOnBackdropClick={isDetail ? true : false} onBackdropClick={handleBackdropClick} variant='right' + zIndex='99999' sidebarContent={isOpen &&
{children}
} /> diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index c0033d72..c9ca3454 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -54,7 +54,7 @@ const FloatingActionsButton = ({
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 973bf031..bee92a57 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,6 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
-
-
-
- + +
+ +
-
- - + } + contentClassName='w-52 mt-3' + > + -
+
); diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 00000000..4489231d --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { ReactNode, useRef, useEffect, useState } from 'react'; +import { cn } from '@/lib/helper'; + +interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + position?: + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end' + | 'right-start' + | 'right-end'; + align?: 'start' | 'center' | 'end'; + hover?: boolean; + className?: string; + contentClassName?: string; +} + +const Dropdown = ({ + trigger, + children, + position = 'bottom', + align = 'start', + hover = false, + className, + contentClassName, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Build position classes + const getPositionClasses = () => { + const classes: string[] = []; + + // Handle combined positions like 'top-start' + if (position.includes('-')) { + const [pos, al] = position.split('-'); + classes.push(`dropdown-${pos}`); + classes.push(`dropdown-${al}`); + } else { + classes.push(`dropdown-${position}`); + if (align !== 'start') { + classes.push(`dropdown-${align}`); + } + } + + return classes.join(' '); + }; + + const handleToggle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + // alert('clicked'); + setIsOpen(!isOpen); + }; + + return ( +
+ {/* Trigger Button */} +
+ {trigger} +
+ + {/* Dropdown Content - Only render when open */} + {isOpen && ( +
setIsOpen(false)} // Close on item click + > + {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md new file mode 100644 index 00000000..e682682a --- /dev/null +++ b/src/components/dropdown/README.md @@ -0,0 +1,83 @@ +# Dropdown Component + +Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka. + +## Features + +- ✅ **Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka +- ✅ **Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown +- ✅ **Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end) +- ✅ **Hover Support**: Optional hover mode untuk membuka dropdown +- ✅ **Customizable**: Mendukung custom className untuk container dan content + +## Usage + +### Basic Example + +```tsx +import Dropdown from '@/components/dropdown/Dropdown'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; + +Click Me + } +> + + console.log('Item 1')} /> + console.log('Item 2')} /> + + +``` + +### With Position + +```tsx +Dropdown} + contentClassName="w-52 mt-3" +> + {/* Your content */} + +``` + +### Hover Mode + +```tsx +Hover Me} +> + {/* Your content */} + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown | +| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan | +| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger | +| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) | +| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown | +| `className` | `string` | - | Custom className untuk container dropdown | +| `contentClassName` | `string` | - | Custom className untuk content dropdown | + +## Position Examples + +- `bottom` - Dropdown muncul di bawah, align ke start +- `bottom-end` - Dropdown muncul di bawah, align ke end +- `bottom-center` - Dropdown muncul di bawah, align ke center +- `top-start` - Dropdown muncul di atas, align ke start +- `left-end` - Dropdown muncul di kiri, align ke end +- Dan seterusnya... + +## Key Benefits + +1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources +2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh +3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada +4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation 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/services/api/closing.ts b/src/services/api/closing.ts index 041108d0..dc0d804a 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -51,4 +51,4 @@ export class ClosingApiService extends BaseApiService { } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); From 429f5ffb629e36ceb44c02f5f01f092a2f400a1d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 13:30:40 +0700 Subject: [PATCH 081/105] feat(FE-311): Add rejection modals and accept handler --- .../purchase/order/PurchaseOrderDetail.tsx | 91 +++++++++++++++++++ src/types/api/purchase/purchase.d.ts | 2 +- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index cf7805fb..eb01aec7 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -27,6 +27,7 @@ import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrde import Card from '@/components/Card'; import { + CreateAcceptApprovalRequestPayload, CreateManagerApprovalRequestPayload, CreateStaffApprovalRequestPayload, Purchase, @@ -88,6 +89,8 @@ const PurchaseOrderDetail = ({ const staffApprovalModal = useModal(); const staffRejectionModal = useModal(); const acceptApprovalModal = useModal(); + const acceptRejectionModal = useModal(); + const managerRejectionModal = useModal(); const editModal = useModal(); const penerimaanBarangModal = useModal(); const deleteModal = useModal(); @@ -216,6 +219,12 @@ const PurchaseOrderDetail = ({ case 1: staffRejectionModal.openModal(); break; + case 2: + managerRejectionModal.openModal(); + break; + case 3: + acceptRejectionModal.openModal(); + break; default: break; } @@ -296,6 +305,33 @@ const PurchaseOrderDetail = ({ [initialValues?.id, searchParams, refetchData] ); + const createAcceptApprovalHandler = useCallback( + async (payload: CreateAcceptApprovalRequestPayload) => { + const purchaseRequestId = searchParams.get('purchaseId') + ? parseInt(searchParams.get('purchaseId')!) + : initialValues?.id || 1; + + if (!purchaseRequestId) { + toast.error('Purchase Request ID is required'); + return; + } + + const res = await PurchaseApi.acceptApproval.create( + purchaseRequestId, + payload + ); + + if (isResponseError(res)) { + toast.error(res.message); + return; + } + toast.success(res?.message as string); + refreshApprovals(); + refetchData?.(); + }, + [initialValues?.id, searchParams, refreshApprovals, refetchData] + ); + // ===== MODAL HANDLERS ===== const handleStaffApprovalModalClose = useCallback(() => { refreshApprovals(); @@ -1026,6 +1062,61 @@ const PurchaseOrderDetail = ({ }} /> + {/* Accept Rejection Modal */} + { + const payload: CreateAcceptApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + items: [], + }; + + await createAcceptApprovalHandler(payload); + await refetchData?.(); + acceptRejectionModal.closeModal(); + }, + }} + secondaryButton={{ + text: 'Batal', + }} + /> + + {/* Manager Rejection Modal */} + { + const payload: CreateManagerApprovalRequestPayload = { + action: 'REJECTED', + notes: notes || null, + }; + + await createManagerApprovalHandler(payload); + await refetchData?.(); + managerRejectionModal.closeModal(); + }, + }} + secondaryButton={{ + text: 'Batal', + }} + /> + {/* Delete Confirmation Modal */} Date: Wed, 10 Dec 2025 13:32:29 +0700 Subject: [PATCH 082/105] fix(FE): Remove closing detail page and layout --- src/app/_closing/detail/layout.tsx | 11 ------ src/app/_closing/detail/page.tsx | 55 ------------------------------ 2 files changed, 66 deletions(-) delete mode 100644 src/app/_closing/detail/layout.tsx delete mode 100644 src/app/_closing/detail/page.tsx diff --git a/src/app/_closing/detail/layout.tsx b/src/app/_closing/detail/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/_closing/detail/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/_closing/detail/page.tsx b/src/app/_closing/detail/page.tsx deleted file mode 100644 index 038e5072..00000000 --- a/src/app/_closing/detail/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; - -const ClosingDetailPage = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const closingId = searchParams.get('closingId'); - - const { data: closing, isLoading: isLoadingClosing } = useSWR( - closingId, - (id: string) => { - const numericId = parseInt(id, 10); - if (isNaN(numericId) || numericId <= 0) { - throw new Error('Invalid closing ID'); - } - return ClosingApi.getPenjualan(numericId); - } - ); - - if (!closingId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingClosing && (!closing || isResponseError(closing))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingClosing && ( -
- -
- )} - {!isLoadingClosing && isResponseSuccess(closing) && ( - - )} -
- ); -}; - -export default ClosingDetailPage; From f48cfca65058ab626ace3421acf5f575df90a99b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:35:42 +0700 Subject: [PATCH 083/105] fix(FE): revert require auth component --- src/components/dropdown/README.md | 83 ----------- src/components/helper/RequireAuth.tsx | 199 +++++--------------------- 2 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 src/components/dropdown/README.md diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md deleted file mode 100644 index e682682a..00000000 --- a/src/components/dropdown/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Dropdown Component - -Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka. - -## Features - -- ✅ **Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka -- ✅ **Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown -- ✅ **Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end) -- ✅ **Hover Support**: Optional hover mode untuk membuka dropdown -- ✅ **Customizable**: Mendukung custom className untuk container dan content - -## Usage - -### Basic Example - -```tsx -import Dropdown from '@/components/dropdown/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; - -Click Me - } -> - - console.log('Item 1')} /> - console.log('Item 2')} /> - - -``` - -### With Position - -```tsx -Dropdown} - contentClassName="w-52 mt-3" -> - {/* Your content */} - -``` - -### Hover Mode - -```tsx -Hover Me} -> - {/* Your content */} - -``` - -## Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown | -| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan | -| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger | -| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) | -| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown | -| `className` | `string` | - | Custom className untuk container dropdown | -| `contentClassName` | `string` | - | Custom className untuk content dropdown | - -## Position Examples - -- `bottom` - Dropdown muncul di bawah, align ke start -- `bottom-end` - Dropdown muncul di bawah, align ke end -- `bottom-center` - Dropdown muncul di bawah, align ke center -- `top-start` - Dropdown muncul di atas, align ke start -- `left-end` - Dropdown muncul di kiri, align ke end -- Dan seterusnya... - -## Key Benefits - -1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources -2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh -3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada -4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -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', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } 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}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth; From 7be32326a9cc21c876db3ee85adfe40bf2a60e6c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 13:56:13 +0700 Subject: [PATCH 084/105] feat(FE-311): Disable approval actions when rejected --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 5 ++++- .../purchase/form/order/PurchaseOrderStaffApprovalForm.tsx | 4 +++- src/components/pages/purchase/order/PurchaseOrderDetail.tsx | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index ab2b373a..15106c5e 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -52,6 +52,8 @@ const PurchaseOrderAcceptApprovalForm = ({ const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -672,7 +674,8 @@ const PurchaseOrderAcceptApprovalForm = ({ disabled={ !formik.isValid || formik.isSubmitting || - hasQuantityExceededErrors + hasQuantityExceededErrors || + isRejected } > Submit diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index bc718eb6..94998a37 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -79,6 +79,8 @@ const PurchaseOrderStaffApprovalForm = ({ } }, [rawDataApprovals, propType, initialValues?.latest_approval?.step_number]); + const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + const router = useRouter(); const searchParams = useSearchParams(); const deleteModal = useModal(); @@ -1142,7 +1144,7 @@ const PurchaseOrderStaffApprovalForm = ({ color='primary' className='px-4' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={!formik.isValid || formik.isSubmitting || isRejected} > Submit diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index eb01aec7..859c6671 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -180,7 +180,10 @@ const PurchaseOrderDetail = ({ }); const showApprovalButton = - approvalStep !== null && approvalStep >= 1 && approvalStep <= 3; + approvalStep !== null && + approvalStep >= 1 && + approvalStep <= 3 && + initialValues?.latest_approval?.action !== 'REJECTED'; const canDeleteItems = useMemo(() => { if (!initialValues?.latest_approval) return false; From d7199fad53d64b9a0bf7b794ae5272153f3e9625 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 15:05:52 +0700 Subject: [PATCH 085/105] hotfix(FE): Pass sales data to ClosingDetail and fix sales API --- src/app/closing/detail/page.tsx | 24 +++++++++---------- .../pages/closing/ClosingDetail.tsx | 15 +++++++++--- .../pages/closing/sale/SalesReportTable.tsx | 2 +- src/services/api/closing.ts | 9 +++---- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 487533be..1b4ebc45 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -4,7 +4,6 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import ClosingDetail from '@/components/pages/closing/ClosingDetail'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -20,9 +19,9 @@ const ClosingDetailPage = () => { (id: number) => ClosingApi.getGeneralInfo(id) ); - const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR( - closingId, - (id: number) => ClosingApi.getPenjualan(id) + const { data: salesData, isLoading: isLoadingSales } = useSWR( + closingId ? `sales-${closingId}` : null, + () => ClosingApi.getPenjualan(Number(closingId)) ); if (!closingId) { @@ -40,17 +39,18 @@ const ClosingDetailPage = () => { return; } + const isLoading = isLoadingClosing || isLoadingSales; + return (
- {isLoadingClosing && ( - - )} + {isLoading && } - {!isLoadingClosing && isResponseSuccess(closing) && ( - - )} - {!isLoadingSalesReport && isResponseSuccess(salesReport) && ( - + {!isLoading && isResponseSuccess(closing) && ( + )}
); diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 147b3fbd..11e28e32 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -7,15 +7,24 @@ import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; -import { ClosingGeneralInformation } from '@/types/api/closing'; +import { + ClosingGeneralInformation, + BaseClosingSales, +} from '@/types/api/closing'; import ClosingSapronakTabContent from './ClosingSapronakTabContent'; +import SalesReportTable from './sale/SalesReportTable'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; + salesData?: BaseClosingSales; } -const ClosingDetail: React.FC = ({ id, initialValue }) => { +const ClosingDetail: React.FC = ({ + id, + initialValue, + salesData, +}) => { const [activeTab, setActiveTab] = useState('sapronak'); const closingDetailTabs = useMemo(() => { @@ -33,7 +42,7 @@ const ClosingDetail: React.FC = ({ id, initialValue }) => { { id: 'penjualan', label: 'Penjualan', - content: 'Penjualan', + content: , }, { id: 'overhead', diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index e509eb7d..89cb6615 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -263,7 +263,7 @@ const SalesReportTable = ({ tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', bodyRowClassName: 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', bodyColumnClassName: diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index fe2c2d50..6ce32995 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -20,10 +20,11 @@ export class ClosingApiService extends BaseApiService { id: number ): Promise | undefined> { try { - const getPenjualanPath = `${id}/penjualan`; - return await this.customRequest>( - getPenjualanPath - ); + const getPenjualanPath = `${this.basePath}/${id}/penjualan`; + const getPenjualanRes = + await httpClient>(getPenjualanPath); + + return getPenjualanRes; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; From 0cc9d0e94e76902f606e19df3e110f034d290f9d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:18:37 +0700 Subject: [PATCH 086/105] hotfix: Centralize SSO redirection logic into a new helper with loop protection, integrate it into the HTTP client and `RequireAuth` component, and add an authentication failure UI. --- src/components/helper/RequireAuth.tsx | 48 ++++++++++++++++----------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..53853b96 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -1,54 +1,46 @@ 'use client'; import { ReactNode, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import useSWRImmutable from 'swr/immutable'; +import useSWR from 'swr'; 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 { redirectToSSO } from '@/lib/auth-helper'; interface RequireAuthProps { children?: ReactNode; } const RequireAuth = ({ children }: RequireAuthProps) => { - const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); const { data: userResponse, isLoading: isLoadingUserResponse, error: userErrorResponse, - } = useSWRImmutable< + } = useSWR< GetMeResponse & { ok?: boolean }, AxiosError, SWRHttpKey >('/sso/userinfo', httpClientFetcher, { shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, }); - useEffect(() => { - setIsLoadingUser(isLoadingUserResponse); - }, [isLoadingUserResponse, setIsLoadingUser]); - 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}` - ); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setUser]); + + // Explicitly handle 401 redirect from the component level + useEffect(() => { + if (userErrorResponse?.response?.status === 401) { + redirectToSSO(); + } + }, [userErrorResponse]); if (isLoadingUserResponse && !userResponse && !userErrorResponse) { return ( @@ -58,6 +50,24 @@ const RequireAuth = ({ children }: RequireAuthProps) => { ); } + if (userErrorResponse) { + return ( +
+

Authentication Failed

+

+ Please try refreshing the page or contact support if the problem + persists. +

+ +
+ ); + } + return <>{isResponseSuccess(userResponse) && children}; }; From 46d70e36dd96c40edbd67e3fdf6edfed88937550 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:21:10 +0700 Subject: [PATCH 087/105] feat: create auth-helper file and redirectToSSO helper function --- src/lib/auth-helper.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/lib/auth-helper.ts diff --git a/src/lib/auth-helper.ts b/src/lib/auth-helper.ts new file mode 100644 index 00000000..97d31a9f --- /dev/null +++ b/src/lib/auth-helper.ts @@ -0,0 +1,25 @@ +/** + * Redirects the user to the SSO login page with loop protection. + * + * This function checks a session storage timestamp to ensure that redirects + * do not happen too frequently (blocking infinite redirect loops). + */ +export const redirectToSSO = () => { + if (typeof window === 'undefined') return; + + const lastRedirect = sessionStorage.getItem('auth_redirect_timestamp'); + const now = Date.now(); + + // Loop protection: allow redirect only if last one was > 2 seconds ago + // or if no redirect has happened yet. + if (!lastRedirect || now - parseInt(lastRedirect, 10) > 2000) { + sessionStorage.setItem('auth_redirect_timestamp', now.toString()); + // const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; + + const ltiSsoStart = `${process.env.NEXT_PUBLIC_API_BASE_URL as string}/sso/start?client_id=${process.env.NEXT_PUBLIC_CLIENT_ID as string}&redirect_url=${window.location.href}`; + const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${ltiSsoStart}`; + window.location.href = ssoLoginUrl; + } else { + console.error('Redirect loop detected. Aborting redirect.'); + } +}; From 757e0435ac7fc711caa6b322ecc4dc7dc56e6b3b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 15:21:46 +0700 Subject: [PATCH 088/105] hotfix: use redirectToSSO function --- src/services/http/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/http/client.ts b/src/services/http/client.ts index f9389a16..68b5282a 100644 --- a/src/services/http/client.ts +++ b/src/services/http/client.ts @@ -2,6 +2,8 @@ import axios from 'axios'; import type { AxiosError, AxiosRequestConfig } from 'axios'; import { RequestOptions } from '@/services/http/base'; +import { redirectToSSO } from '@/lib/auth-helper'; + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 }); @@ -9,8 +11,7 @@ axiosClient.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { - const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; - window.location.href = ssoLoginUrl; + redirectToSSO(); } return Promise.reject(error); From eea1fcb51393e5bf1c8f60efe9fa40a8ff7bf495 Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 10 Dec 2025 08:43:08 +0000 Subject: [PATCH 089/105] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c37bfd35..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -127,6 +127,7 @@ build:dev: NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id' NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id' NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api' + NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia' deploy:dev: <<: *deploy_template From aed58ef10c23a4ffcef48a79da8701c88bb65f48 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 16:23:51 +0700 Subject: [PATCH 090/105] hotfix: Implement client-side dashboard redirect with loading spinner, improve authentication error handling by clearing user state on 401, and extend SSO redirect loop protection. --- src/app/page.tsx | 13 ++++++++++--- src/components/helper/RequireAuth.tsx | 10 ++++++++-- src/lib/auth-helper.ts | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index db9638df..2f22f5aa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,18 @@ -import { redirect } from 'next/navigation'; +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; export default function Home() { - redirect('/dashboard'); + const router = useRouter(); + + useEffect(() => { + router.replace('/dashboard'); + }, [router]); return (
-

LTI ERP

+
); } diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 53853b96..22b22b03 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -38,11 +38,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { // Explicitly handle 401 redirect from the component level useEffect(() => { if (userErrorResponse?.response?.status === 401) { + // Clear cache to prevent stale data from rendering children + // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate + setUser(undefined); redirectToSSO(); } - }, [userErrorResponse]); + }, [userErrorResponse, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + if ( + (isLoadingUserResponse && !userResponse && !userErrorResponse) || + (!userResponse && !userErrorResponse) + ) { return (
diff --git a/src/lib/auth-helper.ts b/src/lib/auth-helper.ts index 97d31a9f..bf05b70e 100644 --- a/src/lib/auth-helper.ts +++ b/src/lib/auth-helper.ts @@ -10,9 +10,9 @@ export const redirectToSSO = () => { const lastRedirect = sessionStorage.getItem('auth_redirect_timestamp'); const now = Date.now(); - // Loop protection: allow redirect only if last one was > 2 seconds ago + // Loop protection: allow redirect only if last one was > 5 seconds ago // or if no redirect has happened yet. - if (!lastRedirect || now - parseInt(lastRedirect, 10) > 2000) { + if (!lastRedirect || now - parseInt(lastRedirect, 10) > 5000) { sessionStorage.setItem('auth_redirect_timestamp', now.toString()); // const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; From 2c6ad71fd33e27aa14c6e3dfc47f1ad3008bc7ff Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 10 Dec 2025 09:42:13 +0000 Subject: [PATCH 091/105] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6028a8cb..f0187471 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -160,6 +160,6 @@ deploy:dev: # variables: # S3_BUCKET: "lti-erp.mbugroup.id" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: +# environment: # name: production From 83d76f7de479b33b7d162031858249129d8a8d5d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 16:57:20 +0700 Subject: [PATCH 092/105] fix: set isLoadingUser in useAuth hook --- src/components/helper/RequireAuth.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 22b22b03..8fc604ab 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -45,6 +45,10 @@ const RequireAuth = ({ children }: RequireAuthProps) => { } }, [userErrorResponse, setUser]); + useEffect(() => { + setIsLoadingUser(isLoadingUserResponse); + }, [isLoadingUserResponse]); + if ( (isLoadingUserResponse && !userResponse && !userErrorResponse) || (!userResponse && !userErrorResponse) From 017b081832d31f9e764e6ba5ed0756d96f7553ab Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 16:57:45 +0700 Subject: [PATCH 093/105] fix: redirect to SSO if user isnt exist and show loading state if still loading user --- src/app/page.tsx | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 2f22f5aa..cc933d52 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,17 +2,30 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { useAuth } from '@/services/hooks/useAuth'; +import { redirectToSSO } from '@/lib/auth-helper'; export default function Home() { + const { user, isLoadingUser } = useAuth(); + const router = useRouter(); useEffect(() => { router.replace('/dashboard'); - }, [router]); + }, [user, isLoadingUser]); - return ( -
- -
- ); + if (isLoadingUser) { + return ( +
+ +
+ ); + } + + if (!isLoadingUser && !user) { + redirectToSSO(); + return; + } + + return null; } From 30ab48e426205fb7dace57aafaaa4444cfa839d9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 17:07:44 +0700 Subject: [PATCH 094/105] fix: redirect to dashboard if pathname is in root path --- src/app/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index cc933d52..05140daf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { useAuth } from '@/services/hooks/useAuth'; import { redirectToSSO } from '@/lib/auth-helper'; @@ -9,10 +9,11 @@ export default function Home() { const { user, isLoadingUser } = useAuth(); const router = useRouter(); + const pathname = usePathname(); - useEffect(() => { + if (pathname === '/') { router.replace('/dashboard'); - }, [user, isLoadingUser]); + } if (isLoadingUser) { return ( From cfaac1482084d6d66360910090fb83b56ba6d7be Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 17:15:23 +0700 Subject: [PATCH 095/105] chore: return loading text if all condition unmet --- src/app/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 05140daf..0330b478 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useAuth } from '@/services/hooks/useAuth'; import { redirectToSSO } from '@/lib/auth-helper'; @@ -28,5 +27,5 @@ export default function Home() { return; } - return null; + return <>Loading...; } From 3826b8ea536eca03748c4cf09bc5e747d1dc6f56 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 17:31:06 +0700 Subject: [PATCH 096/105] feat: set trailingSlash to true --- next.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.ts b/next.config.ts index c781a8ac..b2d25eb6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true }, + trailingSlash: true, }; export default nextConfig; From 4f595c7cad66d574d75c5c2cc48b856643b6a880 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 17:31:21 +0700 Subject: [PATCH 097/105] chore: wrap router.replace in useEffect --- src/app/page.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0330b478..9fe5b724 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useAuth } from '@/services/hooks/useAuth'; import { redirectToSSO } from '@/lib/auth-helper'; @@ -10,9 +11,11 @@ export default function Home() { const router = useRouter(); const pathname = usePathname(); - if (pathname === '/') { - router.replace('/dashboard'); - } + useEffect(() => { + if (pathname === '/') { + router.replace('/dashboard'); + } + }, [pathname]); if (isLoadingUser) { return ( From 6340a5e519f369b716164428ed0112a5ada3a1fc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 18:09:10 +0700 Subject: [PATCH 098/105] fix: export dynamic --- src/app/closing/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx index acaa3ee8..6c5896c9 100644 --- a/src/app/closing/page.tsx +++ b/src/app/closing/page.tsx @@ -8,4 +8,6 @@ const Closing = () => { ); }; +export const dynamic = 'force-static'; + export default Closing; From 280fffe6a543ed65b2b4acfd7df0db9f20f5d812 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 18:09:21 +0700 Subject: [PATCH 099/105] fix: add use-client --- src/app/expense/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/expense/page.tsx b/src/app/expense/page.tsx index d6b00286..1e018879 100644 --- a/src/app/expense/page.tsx +++ b/src/app/expense/page.tsx @@ -1,3 +1,5 @@ +'use client'; + import ExpensesTable from '@/components/pages/expense/ExpensesTable'; const Expense = () => { From 720ff2128f3248c5243d7c8a523f9d53c23bf578 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 18:09:30 +0700 Subject: [PATCH 100/105] fix: add use-client and export dynamic --- src/app/marketing/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index 99a80b64..c0f4f53b 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -1,3 +1,5 @@ +'use client'; + import MarketingTable from '@/components/pages/marketing/MarketingTable'; const Marketing = () => { @@ -7,4 +9,7 @@ const Marketing = () => {
); }; + +export const dynamic = 'force-static'; + export default Marketing; From f939f4b0fbb6a0f98b28dea967ba8aad19157eda Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 18:10:08 +0700 Subject: [PATCH 101/105] fix: return children only if userResponse success and user is set --- src/components/helper/RequireAuth.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 8fc604ab..6d1f050b 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -15,7 +15,7 @@ interface RequireAuthProps { } const RequireAuth = ({ children }: RequireAuthProps) => { - const { setUser, setIsLoadingUser } = useAuth(); + const { user, setUser, setIsLoadingUser } = useAuth(); const { data: userResponse, @@ -78,7 +78,7 @@ const RequireAuth = ({ children }: RequireAuthProps) => { ); } - return <>{isResponseSuccess(userResponse) && children}; + return <>{isResponseSuccess(userResponse) && user && children}; }; export default RequireAuth; From 37f59f94703ab48fab29874be8269a6b2143097d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 18:50:58 +0700 Subject: [PATCH 102/105] fix: remove unnecessary code --- src/app/closing/page.tsx | 2 -- src/app/expense/page.tsx | 2 -- src/app/marketing/page.tsx | 4 ---- 3 files changed, 8 deletions(-) diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx index 6c5896c9..acaa3ee8 100644 --- a/src/app/closing/page.tsx +++ b/src/app/closing/page.tsx @@ -8,6 +8,4 @@ const Closing = () => { ); }; -export const dynamic = 'force-static'; - export default Closing; diff --git a/src/app/expense/page.tsx b/src/app/expense/page.tsx index 1e018879..d6b00286 100644 --- a/src/app/expense/page.tsx +++ b/src/app/expense/page.tsx @@ -1,5 +1,3 @@ -'use client'; - import ExpensesTable from '@/components/pages/expense/ExpensesTable'; const Expense = () => { diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index c0f4f53b..c30ee501 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -1,5 +1,3 @@ -'use client'; - import MarketingTable from '@/components/pages/marketing/MarketingTable'; const Marketing = () => { @@ -10,6 +8,4 @@ const Marketing = () => { ); }; -export const dynamic = 'force-static'; - export default Marketing; From 4356bd8803efe20617285e77029df0595803bdc2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 21:43:05 +0700 Subject: [PATCH 103/105] fix: remove redirectToSSO --- src/app/page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 9fe5b724..9cc0177d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,10 +25,5 @@ export default function Home() { ); } - if (!isLoadingUser && !user) { - redirectToSSO(); - return; - } - return <>Loading...; } From 9628ee88adf4aa8cc1765e7f0256b1d02a466cd1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 10 Dec 2025 21:47:58 +0700 Subject: [PATCH 104/105] chore: add condition for redirecting to SSO --- src/components/helper/RequireAuth.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 6d1f050b..65adf48c 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -37,13 +37,16 @@ const RequireAuth = ({ children }: RequireAuthProps) => { // Explicitly handle 401 redirect from the component level useEffect(() => { - if (userErrorResponse?.response?.status === 401) { + if ( + isResponseError(userResponse) && + userErrorResponse?.response?.status === 401 + ) { // Clear cache to prevent stale data from rendering children // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate setUser(undefined); redirectToSSO(); } - }, [userErrorResponse, setUser]); + }, [userErrorResponse, setUser, userResponse]); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); From 4ec455b3b723cbd548de0938d73a69fbb4977e61 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 23:54:59 +0700 Subject: [PATCH 105/105] feat(FE): Add credit_term to purchase forms and types --- .gitlab-ci.yml | 2 +- .../order/PurchaseOrderStaffApprovalForm.tsx | 8 +- .../request/PurchaseRequestForm.schema.ts | 6 ++ .../form/request/PurchaseRequestForm.tsx | 79 ++++++++++++++++++- .../purchase/order/PurchaseOrderDetail.tsx | 2 - .../purchase/order/PurchaseOrderInvoice.tsx | 3 + src/types/api/purchase/purchase.d.ts | 10 ++- 7 files changed, 99 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0187471..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -160,6 +160,6 @@ deploy:dev: # variables: # S3_BUCKET: "lti-erp.mbugroup.id" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: +# environment: # name: production diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 94998a37..1fcd7a94 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -314,7 +314,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { @@ -362,7 +364,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts index 05167715..67a694bc 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts @@ -7,6 +7,7 @@ type PurchaseRequestFormSchemaType = { label: string; } | null; supplier_id: number; + credit_term: number; area?: { value: number; label: string; @@ -81,6 +82,10 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema ({ warehouse_id: Number(item.warehouse_id) || 0, @@ -338,6 +342,27 @@ const PurchaseRequestForm = ({ }; // ===== UTILITY FUNCTIONS ===== + const updateCreditTermBasedOnSupplier = useCallback( + (supplierId: number) => { + if (supplierId > 0 && isResponseSuccess(supplierRawData)) { + const supplierData = supplierRawData.data.find( + (s: Supplier) => s.id === supplierId + ); + if (supplierData?.due_date) { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', supplierData.due_date.toString()); + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + } + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + } + }, + [supplierRawData] + ); + const resetPurchaseItems = useCallback(() => { if (formik.values.items) { formik.values.items.forEach((_, idx) => { @@ -352,6 +377,16 @@ const PurchaseRequestForm = ({ }, []); // ===== SIDE EFFECTS ===== + useEffect(() => { + if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) { + updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id)); + resetPurchaseItems(); + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + resetPurchaseItems(); + } + }, [formik.values.supplier_id]); // ===== FORM HANDLERS ===== const handleSupplierChange = useCallback( @@ -367,6 +402,23 @@ const PurchaseRequestForm = ({ [] ); + const handleCreditTermChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + + formik.setFieldTouched('credit_term', true); + formik.setFieldValue('credit_term', value); + }, + [] + ); + + const handleCreditTermBlur = useCallback( + (e: React.FocusEvent) => { + formik.handleBlur(e); + }, + [formik] + ); + const handleAreaChange = useCallback( (val: OptionType | OptionType[] | null) => { const area = val as OptionType | null; @@ -447,7 +499,7 @@ const PurchaseRequestForm = ({ body: 'flex flex-col gap-6', }} > -
+
+ + -
+
{ {purchaseData?.supplier?.alias || ''}) {purchaseData?.supplier?.category || '-'} + + Credit Term: {purchaseData?.credit_term || 0} hari + Due Date:{' '} {purchaseData?.due_date diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 2dcde2d9..e4de565b 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -51,6 +51,7 @@ export type BasePurchase = { po_document_path?: string | null; po_date: string; supplier: Supplier; + credit_term?: number; due_date: string; notes?: string | null; deleted_at?: string | null; @@ -66,8 +67,9 @@ export type Purchase = BaseMetadata & BasePurchase; export type CreatePurchaseRequestPayload = { supplier_id: number; + credit_term: number; notes?: string | null; - items: { + items?: { warehouse_id: number; product_id: number; qty: number; @@ -77,7 +79,7 @@ export type CreatePurchaseRequestPayload = { export type CreateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: { + items?: { purchase_item_id: number; qty: number; price: number; @@ -88,7 +90,7 @@ export type CreateStaffApprovalRequestPayload = { export type UpdateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: Array<{ + items?: Array<{ purchase_item_id?: number; product_id?: number; warehouse_id?: number; @@ -106,7 +108,7 @@ export type CreateManagerApprovalRequestPayload = { export type CreateAcceptApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: { + items?: { purchase_item_id: number; received_date: string; travel_number: string;