From 4b6144d0b49d7e3c2246e0c6f8828b00427cdd93 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 27 Nov 2025 13:36:12 +0700 Subject: [PATCH 001/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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/174] 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: Thu, 4 Dec 2025 22:36:22 +0700 Subject: [PATCH 032/174] chore: update next, daisyui, and eslint-config-next library --- package-lock.json | 104 +++++++++++++++++++++++----------------------- package.json | 6 +-- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..3d9be201 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", @@ -36,9 +36,9 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.1.12", + "daisyui": "^5.5.5", "eslint": "^9", - "eslint-config-next": "15.5.3", + "eslint-config-next": "^15.5.7", "husky": "^9.1.7", "prettier": "^3.6.2", "tailwindcss": "^4", @@ -1082,15 +1082,15 @@ } }, "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": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", - "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", + "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -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" ], @@ -3063,9 +3063,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz", - "integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==", + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz", + "integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==", "dev": true, "license": "MIT", "funding": { @@ -3571,13 +3571,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz", - "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz", + "integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.3", + "@next/eslint-plugin-next": "15.5.7", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -5654,12 +5654,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 +5672,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": { diff --git a/package.json b/package.json index 7396d49d..8f90d778 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", @@ -39,9 +39,9 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.1.12", + "daisyui": "^5.5.5", "eslint": "^9", - "eslint-config-next": "15.5.3", + "eslint-config-next": "^15.5.7", "husky": "^9.1.7", "prettier": "^3.6.2", "tailwindcss": "^4", From 611655e40847d7073f22edbe6878f651d948c16c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:42:57 +0700 Subject: [PATCH 033/174] chore: update gitlab-ci --- .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 79a89ea193448ea37536344d8b1ca305db499dca Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:44:17 +0700 Subject: [PATCH 034/174] chore: use SidebarMenu component --- src/components/MainDrawer.tsx | 154 ++-------------------------------- 1 file changed, 7 insertions(+), 147 deletions(-) diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 4a3b44b0..3a09c0b1 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -1,161 +1,21 @@ 'use client'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; import { Icon } from '@iconify/react'; import Drawer from '@/components/Drawer'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Navbar from '@/components/Navbar'; -import Collapse from '@/components/Collapse'; import Button from '@/components/Button'; +import SidebarMenu from '@/components/molecules/SidebarMenu'; import { useUiStore } from '@/stores/ui/ui.store'; import { MAIN_DRAWER_LINKS } from '@/config/constant'; -import { cn } from '@/lib/helper'; - -type CollapseMenuProps = { - title: string; - link: string; - icon: string; - submenu?: CollapseMenuProps[]; - depth?: number; -}; - -const isPathActive = (pathname: string, link?: string) => { - if (!link) return false; - - const splittedPathname = pathname.split('/'); - const splittedLink = link.split('/'); - - const isActiveLinkValid = splittedLink.every((linkChunk, idx) => { - return linkChunk === splittedPathname[idx]; - }); - - return pathname.startsWith(link) && isActiveLinkValid; -}; - -const CollapseMenu = ({ - title, - link, - icon, - submenu, - depth = 0, -}: CollapseMenuProps) => { - const pathname = usePathname(); - const isActive = isPathActive(pathname, link); - const [open, setOpen] = useState(isActive); - - const menuCollapseTitle = ( -
-
- - {title} -
- - -
- ); - - return ( - - -
- {submenu?.map((item, idx) => { - const hasSubmenu = item.submenu && item.submenu.length > 0; - - if (!hasSubmenu) { - return ( - - ); - } - - return ( - - ); - })} -
-
-
- ); -}; - -const MainDrawerMenu = () => { - const pathname = usePathname(); - - return ( - - {MAIN_DRAWER_LINKS.map((item, idx) => { - const hasSubmenu = item.submenu && item.submenu.length > 0; - - if (!hasSubmenu) { - return ( - - ); - } - - return ( - - ); - })} - - ); -}; +import { isPathActive } from '@/lib/helper'; const MainDrawerContent = () => { + const pathname = usePathname(); const { setMainDrawerOpen } = useUiStore(); const closeMainDrawerHandler = () => { @@ -191,7 +51,7 @@ const MainDrawerContent = () => {
- + ); }; @@ -216,9 +76,9 @@ const MainDrawer = ({ const hasSubmenu = menu?.submenu && menu?.submenu.length > 0; if (!title) { - title += menu?.title; + title += menu?.text; } else { - title += ' - ' + menu?.title; + title += ' - ' + menu?.text; } if (!hasSubmenu || !menu.submenu) return; From fd024fdb8f70582a037bab7e70ae8395ff3550c2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:44:43 +0700 Subject: [PATCH 035/174] chore: update Pagination component --- src/components/Pagination.tsx | 508 ++++++++++++++++++++-------------- 1 file changed, 299 insertions(+), 209 deletions(-) diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index e47e480d..8b80abf3 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -1,7 +1,9 @@ 'use client'; -import { ReactNode } from 'react'; +import { ChangeEventHandler, ReactNode } from 'react'; + import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; import { cn } from '@/lib/helper'; @@ -17,16 +19,18 @@ const PaginationButton = ({ disabled?: boolean; onClick?: () => void; }) => ( - + ); const EtcPaginationButton = ({ @@ -90,16 +94,20 @@ const Pagination = ({ currentPage = 1, totalItems = 0, itemsPerPage = 10, + rowOptions = [10, 20, 50, 100], onPageChange, onPrevPage = () => {}, onNextPage = () => {}, + onRowChange, }: { currentPage: number; totalItems: number; itemsPerPage: number; + rowOptions?: number[]; onPageChange: (pageNumber: number) => void; onPrevPage: () => void; onNextPage: () => void; + onRowChange?: (row: number) => void; }) => { const totalPages = Math.ceil(totalItems / itemsPerPage) === 0 @@ -107,30 +115,139 @@ const Pagination = ({ : Math.ceil(totalItems / itemsPerPage); const pageChangeHandler = (pageNumber: number) => onPageChange(pageNumber); + const firstPageClickHandler = () => onPageChange(1); + const lastPageClickHandler = () => onPageChange(totalPages); + + const rowChangeHandler: ChangeEventHandler = (e) => { + onRowChange?.(Number(e.target.value)); + }; + + const DisplayedRowCountSelect = () => ( +
+ Showing + + +
+ ); + + const GoToFirstPageButton = () => ( + + ); + + const PrevPageButton = () => ( + + ); + + const GoToLastPageButton = () => ( + + ); + + const NextPageButton = () => ( + + ); + + const PageInfo = () => ( + + Page {currentPage} of {totalPages} + + ); return ( -
-
- +
+
+
+ +
- {totalPages <= 7 && ( -
- {range(1, totalPages).map((pageNumber) => ( +
+
+ +
+ +
+ +
+ + {totalPages <= 7 && + range(1, totalPages).map((pageNumber) => ( pageChangeHandler(pageNumber)} /> ))} -
- )} - {totalPages > 7 && ( -
- pageChangeHandler(1)} - /> - - {totalPages >= 2 && - (currentPage <= 3 || currentPage >= totalPages - 2) && ( - pageChangeHandler(2)} - /> - )} - - {totalPages >= 2 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 3 && - (currentPage <= 4 || currentPage >= totalPages - 2) && - currentPage !== totalPages - 2 && ( - pageChangeHandler(3)} - /> - )} - - {totalPages >= 7 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - = totalPages - 1 - ? 4 - : 1 - } - endPage={ - currentPage <= 2 || currentPage >= totalPages - 1 - ? totalPages - 3 - : currentPage === totalPages - 2 - ? totalPages - 4 - : 2 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 3 && - currentPage > 4 && - currentPage < totalPages - 1 && ( - pageChangeHandler(currentPage - 1)} - /> - )} - - {totalPages >= 7 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 5 && - currentPage > 2 && - currentPage < totalPages - 2 && ( - pageChangeHandler(currentPage + 1)} - /> - )} - - {totalPages >= 5 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - pageChangeHandler(totalPages - 2)} - /> - )} - - {totalPages >= 6 && - currentPage > 2 && - currentPage < totalPages - 3 && ( - = 4 - ? currentPage + 2 - : 1 - } - endPage={ - currentPage <= 3 - ? totalPages - 2 - : currentPage >= 4 - ? totalPages - 1 - : 0 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 6 && - (currentPage <= 3 || currentPage >= totalPages - 3) && ( - pageChangeHandler(totalPages - 1)} - /> - )} - - {totalPages >= 7 && ( + {totalPages > 7 && ( + <> pageChangeHandler(totalPages)} + content={1} + disabled={currentPage === 1} + onClick={() => pageChangeHandler(1)} /> - )} -
- )} - + +
+ +
+ +
+ +
+
+ +
+ +
-
- +
+
+ + + + +
- +
+ + + +
); From 2bb2da74e6bf0711f3b0fbcc0483110348d1b90c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:45:13 +0700 Subject: [PATCH 036/174] chore: update CheckboxInput component --- src/components/input/CheckboxInput.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index fb0c95c7..32f14f94 100644 --- a/src/components/input/CheckboxInput.tsx +++ b/src/components/input/CheckboxInput.tsx @@ -2,8 +2,9 @@ import { HTMLProps, useEffect, useRef } from 'react'; import { cn } from '@/lib/helper'; +import { Size } from '@/types/theme'; -interface CheckboxInputProps extends HTMLProps { +interface CheckboxInputProps extends Omit, 'size'> { name: string; label?: string; indeterminate?: boolean; @@ -16,6 +17,7 @@ interface CheckboxInputProps extends HTMLProps { isError?: boolean; isValid?: boolean; errorMessage?: string; + size?: Size; } const CheckboxInput = ({ @@ -27,10 +29,19 @@ const CheckboxInput = ({ isValid, isError, errorMessage, + size = 'sm', ...rest }: CheckboxInputProps) => { const ref = useRef(null!); + const checkboxBaseClassName = cn('checkbox cursor-pointer rounded-md', { + 'checkbox-xs': size === 'xs', + 'checkbox-sm': size === 'sm', + 'checkbox-md': size === 'md', + 'checkbox-lg': size === 'lg', + 'checkbox-xl': size === 'xl', + }); + useEffect(() => { if (typeof indeterminate === 'boolean') { ref.current.indeterminate = !rest.checked && indeterminate; @@ -53,7 +64,7 @@ const CheckboxInput = ({ id={name} name={name} className={cn( - 'checkbox cursor-pointer', + checkboxBaseClassName, { 'border-error': isError, 'border-success': isValid, From a8d7fdc30d2ddbea786b41baeaff597f02fb7d30 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:45:20 +0700 Subject: [PATCH 037/174] chore: update Menu component --- src/components/menu/Menu.tsx | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index b3981065..ae74717d 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -1,16 +1,32 @@ import { ReactNode } from 'react'; import { cn } from '@/lib/helper'; +import { Size } from '@/types/theme'; interface MenuProps { children?: ReactNode; + size?: Size; + direction?: 'vertical' | 'horizontal'; className?: string; } -const Menu = ({ children, className }: MenuProps) => { - return ( -
    {children}
- ); +const Menu = ({ + children, + size = 'md', + direction = 'vertical', + className, +}: MenuProps) => { + const menuBaseClassName = cn('menu w-full', { + 'menu-xs': size === 'xs', + 'menu-sm': size === 'sm', + 'menu-md': size === 'md', + 'menu-lg': size === 'lg', + 'menu-xl': size === 'xl', + 'menu-vertical': direction === 'vertical', + 'menu-horizontal': direction === 'horizontal', + }); + + return
    {children}
; }; export default Menu; From cee3d4ba90b9a940f4fa261bd3972f5b09d0e8f1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:45:29 +0700 Subject: [PATCH 038/174] chore: create SidebarMenu component --- src/components/molecules/SidebarMenu.tsx | 92 ++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/components/molecules/SidebarMenu.tsx diff --git a/src/components/molecules/SidebarMenu.tsx b/src/components/molecules/SidebarMenu.tsx new file mode 100644 index 00000000..6a217dcc --- /dev/null +++ b/src/components/molecules/SidebarMenu.tsx @@ -0,0 +1,92 @@ +import Link from 'next/link'; +import Menu from '@/components/menu/Menu'; +import { Icon } from '@iconify/react'; +import { cn, isPathActive } from '@/lib/helper'; + +export interface SidebarMenuItem { + type?: 'item' | 'title'; + text: string; + link: string; + icon?: string; + submenu?: SidebarMenuItem[]; +} + +interface SidebarMenuItemProps { + item: SidebarMenuItem; + activeLink: string; +} + +interface SidebarMenuProps { + menu: SidebarMenuItem[]; + activeLink: string; +} + +const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => { + const isItemActive = isPathActive(activeLink, item.link); + + const menuItemWithoutSubmenu = ( +
  • + + {item.icon && } + + {item.text} + +
  • + ); + + if (!item.submenu || item.submenu.length === 0) { + return menuItemWithoutSubmenu; + } + + const menuItemWithSubmenu = ( +
  • +
    + + {item.icon && } + + {item.text} + + +
      + {item.submenu.map((submenuItem, submenuIdx) => ( + + ))} +
    +
    +
  • + ); + + return menuItemWithSubmenu; +}; + +const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => { + return ( + + {menu.map((menuItem, menuIdx) => ( + + ))} + + ); +}; + +export default SidebarMenu; From 48dd6d7218228a85aaa9654d7b3ce640bd58a090 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:45:48 +0700 Subject: [PATCH 039/174] chore: update MAIN_DRAWER_LINKS structure --- src/config/constant.ts | 107 +++++++++++++---------------------------- 1 file changed, 34 insertions(+), 73 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index dc36025b..926db692 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,155 +1,116 @@ -type MAIN_DRAWER_MENU = { - title: string; - link: string; - icon: string; - submenu?: MAIN_DRAWER_MENU[]; -}; +import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; -export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ +export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { - title: 'Dashboard', + text: 'Dashboard', link: '/dashboard', - icon: 'gg:chart', + icon: 'heroicons-outline:chart-bar-square', }, - { - title: 'Produksi', + text: 'Produksi', link: '/production', - icon: 'material-symbols:conveyor-belt-outline-rounded', + icon: 'heroicons-outline:wrench-screwdriver', submenu: [ { - title: 'List Flock', + text: 'Daftar Flock', link: '/production/project-flock', - icon: 'material-symbols:list-alt-add-outline-rounded', }, - // { // DI HILANGKAN PADA VERSI REFACTORING - // title: 'Chick In', - // link: '/production/chickin', - // icon: 'mdi:home-import-outline', - // }, { - title: 'Recording', + text: 'Recording', link: '/production/recording', - icon: 'mdi:clipboard-text', }, { - title: 'Transfer ke Laying', + text: 'Transfer to Laying', link: '/production/transfer-to-laying', - icon: 'streamline:transfer-van', }, ], }, - { - title: 'Pembelian', + text: 'Pembelian', link: '/purchase', - icon: 'gg:shopping-cart', + icon: 'heroicons-outline:shopping-cart', }, - { - title: 'Penjualan', + text: 'Penjualan', link: '/marketing', - icon: 'mdi:attach-money', + icon: 'heroicons-outline:currency-dollar', }, - { - title: 'Biaya Operasional', + text: 'Biaya Operasional', link: '/expense', - icon: 'uil:wallet', + icon: 'heroicons:wallet', }, - { - title: 'Persediaan', + text: 'Persediaan', link: '/inventory', - icon: 'mdi:warehouse', + icon: 'heroicons-outline:folder', submenu: [ - // { - // title: 'Product', - // link: '/inventory/product', - // icon: 'mdi:package-variant-closed', - // }, { - title: 'Penyesuaian Stok', + text: 'Penyesuaian Stok', link: '/inventory/adjustment', - icon: 'mdi:database-edit', }, { - title: 'Transfer Stok', + text: 'Transfer Stok', link: '/inventory/movement', - icon: 'mdi:swap-horizontal', }, ], }, - { - title: 'Master Data', + text: 'Master Data', link: '/master-data', - icon: 'majesticons:data-line', + icon: 'heroicons-outline:circle-stack', submenu: [ { - title: 'Product', + text: 'Produk', link: '/master-data/product', - icon: 'fluent-mdl2:product-variant', }, { - title: 'Product Category', + text: 'Kategori Produk', link: '/master-data/product-category', - icon: 'carbon:categories', }, { - title: 'Bank', + text: 'Bank', link: '/master-data/bank', - icon: 'mdi:bank-outline', }, { - title: 'Area', + text: 'Area', link: '/master-data/area', - icon: 'majesticons:map-marker-area-line', }, { - title: 'Location', + text: 'Lokasi', link: '/master-data/location', - icon: 'mingcute:location-line', }, { - title: 'Kandang', + text: 'Kandang', link: '/master-data/kandang', - icon: 'mdi:farm-home-outline', }, { - title: 'Warehouse', + text: 'Warehouse', link: '/master-data/warehouse', - icon: 'hugeicons:warehouse', }, { - title: 'Customer', + text: 'Customer', link: '/master-data/customer', - icon: 'ix:customer', }, { - title: 'UOM', + text: 'UOM', link: '/master-data/uom', - icon: 'lsicon:measure-outline', }, { - title: 'Non-Stock', + text: 'Non-Stock', link: '/master-data/nonstock', - icon: 'fluent:box-32-regular', }, { - title: 'FCR', + text: 'FCR', link: '/master-data/fcr', - icon: 'fluent:food-chicken-leg-16-regular', }, { - title: 'Supplier', + text: 'Supplier', link: '/master-data/supplier', - icon: 'material-symbols:add-business-outline-rounded', }, { - title: 'Flock', + text: 'Flock', link: '/master-data/flock', - icon: 'material-symbols:raven-outline-rounded', }, ], }, From ae4c17b39133a259141eacc623f71b678ea47750 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:45:57 +0700 Subject: [PATCH 040/174] chore: create isPathActive helper --- src/lib/helper.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 2c66e1cf..13fdce5d 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -119,3 +119,16 @@ export const convertRowSelectionObjToArr = ( return result; }; + +export const isPathActive = (pathname: string, link?: string) => { + if (!link) return false; + + const splittedPathname = pathname.split('/'); + const splittedLink = link.split('/'); + + const isActiveLinkValid = splittedLink.every((linkChunk, idx) => { + return linkChunk === splittedPathname[idx]; + }); + + return pathname.startsWith(link) && isActiveLinkValid; +}; From b37c3f87b01e88a198707569db948500fb622b89 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:46:18 +0700 Subject: [PATCH 041/174] chore: set color for menu foreground and background --- src/styles/daisyui.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css index fc87399f..8eca2c82 100644 --- a/src/styles/daisyui.css +++ b/src/styles/daisyui.css @@ -1,4 +1,9 @@ @layer utilities { + .menu { + --menu-active-fg: var(--color-primary); + --menu-active-bg: transparent; + } + .step.step-success::before { --step-bg: var(--color-success); --step-fg: var(--color-success-content); From be725d42c33cc2fc79cf0fd2742a209b03e4c30d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 22:46:26 +0700 Subject: [PATCH 042/174] chore: add Size type --- src/types/theme.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/theme.d.ts b/src/types/theme.d.ts index f83750e4..dcd9e13f 100644 --- a/src/types/theme.d.ts +++ b/src/types/theme.d.ts @@ -1,4 +1,4 @@ -type Color = +export type Color = | 'primary' | 'secondary' | 'accent' @@ -9,4 +9,4 @@ type Color = | 'error' | 'none'; -export { Color }; +export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; From bac3f30ce3815af79ca1e6d0b8f143b1d327f28c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 4 Dec 2025 23:09:08 +0700 Subject: [PATCH 043/174] chore: update Table component --- src/components/Table.tsx | 88 +++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b02dd3b5..970c5bc1 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -38,6 +38,7 @@ export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; + onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; @@ -52,6 +53,8 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + withCheckbox?: boolean; + rowOptions?: number[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -64,28 +67,32 @@ const emptyContentDefaultValue = (
    ); +const TABLE_DEFAULT_STYLING = { + containerClassName: 'w-full mb-20', + tableWrapperClassName: + 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', + tableClassName: 'font-inter w-full table-auto text-sm font-medium', + tableHeaderClassName: '', + headerRowClassName: '', + headerColumnClassName: 'px-4 py-3 text-base-content/50', + tableBodyClassName: '', + bodyRowClassName: 'border-t border-t-base-content/10', + bodyColumnClassName: 'px-4 py-3 text-base-content', + paginationClassName: '', +}; + const Table = ({ data = [], columns = [], pageSize = 10, + onPageSizeChange, totalItems, page, onPageChange, isLoading = false, fuzzySearchValue, onFuzzySearchValueChange, - className = { - containerClassName: '', - tableWrapperClassName: '', - tableClassName: '', - tableHeaderClassName: '', - headerRowClassName: '', - headerColumnClassName: '', - tableBodyClassName: '', - bodyRowClassName: '', - bodyColumnClassName: '', - paginationClassName: '', - }, + className = TABLE_DEFAULT_STYLING, emptyContent = emptyContentDefaultValue, sorting, setSorting, @@ -93,12 +100,19 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + withCheckbox = false, + rowOptions = [10, 20, 50, 100], }: TableProps) => { const isServerSideTable = totalItems !== undefined && page !== undefined && onPageChange !== undefined; + const tableClassNames = { + ...TABLE_DEFAULT_STYLING, + ...className, + }; + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: pageSize, @@ -191,12 +205,15 @@ const Table = ({ }, [pageSize, setPageSize]); return ( -
    -
    - - +
    +
    +
    + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( - + {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - - + ) @@ -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 094/174] 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 095/174] 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 096/174] 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 097/174] 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 098/174] 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 099/174] 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 100/174] 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 101/174] 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() && ( +
    + + +
    + )} + + + ); + })} ))} @@ -311,25 +324,27 @@ const Table = ({ ))} - - {renderFooter && - (footerData && footerData.length > 0 - ? footerTable.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - )) - : footerContent)} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + ))} + + )}
    ({ header.column.getCanSort() ? 'cursor-pointer select-none' : '', - className.headerColumnClassName + { + 'first:w-9 first:pr-0': withCheckbox, + }, + tableClassNames.headerColumnClassName )} >
    @@ -216,12 +236,13 @@ const Table = ({ )} {header.column.getCanSort() && ( -
    +
    ({ )} /> ({ ))}
    + {!isLoading && flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -270,7 +298,7 @@ const Table = ({ emptyContent} {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( -
    +
    ({ onPrevPage={prevPageClickHandler} onNextPage={nextPageClickHandler} onPageChange={pageChangeHandler} + rowOptions={rowOptions} + onRowChange={onPageSizeChange} />
    )} From c31b284cf4eecf68ea40df476d36bfc230798b4b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 5 Dec 2025 11:14:52 +0700 Subject: [PATCH 044/174] 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 045/174] 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 046/174] 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 047/174] 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 048/174] 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 049/174] 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 050/174] 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 051/174] 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 052/174] 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 053/174] 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 054/174] 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 055/174] 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 056/174] 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 057/174] 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 058/174] 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 059/174] 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 060/174] 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 061/174] 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 062/174] 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 e09074eed0cd591c744ba3569bf8c51425075920 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 11:55:47 +0700 Subject: [PATCH 063/174] feat(FE): add sapronak table --- .../{_closing => closing}/detail/layout.tsx | 0 src/app/{_closing => closing}/detail/page.tsx | 6 +- src/components/helper/RequireAuth.tsx | 199 +++++++++++++++--- .../sapronak/SapronakCalculationTable.tsx | 5 + 4 files changed, 176 insertions(+), 34 deletions(-) rename src/app/{_closing => closing}/detail/layout.tsx (100%) rename src/app/{_closing => closing}/detail/page.tsx (86%) create mode 100644 src/components/pages/closing/sapronak/SapronakCalculationTable.tsx 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 86% rename from src/app/_closing/detail/page.tsx rename to src/app/closing/detail/page.tsx index 038e5072..c5619c48 100644 --- a/src/app/_closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -5,6 +5,7 @@ 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'; +import SapronakCalculationTable from '@/components/pages/closing/sapronak/SapronakCalculationTable'; const ClosingDetailPage = () => { const router = useRouter(); @@ -46,7 +47,10 @@ const ClosingDetailPage = () => {
    )} {!isLoadingClosing && isResponseSuccess(closing) && ( - + <> + + + )}
    ); 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/closing/sapronak/SapronakCalculationTable.tsx b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx new file mode 100644 index 00000000..15ee31eb --- /dev/null +++ b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx @@ -0,0 +1,5 @@ +const SapronakCalculationTable = () => { + return
    SapronakCalculationTable
    ; +}; + +export default SapronakCalculationTable; From a5c71ff8ceb5c361d58938e67a0c719926b2dfcd Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:43:22 +0700 Subject: [PATCH 064/174] feat(FE-284): Slicing and API Integration Perhitungan Sapronak --- src/app/closing/detail/page.tsx | 59 +++- .../sapronak/SapronakCalculationTable.tsx | 309 +++++++++++++++++- src/dummy/closing.dummy.ts | 225 +++++++++++++ src/services/api/closing.ts | 35 +- src/types/api/closing/closing.d.ts | 36 ++ 5 files changed, 644 insertions(+), 20 deletions(-) create mode 100644 src/dummy/closing.dummy.ts diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index c5619c48..73cce850 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -4,13 +4,17 @@ 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'; +import { isResponseSuccess } from '@/lib/api-helper'; import SapronakCalculationTable from '@/components/pages/closing/sapronak/SapronakCalculationTable'; +import Tabs from '@/components/Tabs'; +import { useState } from 'react'; const ClosingDetailPage = () => { const router = useRouter(); const searchParams = useSearchParams(); + const [activeTab, setActiveTab] = useState('perhitungan_sapronak'); + const closingId = searchParams.get('closingId'); const { data: closing, isLoading: isLoadingClosing } = useSWR( @@ -24,6 +28,17 @@ const ClosingDetailPage = () => { } ); + const { data: sapronakCalculation, isLoading: isLoadingSapronakCalculation } = + useSWR(`/closing/${closingId}/perhitungan_sapronak`, () => { + const numericId = parseInt(closingId ?? '', 10); + if (isNaN(numericId) || numericId <= 0) { + throw new Error('Invalid closing ID'); + } + const res = ClosingApi.getPerhitunganSapronak(numericId); + console.log(res); + return res; + }); + if (!closingId) { router.back(); @@ -34,24 +49,34 @@ const ClosingDetailPage = () => { ); } - if (!isLoadingClosing && (!closing || isResponseError(closing))) { - router.replace('/404'); - return; - } - return (
    - {isLoadingClosing && ( -
    - -
    - )} - {!isLoadingClosing && isResponseSuccess(closing) && ( - <> - - - - )} + + ), + }, + { + id: 'penjualan', + label: 'Penjualan', + content: isResponseSuccess(closing) && ( + + ), + }, + ]} + />
    ); }; diff --git a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx index 15ee31eb..679ec5e7 100644 --- a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx +++ b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx @@ -1,5 +1,310 @@ -const SapronakCalculationTable = () => { - return
    SapronakCalculationTable
    ; +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { + SapronakCalculation, + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; + +interface SapronakCalculationTableProps { + type?: 'detail'; + initialValues?: SapronakCalculation; +} + +interface FooterSapronakCalculationRow extends RowSapronakCalculation { + _isFooter: true; +} + +const SapronakCalculationTable = ({ + type, + initialValues, +}: SapronakCalculationTableProps) => { + const columns: ColumnDef[] = useMemo( + () => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + const value = props.getValue() as string; + if (isFooter) { + return ( +
    + {value} +
    + ); + } + return value || '-'; + }, + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatCurrency(value)} +
    + ); + }, + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatCurrency(value)} +
    + ); + }, + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + ], + [] + ); + + const createFooterRow = ( + total?: TotalSapronakCalculation + ): FooterSapronakCalculationRow[] => { + if (!total) return []; + return [ + { + id: -999, + tanggal: '', + no_referensi: total.label, + qty_masuk: total.qty_masuk, + qty_keluar: total.qty_keluar, + qty_pakai: total.qty_pakai, + uraian: '', + kategori_produk: '', + harga_beli_per_qty: total.harga_beli_per_qty, + total_harga: total.total_harga, + keterangan: '', + _isFooter: true, + }, + ]; + }; + + const docBroilerFooter = useMemo( + () => createFooterRow(initialValues?.doc_broiler.total), + [initialValues?.doc_broiler.total] + ); + + const ovkFooter = useMemo( + () => createFooterRow(initialValues?.ovk.total), + [initialValues?.ovk.total] + ); + + const pakanFooter = useMemo( + () => createFooterRow(initialValues?.pakan.total), + [initialValues?.pakan.total] + ); + + return ( +
    + <> + + + data={initialValues?.doc_broiler.rows ?? []} + columns={columns} + footerData={docBroilerFooter} + renderFooter={ + (initialValues?.doc_broiler.rows.length ?? 0) > 0 && + !!initialValues?.doc_broiler.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.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', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={initialValues?.ovk.rows ?? []} + columns={columns} + footerData={ovkFooter} + renderFooter={ + (initialValues?.ovk.rows.length ?? 0) > 0 && + !!initialValues?.ovk.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.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', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={initialValues?.pakan.rows ?? []} + columns={columns} + footerData={pakanFooter} + renderFooter={ + (initialValues?.pakan.rows.length ?? 0) > 0 && + !!initialValues?.pakan.total + } + className={{ + containerClassName: cn({ + 'mb-20': initialValues?.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', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + +
    + ); }; export default SapronakCalculationTable; diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..0b1b3f5c --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,225 @@ +import { SapronakCalculation } from '@/types/api/closing/closing'; + +// 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: 59271.85, + total_harga: 4919763149, + }, + }, + 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: 176096.36, + total_harga: 39093192, + }, + }, + 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: 8020.93, + total_harga: 179032981, + }, + }, +}; + +export default DUMMY_SAPRONAK_CALCULATION; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 66f88c76..8f2290ee 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,6 +1,7 @@ +import DUMMY_SAPRONAK_CALCULATION from '@/dummy/closing.dummy'; import { BaseApiService } from './base'; import { BaseApiResponse } from '@/types/api/api-general'; -import { ClosingSales } from '@/types/api/closing/closing'; +import { ClosingSales, SapronakCalculation } from '@/types/api/closing/closing'; export class ClosingApiService extends BaseApiService< ClosingSales, @@ -23,6 +24,38 @@ export class ClosingApiService extends BaseApiService< return undefined; } } + + 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}/${projectFlockId}/perhitungan_sapronak`; + + return await httpClient>(path, { + method: 'GET', + }); + } catch (error: unknown) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + */ + } } 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 64d0d465..c56afb78 100644 --- a/src/types/api/closing/closing.d.ts +++ b/src/types/api/closing/closing.d.ts @@ -27,3 +27,39 @@ export type BaseClosingSales = { }; export type ClosingSales = BaseMetadata & BaseClosingSales; + +// ====== PERHITUNGAN SAPRONAK ====== + +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; +}; From 375b50b6465890f16e756e39cbdd002131ba7560 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:45:07 +0700 Subject: [PATCH 065/174] fix(FE): revert RequireAuth Component --- 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 195bbbe44960f49f0230e5134827d4a0ad83491c Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 12:51:13 +0700 Subject: [PATCH 066/174] fix(FE): change closing folder name --- src/app/{closing => _closing}/detail/layout.tsx | 0 src/app/{closing => _closing}/detail/page.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/app/{closing => _closing}/detail/layout.tsx (100%) rename src/app/{closing => _closing}/detail/page.tsx (100%) 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 100% rename from src/app/closing/detail/page.tsx rename to src/app/_closing/detail/page.tsx From b97cc398549d4253d564aa175c2553d233fdd0e7 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 13:10:03 +0700 Subject: [PATCH 067/174] 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 ea2ada8224714babadce5461e923fd8b88b72531 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:44:31 +0700 Subject: [PATCH 068/174] chore: update daisyui version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d9be201..01bff9ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.5.5", + "daisyui": "^5.5.8", "eslint": "^9", "eslint-config-next": "^15.5.7", "husky": "^9.1.7", @@ -3063,9 +3063,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.5.tgz", - "integrity": "sha512-ekvI93ZkWIJoCOtDl0D2QMxnWvTejk9V5nWBqRv+7t0xjiBXqAK5U6o6JE2RPvlIC3EqwNyUoIZSdHX9MZK3nw==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz", + "integrity": "sha512-6psL9jIEOFOw68V10j/BKCWcRgx8dh81mmNxShr+g7HDM6UHNoPharlp9zq/PQkHNuGU1ZQsajR3HgpvavbRKQ==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 8f90d778..e1f92aaf 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.5.5", + "daisyui": "^5.5.8", "eslint": "^9", "eslint-config-next": "^15.5.7", "husky": "^9.1.7", From 72840e2193a064783529389c1f59ee0d85728c1c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:46:14 +0700 Subject: [PATCH 069/174] chore: set container size value --- src/app/globals.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/globals.css b/src/app/globals.css index e50e020d..3fe7db88 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -43,6 +43,12 @@ @theme { --font-inter: var(--font-inter); + + --container-sm: 40rem; + --container-md: 48rem; + --container-lg: 64rem; + --container-xl: 80rem; + --container-2xl: 96rem; } html { From 84ff5e178b139c8b4a19971ead11bf59867e8525 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:51:48 +0700 Subject: [PATCH 070/174] feat(FE-320): create Closing list page --- src/app/closing/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/closing/page.tsx diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx new file mode 100644 index 00000000..acaa3ee8 --- /dev/null +++ b/src/app/closing/page.tsx @@ -0,0 +1,11 @@ +import ClosingsTable from '@/components/pages/closing/ClosingsTable'; + +const Closing = () => { + return ( +
    + +
    + ); +}; + +export default Closing; From d85cf291932dd2e18709f841adde976af12e199f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:52:12 +0700 Subject: [PATCH 071/174] feat(FE-320): create ClosingsTable component --- .../pages/closing/ClosingsTable.tsx | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 src/components/pages/closing/ClosingsTable.tsx diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx new file mode 100644 index 00000000..91e78c8c --- /dev/null +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -0,0 +1,299 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { LocationApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { ClosingApi } from '@/services/api/closing'; +import { Closing } from '@/types/api/closing'; + +const PROJECT_STATUS_OPTIONS = [ + { + value: 1, + label: 'Pengajuan', + }, + { + value: 2, + label: 'Aktif', + }, +]; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; +}) => { + return ( + + {/* TODO: apply RBAC */} +
    + +
    +
    + ); +}; + +const ClosingsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + transactionDate: '', + realizationDate: '', + locationId: '', + projectStatus: '', + userId: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + transactionDate: 'transaction_date', + realizationDate: 'realization_date', + locationId: 'location_id', + projectStatus: 'project_status', + userId: 'user_id', + }, + }); + + const { data: closings, isLoading: isLoadingClosings } = useSWR( + `${ClosingApi.basePath}${getTableFilterQueryString()}`, + ClosingApi.getAllFetcher + ); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const closingsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'location_name', + header: 'Lokasi', + }, + { + accessorKey: 'project_category', + header: 'Kategori', + }, + { + accessorKey: 'period', + header: 'Periode', + }, + { + accessorKey: 'closing_date', + header: 'Periode', + cell: (props) => + formatDate(props.row.original.closing_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'shed_label', + header: 'Jumlah Kandang', + }, + { + accessorKey: 'sales_paid_amount', + header: 'Jumlah Sudah Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_paid_amount)} + + ), + }, + { + accessorKey: 'sales_remaining_amount', + header: 'Jumlah Sisa Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_remaining_amount)} + + ), + }, + { + accessorKey: 'sales_payment_status', + header: 'Status Pembayaran', + }, + { + accessorKey: 'project_status', + header: 'Status', + }, + { + 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 - 3; + + return ( + <> + {currentPageSize > 3 && ( + + + + )} + + {currentPageSize <= 3 && ( + + + + )} + + ); + }, + }, + ]; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const [selectedLocation, setSelectedLocation] = useState( + null + ); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + updateFilter( + 'locationId', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedProjectStatus, setSelectedProjectStatus] = + useState(null); + + const projectStatusChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedProjectStatus(val as OptionType); + updateFilter( + 'projectStatus', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + return ( + <> +
    +
    +
    +
    + +
    + +
    + + + +
    +
    +
    + + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + }} + /> +
    + + ); +}; + +export default ClosingsTable; From d189252551487e9c460e3b62919387856a6926c0 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:52:45 +0700 Subject: [PATCH 072/174] feat(FE-321): create Closing detail page --- src/app/closing/detail/page.tsx | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/app/closing/detail/page.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx new file mode 100644 index 00000000..6225b8dd --- /dev/null +++ b/src/app/closing/detail/page.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ClosingDetail from '@/components/pages/closing/ClosingDetail'; + +import { ClosingApi } from '@/services/api/closing'; +import { isResponseError, isResponseSuccess } 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: number) => ClosingApi.getGeneralInfo(id) + ); + + if (!closingId) { + router.back(); + + return ( +
    + +
    + ); + } + + if (!isLoadingClosing && (!closing || isResponseError(closing))) { + router.replace('/404'); + return; + } + + return ( +
    + {isLoadingClosing && ( + + )} + + {!isLoadingClosing && isResponseSuccess(closing) && ( + + )} +
    + ); +}; + +export default ClosingDetailPage; From 435cc0aedc5ddf9d614f7d1a0794e0e521bb2302 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:53:05 +0700 Subject: [PATCH 073/174] feat(FE-321): create layout file for closing detail route --- src/app/closing/detail/layout.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/closing/detail/layout.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; From 7615daa22a27593b75c6963fa4a5c89d7a7071bd Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:53:20 +0700 Subject: [PATCH 074/174] chore: update Pagination component --- src/components/Pagination.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index 8b80abf3..43b26d90 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -25,7 +25,7 @@ const PaginationButton = ({ disabled={disabled} onClick={onClick} className={cn( - 'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg text-sm font-semibold text-base-content/50 aspect-square', + 'join-item w-10 h-10 grid place-items-center p-2.5 rounded-lg! text-sm font-semibold text-base-content/50 aspect-square', 'disabled:text-primary disabled:pointer-events-auto! disabled:cursor-not-allowed! disabled:bg-primary/10 disabled:active:translate-y-0' )} > @@ -52,7 +52,7 @@ const EtcPaginationButton = ({ tabIndex={0} role='button' className={cn( - 'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square' + 'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square' )} > ... @@ -61,7 +61,7 @@ const EtcPaginationButton = ({
      {pages.map((pageNumber) => (
    • @@ -80,7 +80,7 @@ const EtcPaginationButton = ({
    - Total Penjualan - - {formatNumber(totals.totalQuantity)} - - {formatNumber(totals.totalWeight)} - - {formatNumber(totals.avgWeight)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} - - {formatCurrency(totals.avgPricePartner)} - - {formatCurrency(totals.totalPartner)} -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Lokasi:{initialValue?.location_name}
    Periode:{initialValue?.period}
    Kategori:{initialValue?.project_category}
    Populasi:{initialValue?.population} Ekor
    Jenis Project:{initialValue?.project_type}
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    +
    +
    + ); +}; + +export default ClosingGeneralInformationTable; From 1ae5c1bd6447acee2ec1ec1f4eaae576dbdb8e27 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:54:15 +0700 Subject: [PATCH 077/174] feat(FE-321): create ClosingIncomingSapronaksTable component --- .../closing/ClosingIncomingSapronaksTable.tsx | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/components/pages/closing/ClosingIncomingSapronaksTable.tsx diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx new file mode 100644 index 00000000..206beb3d --- /dev/null +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingIncomingSapronak } from '@/types/api/closing'; + +interface ClosingIncomingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingIncomingSapronaksTable = ({ + projectFlockId, +}: ClosingIncomingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/incoming${getTableFilterQueryString()}`, + ClosingApi.getAllIncomingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const incomingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks.data.length > 0 + : false + ); + } + }, [incomingSapronaks, isResponseSuccess]); + + return ( + + +
    Sapronak Masuk
    + + +
    + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronaks) && + incomingSapronaks?.data?.length === 0, + }), + }} + /> +
    + + + ); +}; + +export default ClosingIncomingSapronaksTable; From 6f7627ac928481be8ee663406c205425fe8e0ce3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:54:27 +0700 Subject: [PATCH 078/174] feat(FE-321): create ClosingOutgoingSapronaksTable component --- .../closing/ClosingOutgoingSapronaksTable.tsx | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx new file mode 100644 index 00000000..9047e79a --- /dev/null +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingOutgoingSapronak } from '@/types/api/closing'; + +interface ClosingOutgoingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingOutgoingSapronaksTable = ({ + projectFlockId, +}: ClosingOutgoingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/outgoing${getTableFilterQueryString()}`, + ClosingApi.getAllOutgoingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const outgoingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks.data.length > 0 + : false + ); + } + }, [outgoingSapronaks, isResponseSuccess]); + + return ( + + +
    Sapronak Keluar
    + + +
    + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks?.data?.length === 0, + }), + }} + /> +
    + +
    + ); +}; + +export default ClosingOutgoingSapronaksTable; From c350bc0be21819f75eaf36deb4f163b6dea74b36 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 16:54:44 +0700 Subject: [PATCH 079/174] feat(FE-321): create ClosingSapronakTabContent component --- .../closing/ClosingSapronakTabContent.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/components/pages/closing/ClosingSapronakTabContent.tsx diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx new file mode 100644 index 00000000..41c7aa05 --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakTabContent.tsx @@ -0,0 +1,26 @@ +'use client'; + +import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; +import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; + +interface ClosingSapronakTableProps { + projectFlockId?: number; +} + +const ClosingSapronakTabContent = ({ + projectFlockId, +}: ClosingSapronakTableProps) => { + return ( +
    + {projectFlockId && ( + <> + + + + + )} +
    + ); +}; + +export default ClosingSapronakTabContent; From 7f326bedd41e48f0109e52772f01b87f6fa79f47 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 17:13:00 +0700 Subject: [PATCH 080/174] chore(FE-320): add Closing menu --- src/config/constant.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 926db692..bad4a802 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -40,6 +40,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/expense', icon: 'heroicons:wallet', }, + { + text: 'Closing', + link: '/closing', + icon: 'heroicons-outline:presentation-chart-bar', + }, { text: 'Persediaan', link: '/inventory', From 5be67ef01ca5804691d5bef586d3cf2428ce47eb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 17:13:27 +0700 Subject: [PATCH 081/174] chore: update formatDate helper function --- src/lib/helper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 13fdce5d..fe67afef 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -10,6 +10,8 @@ export const sleep = (ms: number = 1000) => new Promise((resolve) => setTimeout(resolve, ms)); export const formatDate = (date: moment.MomentInput, format?: string) => { + if (!date) return '-'; + return moment(date).format(format); }; From 17865d733db1849e8e28ea5c95ef97bbb71d5789 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 17:13:53 +0700 Subject: [PATCH 082/174] feat(FE-323): create ClosingApiService --- src/services/api/closing.ts | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/services/api/closing.ts diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts new file mode 100644 index 00000000..dc0d804a --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,54 @@ +import axios from 'axios'; + +import { BaseApiService } from '@/services/api/base'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, +} from '@/types/api/closing'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; + +export class ClosingApiService extends BaseApiService { + constructor(basePath: string) { + super(basePath); + } + + async getAllIncomingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getAllOutgoingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getGeneralInfo(id: number) { + try { + const getGeneralInfoPath = `${this.basePath}/${id}`; + const getGeneralInfoRes = + await httpClient>( + getGeneralInfoPath + ); + + return getGeneralInfoRes; + } catch (error) { + if ( + axios.isAxiosError>(error) + ) { + return error.response?.data; + } + return undefined; + } + } +} + +export const ClosingApi = new ClosingApiService('/closings'); From 090a3183f7a7eb542694745114cf6b25a3bbe9bb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 6 Dec 2025 17:14:09 +0700 Subject: [PATCH 083/174] feat(FE-323): create Closing type --- src/types/api/closing.d.ts | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/types/api/closing.d.ts diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts new file mode 100644 index 00000000..3f7ba816 --- /dev/null +++ b/src/types/api/closing.d.ts @@ -0,0 +1,55 @@ +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'; + +export type BaseClosing = { + id: number; + location_id: number; + location_name: string; + project_category: 'GROWING' | 'LAYING'; + period: number; + closing_date?: string; + shed_label: string; + shed_count: number; + sales_paid_amount: number; + sales_remaining_amount: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; +}; + +export type Closing = BaseMetadata & BaseClosing; + +export type BaseClosingGeneralInformation = BaseClosing & { + flock_id: number; + period: number; + project_type: 'GROWING' | 'LAYING'; + population: number; + active_house_count: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; + closing_status: string; +}; + +export type ClosingGeneralInformation = BaseMetadata & + BaseClosingGeneralInformation; + +export type ClosingIncomingSapronak = { + id: number; + date: string; + reference_number: string; + transaction_type: string; + product_name: string; + product_category: string; + product_sub_category: string; + source_warehouse: string; + destination_warehouse: string; + quantity: number; + unit: string; + formatted_quantity: string; + notes: string; +}; + +export type ClosingOutgoingSapronak = ClosingIncomingSapronak; From 2d1cabb86b61def72179d476554db95b6798adfb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sun, 7 Dec 2025 14:59:25 +0700 Subject: [PATCH 084/174] 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 = ({
    {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 085/174] 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 086/174] 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 087/174] 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 088/174] 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 089/174] 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 3569955e7fdd74f263711b950b332fa246f51e3b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Mon, 8 Dec 2025 14:01:13 +0700 Subject: [PATCH 090/174] fix(FE): fix warn issue next js --- package-lock.json | 14 ++------------ package.json | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f960d1c5..f0212474 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.7", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1855,7 +1855,6 @@ "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" } @@ -1925,7 +1924,6 @@ "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", @@ -2449,7 +2447,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3063,8 +3060,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/daisyui": { "version": "5.5.8", @@ -3520,7 +3516,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3694,7 +3689,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6173,7 +6167,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6204,7 +6197,6 @@ "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" }, @@ -7091,7 +7083,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7259,7 +7250,6 @@ "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 e1f92aaf..52fc6ce2 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.7", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From 58fb9b0c082c21790bc18d6f7c28bb7d367c9b22 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 8 Dec 2025 14:07:25 +0700 Subject: [PATCH 091/174] 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 092/174] 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 093/174] 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 102/174] 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 103/174] 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 104/174] 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 105/174] 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 106/174] 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 107/174] 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' && (
    @@ -231,6 +257,9 @@ const ProjectFlockDetail = ({

    Kandang Aktif

    + {kandangApprovals && !kandangApprovalsLoading && ( + + )} {/* Badge Row */}
    {' '} - Kandang Aktif ({projectFlock.kandangs.length}) + Kandang Aktif ({projectFlock.kandangs?.length})
    - {projectFlock.kandangs.map((kandang) => ( + {projectFlock.kandangs?.map((kandang) => (
    | 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 - }); + // 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`; @@ -181,7 +180,6 @@ export class ProjectFlockKandangService extends BaseApiService< } return undefined; } - */ } } From 98dfd4564c045eba1970de8abb6d95f481540557 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 11:37:26 +0700 Subject: [PATCH 112/174] chore(FE-323): change closing API route path --- src/services/api/closing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index dc0d804a..041108d0 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('/closings'); +export const ClosingApi = new ClosingApiService('/closing'); From 2c72c44be4756d5efc18762318b6174811b27f03 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 11:38:53 +0700 Subject: [PATCH 113/174] chore(FE-323): update incoming sapronak API endpoint --- src/components/pages/closing/ClosingIncomingSapronaksTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx index 206beb3d..53e45710 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -43,7 +43,7 @@ const ClosingIncomingSapronaksTable = ({ const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak/incoming${getTableFilterQueryString()}`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`, ClosingApi.getAllIncomingSapronakFetcher, { keepPreviousData: true, From 33691f45bb4abf9a74757aeb167892ef30d02948 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 11:38:57 +0700 Subject: [PATCH 114/174] chore(FE-323): update outgoing sapronak API endpoint --- src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx index 9047e79a..5662cff1 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -43,7 +43,7 @@ const ClosingOutgoingSapronaksTable = ({ const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak/outgoing${getTableFilterQueryString()}`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`, ClosingApi.getAllOutgoingSapronakFetcher, { keepPreviousData: true, From a26099b5074dcde4584b05cc5f8f203cd1ece4b5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 15:32:17 +0700 Subject: [PATCH 115/174] feat(FE-347): create ClosingProductionData type --- src/types/api/closing.d.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 3f7ba816..f5ea7047 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -53,3 +53,38 @@ export type ClosingIncomingSapronak = { }; export type ClosingOutgoingSapronak = ClosingIncomingSapronak; + +export type ClosingProductionData = { + purchase: { + initial_population: number; + claim_culling: number; + final_population: number; + feed_in_kg: number; + feed_used_kg: number; + feed_used_per_head_kg: number; + }; + sales: { + sales_kg: number; + sales_head: number; + average_weight_kg: number; + average_price_per_kg: number; + }; + performance: { + depletion_head: number; + depletion_percentage: number; + age_days: number; + mortality_std: number; + mortality_act: number; + deff_mortality: number; + fcr_std: number; + fcr_act: number; + deff_fcr: number; + adg: number; + ip: number; + }; + variance: { + variance_head: number; + variance_head_percentage: number; + variance_feed_kg: number; + }; +}; From abf2735b86a3523a5cde360049dfed97282bdcb2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 15:46:12 +0700 Subject: [PATCH 116/174] feat(FE-347): add getProductionData method --- src/services/api/closing.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 041108d0..f3281dba 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -6,6 +6,7 @@ import { ClosingGeneralInformation, ClosingIncomingSapronak, ClosingOutgoingSapronak, + ClosingProductionData, } from '@/types/api/closing'; import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; @@ -49,6 +50,22 @@ export class ClosingApiService extends BaseApiService { return undefined; } } + + async getProductionData(id: number) { + try { + const getProductionDataPath = `${this.basePath}/${id}/production-data`; + const getProductionDataRes = await httpClient< + BaseApiResponse + >(getProductionDataPath); + + return getProductionDataRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } } export const ClosingApi = new ClosingApiService('/closing'); From 8b8702b1b87c335f09f5f756600dfdc30ecae0be Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 15:51:33 +0700 Subject: [PATCH 117/174] feat(FE-346): create ClosingProductionDataTabContent component --- .../ClosingProductionDataTabContent.tsx | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/components/pages/closing/ClosingProductionDataTabContent.tsx diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx new file mode 100644 index 00000000..ba8a12ed --- /dev/null +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -0,0 +1,268 @@ +'use client'; + +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatNumber } from '@/lib/helper'; + +interface ClosingProductionDataTabContentProps { + projectFlockId: number; +} + +const ClosingProductionDataTabContent = ({ + projectFlockId, +}: ClosingProductionDataTabContentProps) => { + const { data: productionData, isLoading } = useSWR( + `${ClosingApi.basePath}/${projectFlockId}/production-data`, + () => ClosingApi.getProductionData(projectFlockId) + ); + + if (isLoading) { + return ( +
    + +
    + ); + } + + if (!productionData || !isResponseSuccess(productionData)) { + return ( +
    + Gagal memuat data produksi. +
    + ); + } + + const { purchase, sales, performance, variance } = productionData.data; + + // Helper for consistent row styling + const DataRow = ({ + label, + value, + unit = '', + valueClassName = 'font-bold text-gray-800', + unitClassName = 'text-gray-500 w-12 text-right', + }: { + label: string; + value: string | number; + unit?: string; + valueClassName?: string; + unitClassName?: string; + }) => ( +
    + {label} +
    + {value} + {unit && {unit}} +
    +
    + ); + + // Helper for rows with two values (e.g., Deplesi: Ekor & %) + const DoubleDataRow = ({ + label, + value1, + unit1, + value2, + unit2, + value1ClassName = 'font-bold text-gray-800', + value2ClassName = 'font-bold text-blue-500', + }: { + label: string; + value1: string | number; + unit1: string; + value2: string | number; + unit2: string; + value1ClassName?: string; + value2ClassName?: string; + }) => ( +
    + {label} +
    +
    + {value1} + {unit1} +
    +
    + {value2} + {unit2} +
    +
    +
    + ); + + return ( +
    +

    Data Produksi

    + +
    + {/* Left Column */} +
    + {/* Purchase Section */} +
    +

    + Pembelian +

    +
    + + + + + + +
    +
    + + {/* Sales Section */} +
    +

    + Penjualan +

    +
    + + + + +
    +
    +
    + + {/* Divider Line (Absolute centered) */} +
    + + {/* Right Column */} +
    + {/* Performance Section */} +
    +

    + Performance +

    +
    + + + + + + + + + + +
    +
    + + {/* Variance Section (Pushed to bottom) */} +
    +

    Selisih

    +
    + + + +
    +
    +
    +
    +
    + ); +}; + +export default ClosingProductionDataTabContent; From 1567a4016f937d754a6107c033e3a2c970323a7e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 15:55:32 +0700 Subject: [PATCH 118/174] feat(FE-347): use ClosingProductionDataTabContent in dataProduksi tab --- src/components/pages/closing/ClosingDetail.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 147b3fbd..f917cfd8 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -6,9 +6,10 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; +import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; +import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; import { ClosingGeneralInformation } from '@/types/api/closing'; -import ClosingSapronakTabContent from './ClosingSapronakTabContent'; interface ClosingDetailProps { id: number; @@ -48,7 +49,7 @@ const ClosingDetail: React.FC = ({ id, initialValue }) => { { id: 'dataProduksi', label: 'Data Produksi', - content: 'Data Produksi', + content: , }, { id: 'keuangan', From f9dfe7b27fdb74f25a3d793b3fe75c82e75fe06a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 9 Dec 2025 17:57:46 +0700 Subject: [PATCH 119/174] feat(FE-284): Refactor table component support for nesting header --- src/components/Table.tsx | 191 ++-- src/components/helper/RequireAuth.tsx | 199 +++- .../ClosingSapronakCalculationTable.tsx | 340 ++---- src/dummy/closing.dummy.ts | 984 ++++++++++++++++++ src/services/api/closing.ts | 83 +- 5 files changed, 1446 insertions(+), 351 deletions(-) create mode 100644 src/dummy/closing.dummy.ts diff --git a/src/components/Table.tsx b/src/components/Table.tsx index f1466744..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -57,8 +58,6 @@ export interface TableProps { setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); renderFooter?: boolean; - footerContent?: ReactNode; - footerData?: TData[]; withCheckbox?: boolean; rowOptions?: number[]; } @@ -73,22 +72,22 @@ const emptyContentDefaultValue = (
    ); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', - - tableFooterClassName: '', - footerRowClassName: '', - footerColumnClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -111,8 +110,6 @@ const Table = ({ setRowSelection, enableRowSelection, renderFooter = false, - footerContent, - footerData = [], withCheckbox = false, rowOptions = [10, 20, 50, 100], }: TableProps) => { @@ -187,14 +184,6 @@ 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(); @@ -235,58 +224,82 @@ const Table = ({ key={headerGroup.id} className={tableClassNames.headerRowClassName} > - {headerGroup.headers.map((header) => ( -
    -
    - {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() && ( -
    - - -
    - )} -
    -
    - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
    + {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} +
    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/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index cd2b8c68..73c10331 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -5,7 +5,6 @@ import Card from '@/components/Card'; import Table from '@/components/Table'; import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { - ClosingSapronakCalculation, RowSapronakCalculation, TotalSapronakCalculation, } from '@/types/api/closing'; @@ -20,10 +19,6 @@ interface ClosingSapronakCalculationTableProps { projectFlockId: number; } -interface FooterSapronakCalculationRow extends RowSapronakCalculation { - _isFooter: true; -} - const ClosingSapronakCalculationTable = ({ type, projectFlockId, @@ -33,176 +28,124 @@ const ClosingSapronakCalculationTable = ({ () => ClosingApi.getPerhitunganSapronak(projectFlockId) ); - const columns: ColumnDef[] = useMemo( - () => [ - { - header: 'Tanggal', - accessorKey: 'tanggal', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'No. Referensi', - accessorKey: 'no_referensi', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - const value = props.getValue() as string; - if (isFooter) { - return ( -
    - {value} -
    - ); - } - return value || '-'; - }, - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_masuk', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_keluar', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_pakai', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'Uraian', - accessorKey: 'uraian', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Kategori Produk', - accessorKey: 'kategori_produk', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'harga_beli_per_qty', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_harga', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - header: 'Keterangan', - accessorKey: 'keterangan', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - ], - [] - ); - - const createFooterRow = ( + // Helper function to create columns with footer support + const createColumns = ( total?: TotalSapronakCalculation - ): FooterSapronakCalculationRow[] => { - if (!total) return []; - return [ - { - id: -999, - tanggal: '', - no_referensi: total.label, - qty_masuk: total.qty_masuk, - qty_keluar: total.qty_keluar, - qty_pakai: total.qty_pakai, - uraian: '', - kategori_produk: '', - harga_beli_per_qty: total.harga_beli_per_qty, - total_harga: total.total_harga, - keterangan: '', - _isFooter: true, - }, - ]; - }; + ): ColumnDef[] => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => (props.getValue() as string) || '-', + footer: 'Total', + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
    + {formatNumber(total.qty_masuk)} +
    + ) + : '', + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
    + {formatNumber(total.qty_keluar)} +
    + ) + : '', + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
    + {formatNumber(total.qty_pakai)} +
    + ) + : '', + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
    + {formatCurrency(total.harga_beli_per_qty)} +
    + ) + : '', + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
    + {formatCurrency(total.total_harga)} +
    + ) + : '', + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + ]; - const docBroilerFooter = useMemo( + // Memoize columns untuk setiap kategori + const docBroilerColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.doc_broiler.total) - : [], + ? createColumns(sapronakCalculation.data?.doc_broiler.total) + : createColumns(), [sapronakCalculation] ); - const ovkFooter = useMemo( + const ovkColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.ovk.total) - : [], + ? createColumns(sapronakCalculation.data?.ovk.total) + : createColumns(), [sapronakCalculation] ); - const pakanFooter = useMemo( + const pakanColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createFooterRow(sapronakCalculation.data?.pakan.total) - : [], + ? createColumns(sapronakCalculation.data?.pakan.total) + : createColumns(), [sapronakCalculation] ); @@ -212,39 +155,20 @@ const ClosingSapronakCalculationTable = ({ <> data={sapronakCalculation.data?.doc_broiler.rows ?? []} - columns={columns} - footerData={docBroilerFooter} - renderFooter={ - (sapronakCalculation.data?.doc_broiler.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.doc_broiler.total - } + columns={docBroilerColumns} className={{ - containerClassName: cn({ - 'mb-20': - 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', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> @@ -259,29 +183,11 @@ const ClosingSapronakCalculationTable = ({ > data={sapronakCalculation.data?.ovk.rows ?? []} - columns={columns} - footerData={ovkFooter} - renderFooter={ - (sapronakCalculation.data?.ovk.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.ovk.total - } + columns={ovkColumns} className={{ - containerClassName: cn({ - 'mb-20': 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', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> @@ -296,29 +202,11 @@ const ClosingSapronakCalculationTable = ({ > data={sapronakCalculation.data?.pakan.rows ?? []} - columns={columns} - footerData={pakanFooter} - renderFooter={ - (sapronakCalculation.data?.pakan.rows.length ?? 0) > 0 && - !!sapronakCalculation.data?.pakan.total - } + columns={pakanColumns} className={{ - containerClassName: cn({ - 'mb-20': 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', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + containerClassName: 'my-4', }} + renderFooter /> diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..8ebb0164 --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,984 @@ +/** + * Dummy Data untuk Closing API + * + * File ini berisi dummy data untuk testing API Closing sebelum backend siap. + * + * Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts + * + * @example + * // 1. Menggunakan getAllFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * '/closings', + * ClosingApi.getAllFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of Closing objects + * } + * + * @example + * // 2. Menggunakan getSingle: + * import { ClosingApi } from '@/services/api/closing'; + * + * const response = await ClosingApi.getSingle(1); + * if (response?.status === 'success') { + * console.log(response.data); // Single Closing object + * } else if (response?.status === 'error') { + * console.error(response.message); // Error message + * } + * + * @example + * // 3. Menggunakan getGeneralInfo dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const closingId = 1; + * const { data, error, isLoading } = useSWR( + * closingId, + * (id: number) => ClosingApi.getGeneralInfo(id) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // ClosingGeneralInformation object + * } + * + * @example + * // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/incoming`, + * ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingIncomingSapronak + * } + * + * @example + * // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/outgoing`, + * ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingOutgoingSapronak + * } + * + * @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts} + */ + +import { format } from 'date-fns'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingSapronakCalculation, +} from '@/types/api/closing'; +import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; + +// Waktu saat ini untuk created_at/updated_at +const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); +const today = format(new Date(), 'yyyy-MM-dd'); +const yesterday = format( + new Date().setDate(new Date().getDate() - 1), + 'yyyy-MM-dd' +); +const lastWeek = format( + new Date().setDate(new Date().getDate() - 7), + 'yyyy-MM-dd' +); +const lastMonth = format( + new Date().setMonth(new Date().getMonth() - 1), + 'yyyy-MM-dd' +); + +// ====================== +// 👤 Created User +// ====================== +export const createdUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin Utama', +}; + +// ====================== +// 📊 Closing Dummy Data +// ====================== +export const dummyClosings: Closing[] = [ + // 1. Closing dengan status Pengajuan - GROWING + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. Closing dengan status Aktif - LAYING + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. Closing dengan status Selesai - GROWING + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. Closing dengan status Aktif - LAYING + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. Closing dengan status Selesai - GROWING + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. Closing dengan status Pengajuan - LAYING + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, // Belum ada tanggal closing + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. Closing dengan status Aktif - GROWING + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. Closing dengan status Selesai - LAYING + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📊 Closing General Information Dummy Data +// ====================== +export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [ + // 1. General Info - GROWING - Pengajuan + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + flock_id: 101, + project_type: 'GROWING', + population: 15000, + active_house_count: 3, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. General Info - LAYING - Aktif + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + flock_id: 102, + project_type: 'LAYING', + population: 10000, + active_house_count: 2, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. General Info - GROWING - Selesai + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + flock_id: 103, + project_type: 'GROWING', + population: 20000, + active_house_count: 4, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. General Info - LAYING - Aktif + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + flock_id: 104, + project_type: 'LAYING', + population: 5000, + active_house_count: 1, + closing_status: 'In Progress', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. General Info - GROWING - Selesai + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 105, + project_type: 'GROWING', + population: 25000, + active_house_count: 5, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. General Info - LAYING - Pengajuan + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + flock_id: 106, + project_type: 'LAYING', + population: 12000, + active_house_count: 2, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. General Info - GROWING - Aktif + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + flock_id: 107, + project_type: 'GROWING', + population: 18000, + active_house_count: 3, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. General Info - LAYING - Selesai + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 108, + project_type: 'LAYING', + population: 30000, + active_house_count: 6, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📦 Incoming Sapronak Dummy Data +// ====================== +export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'IN-2025-001', + transaction_type: 'Pembelian', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang A1', + quantity: 5000, + unit: 'Ekor', + formatted_quantity: '5,000 Ekor', + notes: 'DOC berkualitas tinggi dari supplier terpercaya', + }, + { + id: 2, + date: yesterday, + reference_number: 'IN-2025-002', + transaction_type: 'Transfer Masuk', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Gudang Area Bandung', + destination_warehouse: 'Kandang B1', + quantity: 100, + unit: 'Sak', + formatted_quantity: '100 Sak (5,000 Kg)', + notes: 'Pakan starter untuk periode awal', + }, + { + id: 3, + date: lastWeek, + reference_number: 'IN-2025-003', + transaction_type: 'Pembelian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Supplier Medion', + destination_warehouse: 'Gudang Farmasi', + quantity: 50, + unit: 'Botol', + formatted_quantity: '50 Botol', + notes: 'Vitamin untuk meningkatkan daya tahan tubuh', + }, + { + id: 4, + date: today, + reference_number: 'IN-2025-004', + transaction_type: 'Pembelian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang C1', + quantity: 200, + unit: 'Sak', + formatted_quantity: '200 Sak (10,000 Kg)', + notes: 'Pakan finisher untuk periode akhir', + }, + { + id: 5, + date: yesterday, + reference_number: 'IN-2025-005', + transaction_type: 'Transfer Masuk', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Area Jakarta', + destination_warehouse: 'Gudang Farmasi', + quantity: 30, + unit: 'Box', + formatted_quantity: '30 Box', + notes: 'Antibiotik untuk pencegahan penyakit', + }, +]; + +// ====================== +// 📤 Outgoing Sapronak Dummy Data +// ====================== +export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'OUT-2025-001', + transaction_type: 'Pemakaian', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang A1', + destination_warehouse: 'Konsumsi Kandang A1', + quantity: 50, + unit: 'Sak', + formatted_quantity: '50 Sak (2,500 Kg)', + notes: 'Pemakaian pakan harian periode starter', + }, + { + id: 2, + date: yesterday, + reference_number: 'OUT-2025-002', + transaction_type: 'Transfer Keluar', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Kandang B1', + destination_warehouse: 'Kandang B2', + quantity: 1000, + unit: 'Ekor', + formatted_quantity: '1,000 Ekor', + notes: 'Transfer DOC ke kandang baru', + }, + { + id: 3, + date: lastWeek, + reference_number: 'OUT-2025-003', + transaction_type: 'Pemakaian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 10, + unit: 'Botol', + formatted_quantity: '10 Botol', + notes: 'Pemberian vitamin untuk meningkatkan kesehatan', + }, + { + id: 4, + date: today, + reference_number: 'OUT-2025-004', + transaction_type: 'Pemakaian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Kandang C1', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 80, + unit: 'Sak', + formatted_quantity: '80 Sak (4,000 Kg)', + notes: 'Pemakaian pakan harian periode finisher', + }, + { + id: 5, + date: yesterday, + reference_number: 'OUT-2025-005', + transaction_type: 'Pemakaian', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang D1', + quantity: 5, + unit: 'Box', + formatted_quantity: '5 Box', + notes: 'Pengobatan untuk ayam yang sakit', + }, + { + id: 6, + date: lastWeek, + reference_number: 'OUT-2025-006', + transaction_type: 'Transfer Keluar', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang E1', + destination_warehouse: 'Kandang E2', + quantity: 30, + unit: 'Sak', + formatted_quantity: '30 Sak (1,500 Kg)', + notes: 'Transfer pakan antar kandang', + }, +]; + +// ====================== +// 📊 Perhitungan Sapronak Dummy Data +// ====================== +export const dummySapronakCalculation: ClosingSapronakCalculation = { + // DOC Broiler Calculation + doc_broiler: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-001', + qty_masuk: 5000, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 40000000, + keterangan: 'Pembelian DOC dari supplier', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'OUT-2025-002', + qty_masuk: 0, + qty_keluar: 1000, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 8000000, + keterangan: 'Transfer DOC ke kandang lain', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'USE-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 400000, + keterangan: 'Mortalitas DOC', + }, + ], + total: { + label: 'Total DOC Broiler', + qty_masuk: 5000, + qty_keluar: 1000, + qty_pakai: 50, + harga_beli_per_qty: 8000, + total_harga: 48400000, + }, + }, + + // OVK Calculation + ovk: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-003', + qty_masuk: 50, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 7500000, + keterangan: 'Pembelian vitamin', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'IN-2025-005', + qty_masuk: 30, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 7500000, + keterangan: 'Pembelian antibiotik', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'OUT-2025-003', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 10, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 1500000, + keterangan: 'Pemakaian vitamin', + }, + { + id: 4, + tanggal: yesterday, + no_referensi: 'OUT-2025-005', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 5, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 1250000, + keterangan: 'Pemakaian antibiotik', + }, + ], + total: { + label: 'Total OVK', + qty_masuk: 80, + qty_keluar: 0, + qty_pakai: 15, + harga_beli_per_qty: 200000, + total_harga: 17750000, + }, + }, + + // Pakan Calculation + pakan: { + rows: [ + { + id: 1, + tanggal: yesterday, + no_referensi: 'IN-2025-002', + qty_masuk: 100, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 45000000, + keterangan: 'Pembelian pakan starter', + }, + { + id: 2, + tanggal: today, + no_referensi: 'IN-2025-004', + qty_masuk: 200, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 96000000, + keterangan: 'Pembelian pakan finisher', + }, + { + id: 3, + tanggal: today, + no_referensi: 'OUT-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 22500000, + keterangan: 'Pemakaian pakan starter', + }, + { + id: 4, + tanggal: today, + no_referensi: 'OUT-2025-004', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 80, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 38400000, + keterangan: 'Pemakaian pakan finisher', + }, + { + id: 5, + tanggal: lastWeek, + no_referensi: 'OUT-2025-006', + qty_masuk: 0, + qty_keluar: 30, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 13500000, + keterangan: 'Transfer pakan ke kandang lain', + }, + ], + total: { + label: 'Total Pakan', + qty_masuk: 300, + qty_keluar: 30, + qty_pakai: 130, + harga_beli_per_qty: 465000, + total_harga: 215400000, + }, + }, +}; + +// ====================== +// 🔧 Dummy API Response Functions +// ====================== + +/** + * Dummy implementation for getAllFetcher + * Returns all closing records + */ +export const dummyGetAllFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: Closing[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: dummyClosings, + }; +}; + +/** + * Dummy implementation for getSingle + * Returns a single closing by ID + */ +export const dummyGetSingle = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closing = dummyClosings.find((c) => c.id === id); + + if (!closing) { + return { + code: 404, + status: 'error', + message: `Closing dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: closing, + }; +}; + +/** + * Dummy implementation for getAllIncomingSapronakFetcher + * Returns all incoming sapronak records + */ +export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingIncomingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak masuk berhasil diambil', + data: dummyIncomingSapronaks, + }; +}; + +/** + * Dummy implementation for getAllOutgoingSapronakFetcher + * Returns all outgoing sapronak records + */ +export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingOutgoingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak keluar berhasil diambil', + data: dummyOutgoingSapronaks, + }; +}; + +/** + * Dummy implementation for getGeneralInfo + * Returns closing general information by ID + */ +export const dummyGetGeneralInfo = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id); + + if (!closingInfo) { + return { + code: 404, + status: 'error', + message: `Closing general information dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing general information berhasil diambil', + data: closingInfo, + }; +}; + +/** + * Dummy implementation for getPerhitunganSapronak + * Returns sapronak calculation data + */ +export const dummyGetPerhitunganSapronak = async ( + id: number +): Promise< + | { + code: number; + status: 'success'; + message: string; + data: ClosingSapronakCalculation; + } + | undefined +> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data perhitungan sapronak berhasil diambil', + data: dummySapronakCalculation, + }; +}; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 9dc5ab30..9514f6a3 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -8,17 +8,62 @@ import { ClosingOutgoingSapronak, ClosingSapronakCalculation, } from '@/types/api/closing'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; +import { + dummyGetAllFetcher, + dummyGetSingle, + dummyGetAllIncomingSapronakFetcher, + dummyGetAllOutgoingSapronakFetcher, + dummyGetGeneralInfo, + dummyGetPerhitunganSapronak, +} from '@/dummy/closing.dummy'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; export class ClosingApiService extends BaseApiService { constructor(basePath: string) { super(basePath); } + async getAllFetcher(endpoint: string): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllFetcher(); + + // Uncomment this when backend is ready + return await httpClientFetcher>(endpoint); + } + + async getSingle(id: number): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetSingle(id); + // } catch (error) { + // if (axios.isAxiosError>(error)) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const getSinglePath = `${this.basePath}/${id}`; + const getSingleRes = + await httpClient>(getSinglePath); + return getSingleRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + async getAllIncomingSapronakFetcher( endpoint: string ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllIncomingSapronakFetcher(); + + // Uncomment this when backend is ready return await httpClientFetcher>( endpoint ); @@ -27,19 +72,37 @@ export class ClosingApiService extends BaseApiService { async getAllOutgoingSapronakFetcher( endpoint: string ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllOutgoingSapronakFetcher(); + + // Uncomment this when backend is ready return await httpClientFetcher>( endpoint ); } - async getGeneralInfo(id: number) { + async getGeneralInfo( + id: number + ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetGeneralInfo(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready try { const getGeneralInfoPath = `${this.basePath}/${id}`; const getGeneralInfoRes = await httpClient>( getGeneralInfoPath ); - return getGeneralInfoRes; } catch (error) { if ( @@ -54,9 +117,21 @@ export class ClosingApiService extends BaseApiService { async getPerhitunganSapronak( id: number ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetPerhitunganSapronak(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready try { const path = `${this.basePath}/${id}/perhitungan_sapronak`; - return await httpClient>( path, { From 8c7640eb9c0ac9f42e8fc8e78a6fa86b98623d62 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 9 Dec 2025 18:14:46 +0700 Subject: [PATCH 120/174] feat(FE-333): adding feature overhead closing report --- src/components/helper/RequireAuth.tsx | 199 +++++++++++++++--- .../pages/closing/ClosingDetail.tsx | 3 +- .../closing/ClosingOverheadTabContent.tsx | 19 ++ .../pages/closing/ClosingOverheadTable.tsx | 159 ++++++++++++++ src/dummy/closing.dummy.ts | 152 +++++++++++++ src/services/api/closing.ts | 37 +++- src/types/api/closing.d.ts | 27 +++ 7 files changed, 558 insertions(+), 38 deletions(-) create mode 100644 src/components/pages/closing/ClosingOverheadTabContent.tsx create mode 100644 src/components/pages/closing/ClosingOverheadTable.tsx 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/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 86f39d18..eff26135 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -10,6 +10,7 @@ import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGe import { ClosingGeneralInformation } from '@/types/api/closing'; import ClosingSapronakTabContent from './ClosingSapronakTabContent'; import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; +import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; interface ClosingDetailProps { id: number; @@ -39,7 +40,7 @@ const ClosingDetail: React.FC = ({ id, initialValue }) => { { id: 'overhead', label: 'Overhead', - content: 'Overhead', + content: , }, { id: 'hppEkspedisi', diff --git a/src/components/pages/closing/ClosingOverheadTabContent.tsx b/src/components/pages/closing/ClosingOverheadTabContent.tsx new file mode 100644 index 00000000..458cff0f --- /dev/null +++ b/src/components/pages/closing/ClosingOverheadTabContent.tsx @@ -0,0 +1,19 @@ +import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable'; + +interface ClosingOverheadTabContentProps { + projectFlockId: number; +} + +const ClosingOverheadTabContent = ({ + projectFlockId, +}: ClosingOverheadTabContentProps) => { + return ( +
    + {projectFlockId && ( + + )} +
    + ); +}; + +export default ClosingOverheadTabContent; diff --git a/src/components/pages/closing/ClosingOverheadTable.tsx b/src/components/pages/closing/ClosingOverheadTable.tsx new file mode 100644 index 00000000..ca5c2671 --- /dev/null +++ b/src/components/pages/closing/ClosingOverheadTable.tsx @@ -0,0 +1,159 @@ +import Card from '@/components/Card'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { ClosingApi } from '@/services/api/closing'; +import { Overhead, OverheadTotal } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; + +interface ClosingOverheadTableProps { + type?: 'detail'; + projectFlockId: number; +} + +const ClosingOverheadTable = ({ + type, + projectFlockId, +}: ClosingOverheadTableProps) => { + const { data: overhead, isLoading: isLoadingOverhead } = useSWR( + `${ClosingApi.basePath}/${projectFlockId}/overhead`, + () => ClosingApi.getOverhead(projectFlockId) + ); + + // Helper function to create columns with footer support + const createColumns = (total?: OverheadTotal): ColumnDef[] => [ + // Group untuk kolom tanpa footer + { + header: 'Nama Item', + accessorFn: (props) => props.item_name, + footer: 'Total Pengeluaran Overhead', + }, + { + header: 'Satuan', + accessorFn: (props) => props.uom_name, + }, + { + header: 'Budget Pengajuan', + footer: '', + columns: [ + { + id: 'budget_quantity', + header: 'Jumlah', + accessorFn: (props) => + props.budget_quantity ? formatNumber(props.budget_quantity) : '-', + footer: total ? () => formatNumber(total.budget_quantity) : '', + }, + { + id: 'budget_unit_price', + header: 'Harga Satuan', + accessorFn: (props) => + props.budget_unit_price + ? formatCurrency(props.budget_unit_price) + : '-', + footer: '', + }, + { + id: 'budget_total_amount', + header: 'Total', + accessorFn: (props) => + props.budget_total_amount + ? formatCurrency(props.budget_total_amount) + : '-', + footer: total ? () => formatCurrency(total.budget_total_amount) : '', + }, + ], + }, + { + header: 'Realisai', + footer: '', + columns: [ + { + id: 'actual_date', + header: 'Tanggal', + accessorFn: (props) => + props.actual_date + ? formatDate(props.actual_date, 'DD MMM, YYYY') + : '-', + footer: '', + }, + { + id: 'actual_quantity', + header: 'Jumlah', + accessorFn: (props) => + props.actual_quantity ? formatNumber(props.actual_quantity) : '-', + footer: total ? () => formatNumber(total.actual_quantity) : '', + }, + { + id: 'actual_unit_price', + header: 'Harga Satuan', + accessorFn: (props) => + props.actual_unit_price + ? formatCurrency(props.actual_unit_price) + : '-', + footer: '', + }, + { + id: 'actual_total_amount', + header: 'Total', + accessorFn: (props) => + props.actual_total_amount + ? formatCurrency(props.actual_total_amount) + : '-', + footer: total ? () => formatCurrency(total.actual_total_amount) : '', + }, + ], + }, + { + id: 'cost_per_bird', + header: 'Rp/Ekor', + accessorFn: (props) => + props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-', + footer: total ? () => formatCurrency(total.cost_per_bird) : '', + }, + ]; + + const columns = useMemo( + () => + isResponseSuccess(overhead) + ? createColumns(overhead.data?.total) + : createColumns(), + [overhead] + ); + + return ( + <> + + + data={ + isResponseSuccess(overhead) ? (overhead.data?.overheads ?? []) : [] + } + columns={columns} + className={{ + containerClassName: 'my-4', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'whitespace-nowrap' + ), + }} + renderFooter={ + isResponseSuccess(overhead) + ? overhead.data?.overheads.length > 0 + : false + } + /> + + + ); +}; + +export default ClosingOverheadTable; diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts index 8ebb0164..3a20cdaf 100644 --- a/src/dummy/closing.dummy.ts +++ b/src/dummy/closing.dummy.ts @@ -82,6 +82,7 @@ import { ClosingGeneralInformation, ClosingIncomingSapronak, ClosingOutgoingSapronak, + ClosingOverhead, ClosingSapronakCalculation, } from '@/types/api/closing'; import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; @@ -846,6 +847,141 @@ export const dummySapronakCalculation: ClosingSapronakCalculation = { }, }; +// ====================== +// 💰 Overhead Dummy Data +// ====================== +export const dummyOverhead: ClosingOverhead = { + overheads: [ + { + item_name: 'Expedisi DOC', + uom_name: 'Ekor', + budget_quantity: 500, + budget_unit_price: 8000, + budget_total_amount: 4000000, + actual_date: '', + actual_quantity: 0, + actual_unit_price: 0, + actual_total_amount: 0, + cost_per_bird: 0, + }, + { + item_name: 'Solar', + uom_name: 'Liter', + budget_quantity: 0, + budget_unit_price: 0, + budget_total_amount: 0, + actual_date: today, + actual_quantity: 20, + actual_unit_price: 10000, + actual_total_amount: 200000, + cost_per_bird: 200, + }, + { + item_name: 'Gaji Karyawan Kandang', + uom_name: 'Orang', + budget_quantity: 3, + budget_unit_price: 3000000, + budget_total_amount: 9000000, + actual_date: today, + actual_quantity: 3, + actual_unit_price: 3200000, + actual_total_amount: 9600000, + cost_per_bird: 640, + }, + { + item_name: 'Listrik Kandang', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 2500000, + budget_total_amount: 2500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 2800000, + actual_total_amount: 2800000, + cost_per_bird: 187, + }, + { + item_name: 'Air Bersih', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 500000, + budget_total_amount: 500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 450000, + actual_total_amount: 450000, + cost_per_bird: 30, + }, + { + item_name: 'Perbaikan Kandang', + uom_name: 'Paket', + budget_quantity: 1, + budget_unit_price: 3000000, + budget_total_amount: 3000000, + actual_date: yesterday, + actual_quantity: 1, + actual_unit_price: 3500000, + actual_total_amount: 3500000, + cost_per_bird: 233, + }, + { + item_name: 'Service Peralatan', + uom_name: 'Kali', + budget_quantity: 2, + budget_unit_price: 500000, + budget_total_amount: 1000000, + actual_date: lastWeek, + actual_quantity: 2, + actual_unit_price: 550000, + actual_total_amount: 1100000, + cost_per_bird: 73, + }, + { + item_name: 'ATK & Supplies', + uom_name: 'Paket', + budget_quantity: 1, + budget_unit_price: 500000, + budget_total_amount: 500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 450000, + actual_total_amount: 450000, + cost_per_bird: 30, + }, + { + item_name: 'Biaya Komunikasi', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 300000, + budget_total_amount: 300000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 320000, + actual_total_amount: 320000, + cost_per_bird: 21, + }, + { + item_name: 'BBM Kendaraan Operasional', + uom_name: 'Liter', + budget_quantity: 200, + budget_unit_price: 10000, + budget_total_amount: 2000000, + actual_date: today, + actual_quantity: 220, + actual_unit_price: 10500, + actual_total_amount: 2310000, + cost_per_bird: 154, + }, + ], + total: { + budget_quantity: 710, + budget_total_amount: 23300000, + actual_quantity: 250, + actual_total_amount: 24530000, + cost_per_bird: 1568, + }, +}; + // ====================== // 🔧 Dummy API Response Functions // ====================== @@ -982,3 +1118,19 @@ export const dummyGetPerhitunganSapronak = async ( data: dummySapronakCalculation, }; }; + +/** + * Dummy implementation for getOverhead + * Returns overhead data + */ +export const dummyGetOverhead = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data overhead berhasil diambil', + data: dummyOverhead, + }; +}; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 9514f6a3..6efd4a36 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -6,6 +6,7 @@ import { ClosingGeneralInformation, ClosingIncomingSapronak, ClosingOutgoingSapronak, + ClosingOverhead, ClosingSapronakCalculation, } from '@/types/api/closing'; import { BaseApiResponse } from '@/types/api/api-general'; @@ -16,6 +17,7 @@ import { dummyGetAllOutgoingSapronakFetcher, dummyGetGeneralInfo, dummyGetPerhitunganSapronak, + dummyGetOverhead, } from '@/dummy/closing.dummy'; import { httpClient, httpClientFetcher } from '@/services/http/client'; @@ -73,12 +75,12 @@ export class ClosingApiService extends BaseApiService { endpoint: string ): Promise> { // TODO: Remove this block when backend is ready - // return await dummyGetAllOutgoingSapronakFetcher(); + return await dummyGetAllOutgoingSapronakFetcher(); // Uncomment this when backend is ready - return await httpClientFetcher>( - endpoint - ); + // return await httpClientFetcher>( + // endpoint + // ); } async getGeneralInfo( @@ -147,6 +149,33 @@ export class ClosingApiService extends BaseApiService { return undefined; } } + + async getOverhead( + id: number + ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetOverhead(id); + // } catch (error) { + // if (axios.isAxiosError>(error)) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const path = `${this.basePath}/${id}/overhead`; + return await httpClient>(path, { + method: 'GET', + }); + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } } export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 0c4c84df..85217002 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -89,3 +89,30 @@ export type ClosingSapronakCalculation = { ovk: ClosingSapronakCalculationItem; pakan: ClosingSapronakCalculationItem; }; + +// ====== OVERHEAD ====== +export type ClosingOverhead = { + overheads: Overhead[]; + total: OverheadTotal; +}; + +export type Overhead = { + item_name: string; + uom_name: string; + budget_quantity: number; + budget_unit_price: number; + budget_total_amount: number; + actual_date: string; + actual_quantity: number; + actual_unit_price: number; + actual_total_amount: number; + cost_per_bird: number; +}; + +export type OverheadTotal = { + budget_quantity: number; + budget_total_amount: number; + actual_quantity: number; + actual_total_amount: number; + cost_per_bird: number; +}; From e90c7d993c4ad3b379a1904172b9352ec1eb6ead Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:44:46 +0700 Subject: [PATCH 121/174] =?UTF-8?q?Merge=20branch=20=E2=80=98development?= =?UTF-8?q?=E2=80=99=20of=20gitlab.com:mbugroup/lti-web-client=20into=20fe?= =?UTF-8?q?at/FE/US-285/marketing-closing-report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Table.tsx | 162 +++++++++----- .../pages/closing/sale/SalesReportTable.tsx | 209 +++++------------- src/types/api/closing.d.ts | 30 ++- 3 files changed, 197 insertions(+), 204 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 970c5bc1..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -31,6 +32,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -53,6 +57,7 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + renderFooter?: boolean; withCheckbox?: boolean; rowOptions?: number[]; } @@ -67,18 +72,22 @@ const emptyContentDefaultValue = (
    ); -const TABLE_DEFAULT_STYLING = { +export const TABLE_DEFAULT_STYLING = { containerClassName: 'w-full mb-20', tableWrapperClassName: 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', tableClassName: 'font-inter w-full table-auto text-sm font-medium', tableHeaderClassName: '', headerRowClassName: '', - headerColumnClassName: 'px-4 py-3 text-base-content/50', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', tableBodyClassName: '', - bodyRowClassName: 'border-t border-t-base-content/10', + bodyRowClassName: 'border-t border-base-content/10', bodyColumnClassName: 'px-4 py-3 text-base-content', paginationClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', }; const Table = ({ @@ -100,6 +109,7 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + renderFooter = false, withCheckbox = false, rowOptions = [10, 20, 50, 100], }: TableProps) => { @@ -214,58 +224,82 @@ const Table = ({ key={headerGroup.id} className={tableClassNames.headerRowClassName} > - {headerGroup.headers.map((header) => ( - -
    - {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() && ( -
    - - -
    - )} -
    - - ))} + {header.column.getCanSort() && ( +
    + + +
    + )} +
    + + ); + })} ))} @@ -290,6 +324,28 @@ const Table = ({ ))} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + {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 122/174] 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 123/174] 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 124/174] 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 125/174] 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 126/174] 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 127/174] 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 128/174] 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 129/174] 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 130/174] 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 131/174] 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 132/174] 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 133/174] 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 a073488c2c2f77c657828cbf407374914e9f0064 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 16:32:50 +0700 Subject: [PATCH 134/174] fix(FE): fixing closing project flock and fetching data in closing report --- src/app/closing/detail/page.tsx | 3 --- src/components/pages/closing/ClosingOverheadTable.tsx | 7 +++++-- .../pages/closing/ClosingSapronakCalculationTable.tsx | 5 ++++- .../project-flock/closing/ProjectFlockClosingForm.tsx | 4 ++-- .../production/project-flock/detail/ProjectFlockDetail.tsx | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 487533be..4848e92a 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -49,9 +49,6 @@ const ClosingDetailPage = () => { {!isLoadingClosing && isResponseSuccess(closing) && ( )} - {!isLoadingSalesReport && isResponseSuccess(salesReport) && ( - - )}
    ); }; diff --git a/src/components/pages/closing/ClosingOverheadTable.tsx b/src/components/pages/closing/ClosingOverheadTable.tsx index ca5c2671..3df0844d 100644 --- a/src/components/pages/closing/ClosingOverheadTable.tsx +++ b/src/components/pages/closing/ClosingOverheadTable.tsx @@ -19,7 +19,10 @@ const ClosingOverheadTable = ({ }: ClosingOverheadTableProps) => { const { data: overhead, isLoading: isLoadingOverhead } = useSWR( `${ClosingApi.basePath}/${projectFlockId}/overhead`, - () => ClosingApi.getOverhead(projectFlockId) + () => ClosingApi.getOverhead(projectFlockId), + { + keepPreviousData: true, + } ); // Helper function to create columns with footer support @@ -66,7 +69,7 @@ const ClosingOverheadTable = ({ ], }, { - header: 'Realisai', + header: 'Realisasi', footer: '', columns: [ { diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index 73c10331..445b7d8c 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -25,7 +25,10 @@ const ClosingSapronakCalculationTable = ({ }: ClosingSapronakCalculationTableProps) => { const { data: sapronakCalculation, isLoading } = useSWR( `/closing/sapronak-calculation/${projectFlockId}`, - () => ClosingApi.getPerhitunganSapronak(projectFlockId) + () => ClosingApi.getPerhitunganSapronak(projectFlockId), + { + keepPreviousData: true, + } ); // Helper function to create columns with footer support diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 0f3cba90..f0b2b4b7 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -62,13 +62,13 @@ const ProjectFlockClosingForm = ({ const errorStock = useMemo(() => { return isResponseSuccess(closingData) ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) - : false; + : true; }, [closingData]); const errorExpense = useMemo(() => { return isResponseSuccess(closingData) ? closingData?.data?.expenses.every((expense) => expense.step < 5) - : false; + : true; }, [closingData]); const isCanCloseValid = !errorStock && !errorExpense; diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index ea3e64a8..39272216 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -428,7 +428,7 @@ const ProjectFlockDetail = ({ - - ); - } - - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 094f0d93..99eb1cb3 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -145,7 +145,7 @@ const ChickinLogsView = ({ }) )} - {initialValues?.approval?.step_number >= 1 && ( + {initialValues?.approval?.step_number <= 2 && ( - {!isApproved && ( + {!isApproved && !isRejected && ( - {type === 'detail' && !isRecordingApproved(initialValues) && ( -
    - + {type === 'detail' && + initialValues?.approval && + !isRecordingApproved(initialValues) && + !isRecordingRejected(initialValues) && ( +
    + - -
    - )} + +
    + )}

    @@ -2803,7 +2818,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Approve Confirmation Modal */} {(type as 'add' | 'edit' | 'detail') === 'detail' && - !isRecordingApproved(initialValues) && ( + !isRecordingApproved(initialValues) && + !isRecordingRejected(initialValues) && ( Date: Mon, 15 Dec 2025 11:13:18 +0700 Subject: [PATCH 157/174] chore(FE-347): update closings API endpoint '/closings' --- src/services/api/closing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index f3281dba..e2c604cc 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -68,4 +68,4 @@ export class ClosingApiService extends BaseApiService { } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); From f58b03ba0e2ab4546d61acc354a16d48816bcdbe Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 16 Dec 2025 14:24:52 +0700 Subject: [PATCH 158/174] fix(FE): revert auth component --- src/components/helper/RequireAuth.tsx | 238 +++++++------------------- 1 file changed, 64 insertions(+), 174 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..65adf48c 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -1,197 +1,87 @@ '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 { 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'; +import { redirectToSSO } from '@/lib/auth-helper'; interface RequireAuthProps { children?: ReactNode; } const RequireAuth = ({ children }: RequireAuthProps) => { - const router = useRouter(); - const { setUser, setIsLoadingUser } = useAuth(); + const { user, setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); - - useEffect(() => { - setIsLoadingUser(isLoadingUserResponse); - }, [isLoadingUserResponse, setIsLoadingUser]); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWR< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + }); 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); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
    - // - //
    - // ); - // } + // Explicitly handle 401 redirect from the component level + useEffect(() => { + 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, userResponse]); - return <>{children}; + useEffect(() => { + setIsLoadingUser(isLoadingUserResponse); + }, [isLoadingUserResponse]); + + if ( + (isLoadingUserResponse && !userResponse && !userErrorResponse) || + (!userResponse && !userErrorResponse) + ) { + return ( +
    + +
    + ); + } + + if (userErrorResponse) { + return ( +
    +

    Authentication Failed

    +

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

    + +
    + ); + } + + return <>{isResponseSuccess(userResponse) && user && children}; }; export default RequireAuth; From 5def3c9f17919f073ad1c4145f70fee893285fbf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:04:43 +0700 Subject: [PATCH 159/174] chore: update next version and install xlsx --- package-lock.json | 120 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 3 +- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0212474..1e8f3fd8 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.7", + "next": "^15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -26,6 +26,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "^0.18.5", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -1082,9 +1083,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2464,6 +2465,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2924,6 +2934,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2965,6 +2988,15 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3035,6 +3067,18 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4180,6 +4224,15 @@ "react": ">=16.8.0" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5654,12 +5707,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -6754,6 +6807,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7515,6 +7580,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7525,6 +7608,27 @@ "node": ">=0.10.0" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index 52fc6ce2..aa26b7bf 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.7", + "next": "^15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -29,6 +29,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "^0.18.5", "yup": "^1.7.0", "zustand": "^5.0.8" }, From b8c6f94db85427fc1a66291d2e9fa9969eebe184 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:06:19 +0700 Subject: [PATCH 160/174] feat(FE-364): create Marketing Report page --- src/app/report/marketing/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/report/marketing/page.tsx diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx new file mode 100644 index 00000000..52a3d4dd --- /dev/null +++ b/src/app/report/marketing/page.tsx @@ -0,0 +1,11 @@ +import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; + +const MarketingReportPage = () => { + return ( +
    + +
    + ); +}; + +export default MarketingReportPage; From e49c247f029066be9b0472bcadedfd5676a52b9c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:06:41 +0700 Subject: [PATCH 161/174] chore: update Tabs component --- src/components/Tabs.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 2ad2477d..8f685452 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -21,6 +21,7 @@ export interface TabsProps className?: | string | { + container?: string; wrapper?: string; tab?: string; content?: string; @@ -53,10 +54,14 @@ const Tabs = ({ onTabChange?.(tabId); }; - const { wrapper: wrapperClassName, tab: tabClassName } = - typeof className === 'object' - ? className - : { wrapper: className, tab: undefined }; + const { + container: containerClassName, + wrapper: wrapperClassName, + tab: tabClassName, + content: contentClassName, + } = typeof className === 'object' + ? className + : { wrapper: className, tab: undefined }; const getTabsClasses = () => { const variantClasses: Record = { @@ -104,7 +109,7 @@ const Tabs = ({ {...props} className={cn( 'w-full', - typeof className === 'string' ? className : undefined + typeof className === 'string' ? className : containerClassName )} >
    @@ -121,7 +126,9 @@ const Tabs = ({ ))}
    - {activeContent &&
    {activeContent}
    } + {activeContent && ( +
    {activeContent}
    + )} ); }; From b9b7e45bc7601c48219966e6da63dbece2abc77a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:06:48 +0700 Subject: [PATCH 162/174] chore: update Dropdown component --- src/components/dropdown/Dropdown.tsx | 166 +++++++++++++-------------- 1 file changed, 82 insertions(+), 84 deletions(-) diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx index 4489231d..5bfa7a7d 100644 --- a/src/components/dropdown/Dropdown.tsx +++ b/src/components/dropdown/Dropdown.tsx @@ -1,111 +1,109 @@ -'use client'; +import React, { ReactNode, useState, useRef } from 'react'; -import { ReactNode, useRef, useEffect, useState } from 'react'; import { cn } from '@/lib/helper'; -interface DropdownProps { +export 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'; + className?: { + wrapper?: string; + trigger?: string; + content?: string; + }; align?: 'start' | 'center' | 'end'; + direction?: 'top' | 'bottom' | 'left' | 'right'; hover?: boolean; - className?: string; - contentClassName?: string; + defaultOpen?: boolean; + open?: boolean; + close?: boolean; + controlled?: boolean; } const Dropdown = ({ trigger, children, - position = 'bottom', - align = 'start', - hover = false, className, - contentClassName, + align, + direction, + hover, + defaultOpen = false, + open, + close, + controlled = false, }: DropdownProps) => { - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(defaultOpen); 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); + const toggleDropdown = () => { + if (!controlled) { + const newState = !isOpen; + setIsOpen(newState); } - - 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); + const getWrapperClasses = () => { + const openState = controlled ? open : isOpen; + + return cn( + 'dropdown', + { + 'dropdown-start': align === 'start', + 'dropdown-center': align === 'center', + 'dropdown-end': align === 'end', + 'dropdown-top': direction === 'top', + 'dropdown-bottom': direction === 'bottom', + 'dropdown-left': direction === 'left', + 'dropdown-right': direction === 'right', + 'dropdown-hover': hover, + 'dropdown-open': openState && !close, + 'dropdown-close': close, + }, + className?.wrapper + ); }; + const getTriggerClasses = () => { + return cn(className?.trigger); + }; + + const getContentClasses = () => { + return cn( + 'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box', + className?.content + ); + }; + + if (controlled) { + return ( +
    + {trigger} + {open && !close && ( +
    + {children} +
    + )} +
    + ); + } + return ( -
    - {/* Trigger Button */} -
    +
    +
    { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > {trigger}
    - - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
    setIsOpen(false)} // Close on item click - > + {!close && ( +
    {children}
    )} From 09d36f504b7b2bc1a0c0cf0910921d327ba6aa81 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:07:04 +0700 Subject: [PATCH 163/174] feat: refresh user data every 13 minutes --- src/components/helper/RequireAuth.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 65adf48c..9dbd2557 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -27,6 +27,9 @@ const RequireAuth = ({ children }: RequireAuthProps) => { SWRHttpKey >('/sso/userinfo', httpClientFetcher, { shouldRetryOnError: false, + + // refresh every 13 minutes + refreshInterval: 13 * 60 * 1000, }); useEffect(() => { From 3a35c72e06c39b0e9188bdd7e730530612cba4ff Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:07:19 +0700 Subject: [PATCH 164/174] feat: add isLoading prop --- src/components/menu/MenuItem.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx index dce81dac..61af4b04 100644 --- a/src/components/menu/MenuItem.tsx +++ b/src/components/menu/MenuItem.tsx @@ -8,6 +8,7 @@ interface MenuItemProps { href?: string; icon?: string; active?: boolean; + isLoading?: boolean; onClick?: () => void; className?: string; } @@ -17,6 +18,7 @@ const MenuItem = ({ href, icon, active = false, + isLoading = false, className, onClick, }: MenuItemProps) => { @@ -50,17 +52,28 @@ const MenuItem = ({ return (
  • - {href && ( + {!isLoading && href && ( {menuItemContent} )} - {!href && ( + {!isLoading && !href && ( )} + + {isLoading && ( + + )}
  • ); }; From 466ea471217dedbb7dbd09bb3ace20375a6b3854 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:08:15 +0700 Subject: [PATCH 165/174] feat(FE-364): create DailyMarketingReportContent component --- .../report/DailyMarketingReportContent.tsx | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 src/components/pages/report/DailyMarketingReportContent.tsx diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx new file mode 100644 index 00000000..1eba4ea3 --- /dev/null +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -0,0 +1,413 @@ +'use client'; + +import { ChangeEventHandler, useState } from 'react'; +import { pdf } from '@react-pdf/renderer'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF'; + +import { Area } from '@/types/api/master-data/area'; +import { + AreaApi, + CustomerApi, + LocationApi, + WarehouseApi, +} from '@/services/api/master-data'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { Customer } from '@/types/api/master-data/customer'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; +import { MARKETING_TYPE_OPTIONS } from '@/config/constant'; +import { httpClient } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { isResponseError } from '@/lib/api-helper'; + +const DailyMarketingReportContent = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter({ + initial: { + search: '', + area_id: '', + location_id: '', + warehouse_id: '', + customer_id: '', + start_date: '', + end_date: '', + marketing_type: '', + filter_by: '', + sort_by: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + area_id: 'area_id', + location_id: 'location_id', + warehouse_id: 'warehouse_id', + customer_id: 'customer_id', + start_date: 'start_date', + end_date: 'end_date', + marketing_type: 'marketing_type', + filter_by: 'filter_by', + sort_by: 'sort_by', + }, + }); + + const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`; + + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); + const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); + + const [selectedArea, setSelectedArea] = useState(null); + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + } = useSelect(AreaApi.basePath, 'id', 'name'); + + const areaChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedArea(val as OptionType); + updateFilter('area_id', val ? ((val as OptionType).value as string) : ''); + }; + + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + updateFilter( + 'location_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedWarehouse, setSelectedWarehouse] = useState( + null + ); + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + } = useSelect(WarehouseApi.basePath, 'id', 'name'); + + const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedWarehouse(val as OptionType); + updateFilter( + 'warehouse_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedCustomer, setSelectedCustomer] = useState( + null + ); + const { + setInputValue: setCustomerInputValue, + options: customerOptions, + isLoadingOptions: isLoadingCustomerOptions, + } = useSelect(CustomerApi.basePath, 'id', 'name'); + + const customerChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedCustomer(val as OptionType); + updateFilter( + 'customer_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const startDateChangeHandler = (e: React.ChangeEvent) => { + updateFilter('start_date', e.target.value ? e.target.value : ''); + }; + + const endDateChangeHandler = (e: React.ChangeEvent) => { + updateFilter('end_date', e.target.value ? e.target.value : ''); + }; + + const [selectedMarketingType, setSelectedMarketingType] = + useState(null); + const marketingTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedMarketingType(val as OptionType); + updateFilter( + 'marketing_type', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const filterByChangeHandler = (filterBy: string) => { + updateFilter('filter_by', filterBy); + }; + + const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => { + updateFilter('sort_by', sort); + }; + + const exportToExcelHandler = async () => { + setIsLoadingExportingToExcel(true); + + await MarketingReportApi.exportDailyMarketingToExcel( + getTableFilterQueryString() + ); + + setIsLoadingExportingToExcel(false); + }; + + const exportToPdfHandler = async () => { + setIsLoadingExportingToPdf(true); + + const params = new URLSearchParams(getTableFilterQueryString()); + + params.set('limit', '9999999'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClient< + BaseApiResponse + >(`${MarketingReportApi.basePath}${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + return; + } + + const openPdf = async () => { + const dailyMarketingReportPdfBlob = await pdf( + + ).toBlob(); + + const dailyMarketingReportPdfUrl = URL.createObjectURL( + dailyMarketingReportPdfBlob + ); + window.open(dailyMarketingReportPdfUrl, '_blank'); + }; + + const downloadPdf = async () => { + const blob = await pdf( + + ).toBlob(); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = 'laporan-penjualan-harian.pdf'; + link.click(); + + URL.revokeObjectURL(url); + }; + + await openPdf(); + } catch (error) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + } + + setIsLoadingExportingToPdf(false); + }; + + const handleReset = () => { + setSelectedArea(null); + setSelectedLocation(null); + setSelectedWarehouse(null); + setSelectedCustomer(null); + setSelectedMarketingType(null); + resetFilter(); + }; + + return ( +
    +
    +

    Penjualan Harian

    +
    + + {/* Filters */} +
    +
    + + + + + + + + + + + +
    + +
    + + +
    + + + + + + Export{' '} + + + } + > + + + + + +
    +
    +
    + + +
    + ); +}; + +export default DailyMarketingReportContent; From 61d85154fdf286a6deb5c4444aa4beaf9a5489ee Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:08:46 +0700 Subject: [PATCH 166/174] feat(FE-368): create DailyMarketingReportPDF component --- .../pages/report/DailyMarketingReportPDF.tsx | 550 ++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 src/components/pages/report/DailyMarketingReportPDF.tsx diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx new file mode 100644 index 00000000..337892b3 --- /dev/null +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -0,0 +1,550 @@ +'use client'; + +import { + Document, + Image, + Page, + StyleSheet, + Text, + View, +} from '@react-pdf/renderer'; + +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; + +interface DailyMarketingReportPDFProps { + data?: DailyMarketingReport; +} + +const DailyMarketingReportPDFStyle = StyleSheet.create({ + page: { + paddingTop: 24, + paddingBottom: 64, + paddingHorizontal: 16, // Reduce padding to fit more columns + orientation: 'landscape', + }, + + companyInfoHeader: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + companyLogo: { + width: 64, + height: 'auto', + }, + companyInfoHeaderDate: { + paddingTop: 8, + fontSize: 10, + }, + companyName: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + }, + companyAddress: { + fontSize: 8, + maxWidth: 400, + marginBottom: 10, + }, + + title: { + marginTop: 16, + fontSize: 14, + lineHeight: '150%', + textAlign: 'center', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + }, + + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + + position: 'absolute', + fontSize: 8, + bottom: 30, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, + + // Table Styles + table: { + width: '100%', + marginTop: 16, + borderWidth: 1, + borderColor: '#000000', + borderBottomWidth: 0, + fontSize: 7, // Smaller font for report + }, + tableRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000000', + alignItems: 'center', + minHeight: 20, + }, + tableHeader: { + backgroundColor: '#f0f0f0', + fontWeight: 'bold', + }, + + // Columns definition (Total 100%) + colNo: { + width: '3%', + padding: 2, + textAlign: 'center', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSoDate: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colDoDate: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colAging: { + width: '3%', + padding: 2, + textAlign: 'center', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colWarehouse: { + width: '7%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colCustomer: { + width: '9%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, // Reduced slightly + colSales: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colProduct: { + width: '8%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, // Reduced slightly + colDoNumber: { + width: '7%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colVehicle: { + width: '5%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colMarketingType: { + width: '5%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colQty: { + width: '4%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colAvgWeight: { + width: '4%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colTotalWeight: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSalesPrice: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colHppPrice: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSalesAmount: { + width: '6%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column + + // Text inside columns + cellText: { + fontSize: 6, + }, + headerText: { + fontSize: 7, + fontWeight: 'bold', + textAlign: 'center', + }, + + // Utils + doubleDivider: { + width: '100%', + height: 6, + borderTop: '2px solid black', + borderBottom: '2px solid black', + }, + + // Summary + summaryContainer: { + marginTop: 12, + flexDirection: 'row', + justifyContent: 'flex-end', + width: '100%', + }, + summaryTable: { + width: '30%', + borderWidth: 1, + borderColor: '#000000', + fontSize: 8, + }, + summaryRow: { + flexDirection: 'row', + padding: 2, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + summaryLabel: { + width: '50%', + fontWeight: 'bold', + }, + summaryValue: { + width: '50%', + textAlign: 'right', + }, +}); + +const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { + const rows = data?.rows || []; + const summary = data?.summary; + + return ( + + + + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + + PT LUMBUNG TELUR INDONESIA + + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + + Laporan Penjualan Harian + + + {/* Data Table */} + + {/* Header */} + + + No + + + + Tgl SO + + + + + Tgl DO + + + + Aging + + + + Gudang + + + + + Pelanggan + + + + Sales + + + + Produk + + + + No DO + + + + Plat No + + + + Tipe + + + Qty + + + + Rerata + + + + Berat + + + + Hrg Jual + + + + + HPP/kg + + + + + Total Jual + + + + + Total HPP + + + + + {/* Rows */} + {rows.map((row, index) => ( + + + + {index + 1} + + + + + {formatDate(row.so_date, 'DD/MM/YYYY')} + + + + + {formatDate(row.do_date, 'DD/MM/YYYY')} + + + + + {row.aging_days} + + + + + {row.warehouse?.name} + + + + + {row.customer?.name} + + + + + {row.sales} + + + + + {row.product?.name} + + + + + {row.do_number} + + + + + {row.vehicle_number} + + + + + {row.marketing_type} + + + + + {formatNumber(row.qty)} + + + + + {formatNumber(row.average_weight_kg)} + + + + + {formatNumber(row.total_weight_kg)} + + + + + {formatCurrency(row.sales_price_per_kg)} + + + + + {formatCurrency(row.hpp_price_per_kg)} + + + + + {formatCurrency(row.sales_amount)} + + + + + {formatCurrency(row.hpp_amount)} + + + + ))} + + + {/* Summary */} + + + + + Total Qty: + + + {formatNumber(summary?.total_qty ?? 0)} + + + + + Total Berat (kg): + + + {formatNumber(summary?.total_weight_kg ?? 0)} + + + + + Total Penjualan: + + + {formatCurrency(summary?.total_sales_amount ?? 0)} + + + + + Total HPP: + + + {formatCurrency(summary?.total_hpp_amount ?? 0)} + + + + + + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default DailyMarketingReportPDF; From 33d8d2aa2af86f1e881715ed12ad0d05e582062a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:09:34 +0700 Subject: [PATCH 167/174] feat(FE-364): create DailyMarketingsTable component --- .../pages/report/DailyMarketingsTable.tsx | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/components/pages/report/DailyMarketingsTable.tsx diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx new file mode 100644 index 00000000..d6914cf1 --- /dev/null +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -0,0 +1,255 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { DailyMarketingRow } from '@/types/api/report/marketing'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; + +interface DailyMarketingsTableProps { + dailyMarketingsReportUrl: string; + onSetPage: (page: number) => void; + pageSize: number; + onSetPageSize: (pageSize: number) => void; + searchValue: string; + onSearchChange: ChangeEventHandler; + onFilterByChange: (filterBy: string) => void; + onSortByChange: (sort: 'asc' | 'desc' | '') => void; +} + +const DailyMarketingsTable = ({ + dailyMarketingsReportUrl, + onSetPage, + pageSize, + onSetPageSize, + searchValue, + onSearchChange, + onFilterByChange, + onSortByChange, +}: DailyMarketingsTableProps) => { + const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR( + dailyMarketingsReportUrl, + MarketingReportApi.getAllDailyMarketingFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + + const dailyMarketingColumns: ColumnDef[] = [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'so_date', + header: 'Tanggal Jual', + cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), + footer: 'Total', + }, + { + accessorKey: 'do_date', + header: 'Tanggal DO', + cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'), + }, + { + accessorKey: 'aging_days', + header: 'Aging', + cell: (props) => `${props.row.original.aging_days} hari`, + }, + { + accessorKey: 'warehouse.name', + header: 'Gudang', + }, + { + accessorKey: 'customer.name', + header: 'Pelanggan', + }, + { + accessorKey: 'do_number', + header: 'No. DO', + }, + { + accessorKey: 'sales', + header: 'Sales/Marketing', + }, + { + accessorKey: 'vehicle_number', + header: 'No. Polisi', + cell: (props) => ( + {props.row.original.vehicle_number} + ), + }, + { + accessorKey: 'marketing_type', + header: 'Marketing Type', + }, + { + accessorKey: 'product.name', + header: 'Produk', + }, + { + accessorKey: 'qty', + header: 'Kuantitas', + cell: (props) => formatNumber(props.row.original.qty), + footer: () => { + const totalQty = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_qty + : 0; + + return formatNumber(totalQty); + }, + }, + { + accessorKey: 'average_weight_kg', + header: 'Bobot Rata-Rata (Kg)', + cell: (props) => formatNumber(props.row.original.average_weight_kg), + }, + { + accessorKey: 'total_weight_kg', + header: 'Bobot Total (Kg)', + cell: (props) => formatNumber(props.row.original.total_weight_kg), + footer: () => { + const totalWeightKg = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_weight_kg + : 0; + + return formatNumber(totalWeightKg); + }, + }, + { + accessorKey: 'sales_price_per_kg', + header: 'Harga Jual (Rp)', + cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), + }, + { + accessorKey: 'hpp_price_per_kg', + header: 'HPP (Rp)', + cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + }, + { + accessorKey: 'sales_amount', + header: 'Total (Rp)', + cell: (props) => formatCurrency(props.row.original.sales_amount), + footer: () => { + const totalSalesAmount = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_sales_amount + : 0; + + return formatCurrency(totalSalesAmount); + }, + }, + ]; + + useEffect(() => { + if (sorting.length === 1) { + onFilterByChange(sorting[0].id); + onSortByChange(sorting[0].desc ? 'desc' : 'asc'); + } else { + onFilterByChange(''); + onSortByChange(''); + } + }, [sorting]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.rows.length > 0 + : false + ); + } + }, [dailyMarketings, isResponseSuccess]); + + return ( + + +
    Penjualan Harian
    + + +
    + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.data.rows + : [] + } + columns={dailyMarketingColumns} + pageSize={pageSize} + onPageSizeChange={onSetPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.meta?.total_results + : 0 + } + onPageChange={onSetPage} + isLoading={isLoadingDailyMarketings} + sorting={sorting} + setSorting={setSorting} + renderFooter={true} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(dailyMarketings) && + dailyMarketings?.data?.rows.length === 0, + }), + }} + /> +
    + + + ); +}; + +export default DailyMarketingsTable; From c9cf33f1ad3788bd61ce3ad38c894890a58f9254 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:10:10 +0700 Subject: [PATCH 168/174] feat(FE-364): create MarketingReportContent component --- .../pages/report/MarketingReportContent.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/components/pages/report/MarketingReportContent.tsx diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx new file mode 100644 index 00000000..160de8b2 --- /dev/null +++ b/src/components/pages/report/MarketingReportContent.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { JSX, useState } from 'react'; + +import Tabs from '@/components/Tabs'; +import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent'; + +type MarketingReportTabType = + | 'daily' + | 'transaction' + | 'hpp-comparison' + | 'daily-hpp'; + +const marketingReportTabs: { + id: MarketingReportTabType; + label: string; + content: JSX.Element; +}[] = [ + { + id: 'daily', + label: 'Penjualan Harian', + content: , + }, +]; + +const MarketingReportContent = () => { + const [activeTab, setActiveTab] = useState('daily'); + + return ( +
    + +
    + ); +}; + +export default MarketingReportContent; From d9b41a67603d7585597b97e87b8031e1811231d7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:11:59 +0700 Subject: [PATCH 169/174] feat: add laporan menu, FILTER_TYPE_OPTIONS, and MARKETING_TYPE_OPTIONS --- src/config/constant.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 96fc8401..c16862af 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -45,6 +45,17 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/closing', icon: 'heroicons-outline:presentation-chart-bar', }, + { + text: 'Laporan', + link: '/report', + icon: 'heroicons-outline:document-text', + submenu: [ + { + text: 'Penjualan', + link: '/report/marketing', + }, + ], + }, { text: 'Persediaan', link: '/inventory', @@ -251,3 +262,29 @@ export const ACCEPTED_FILE_TYPE = { 'image/*': [], }, }; + +export const FILTER_TYPE_OPTIONS = [ + { + label: 'Tanggal Realisasi', + value: 'REALIZATION_DATE', + }, + { + label: 'Tanggal DO', + value: 'DO_DATE', + }, +]; + +export const MARKETING_TYPE_OPTIONS = [ + { + label: 'Ayam', + value: 'ayam', + }, + { + label: 'Telur', + value: 'telur', + }, + { + label: 'Trading', + value: 'trading', + }, +]; From fa63bd8ff9a9b05659726d99ff11bdf2acec0765 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:12:19 +0700 Subject: [PATCH 170/174] feat: create marketing-report.dummy.ts for marketing report dummy data --- src/dummy/report/marketing-report.dummy.ts | 139 +++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/dummy/report/marketing-report.dummy.ts diff --git a/src/dummy/report/marketing-report.dummy.ts b/src/dummy/report/marketing-report.dummy.ts new file mode 100644 index 00000000..ea5af398 --- /dev/null +++ b/src/dummy/report/marketing-report.dummy.ts @@ -0,0 +1,139 @@ +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; + +// TODO: delete this later +export const DAILY_MARKETING_DUMMY_DATA: BaseApiResponse = + { + code: 200, + status: 'success', + message: 'Get daily marketing report successfully', + meta: { + page: 1, + limit: 10, + total_pages: 1, + total_results: 2, + }, + data: { + rows: [ + { + // metadata + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-12-01T08:00:00Z', + updated_at: '2025-12-01T08:00:00Z', + + // row data + no: 1, + so_date: '2025-12-01', + do_date: '2025-12-08', + aging_days: 7, + + warehouse: { + id: 1, + name: 'Warehouse Kandang A', + type: 'KANDANG', + area: { + id: 1, + name: 'Area Barat', + }, + location: { + id: 1, + name: 'Farm Bandung', + address: 'Jl. Raya Farm No. 1', + area: null, + }, + kandang: { + id: 1, + name: 'Kandang A1', + status: 'ACTIVE', + capacity: 5000, + location: null, + pic: null, + }, + }, + + customer: { + id: 1, + name: 'PT Maju Jaya', + pic_id: 10, + pic: { + id: 10, + id_user: 210, + email: 'pic@majujaya.com', + name: 'Budi Santoso', + }, + type: 'BROILER', + address: 'Jl. Industri No. 10', + phone: '08123456789', + email: 'contact@majujaya.com', + account_number: '1234567890', + }, + + sales: 'Andi Wijaya', + + product: { + id: 1, + name: 'Live Chicken', + brand: 'LTI Farm', + sku: 'LC-001', + product_price: 18_000, + selling_price: 20_000, + tax: 0, + expiry_period: 0, + uom: { + id: 1, + name: 'Kg', + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'BROILER', + name: 'Broiler Chicken', + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + suppliers: [], + flags: ['LIVE'], + }, + + do_number: 'DO-2025-0001', + vehicle_number: 'B 1234 CD', + marketing_type: 'REGULAR', + + qty: 1000, + average_weight_kg: 1.8, + total_weight_kg: 1800, + + sales_price_per_kg: 20_000, + hpp_price_per_kg: 18_000, + + sales_amount: 36_000_000, + hpp_amount: 32_400_000, + }, + ], + + summary: { + total_qty: 1000, + total_weight_kg: 1800, + total_sales_amount: 36_000_000, + total_hpp_amount: 32_400_000, + }, + }, + }; From 86cef78a12a627ea1aae7a41ff146089aa1e35f9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:13:37 +0700 Subject: [PATCH 171/174] feat(FE-347): create product data dummy data --- src/services/api/closing.ts | 58 +++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 2511a4f9..279cdd7c 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -22,6 +22,51 @@ import { } from '@/dummy/closing.dummy'; import { httpClient, httpClientFetcher } from '@/services/http/client'; import { ClosingSales } from '@/types/api/closing'; +import { sleep } from '@/lib/helper'; + +export const dummyClosingProductionResponse: BaseApiResponse = + { + code: 200, + status: 'success', + message: 'Closing production data fetched successfully', + data: { + purchase: { + initial_population: 10_000, + claim_culling: 150, + final_population: 9_850, + feed_in_kg: 18_000, + feed_used_kg: 17_200, + feed_used_per_head_kg: 1.75, + }, + + sales: { + sales_kg: 18_500, + sales_head: 9_600, + average_weight_kg: 1.93, + average_price_per_kg: 20_500, + }, + + performance: { + depletion_head: 400, + depletion_percentage: 4, + age_days: 35, + mortality_std: 3.5, + mortality_act: 4, + deff_mortality: 0.5, + fcr_std: 1.6, + fcr_act: 1.72, + deff_fcr: 0.12, + adg: 55, + ip: 320, + }, + + variance: { + variance_head: -250, + variance_head_percentage: -2.5, + variance_feed_kg: -800, + }, + }, + }; export class ClosingApiService extends BaseApiService { constructor(basePath: string) { @@ -137,12 +182,15 @@ export class ClosingApiService extends BaseApiService { async getProductionData(id: number) { try { - const getProductionDataPath = `${this.basePath}/${id}/production-data`; - const getProductionDataRes = await httpClient< - BaseApiResponse - >(getProductionDataPath); + // const getProductionDataPath = `${this.basePath}/${id}/production-data`; + // const getProductionDataRes = await httpClient< + // BaseApiResponse + // >(getProductionDataPath); - return getProductionDataRes; + // return getProductionDataRes; + + await sleep(1000); + return dummyClosingProductionResponse; } catch (error) { if (axios.isAxiosError>(error)) { return error.response?.data; From cf41fbfdaff68284cecb0570b473f1405d765d7a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:18:11 +0700 Subject: [PATCH 172/174] feat(FE-365): create Marketing Report API Service --- src/services/api/report/marketing-report.ts | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/services/api/report/marketing-report.ts diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts new file mode 100644 index 00000000..b1bcafae --- /dev/null +++ b/src/services/api/report/marketing-report.ts @@ -0,0 +1,75 @@ +import * as XLSX from 'xlsx'; +import toast from 'react-hot-toast'; + +import { BaseApiService } from '@/services/api/base'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatDate, sleep } from '@/lib/helper'; + +export class MarketingReportApiService extends BaseApiService< + DailyMarketingReport, + unknown, + unknown +> { + constructor(basePath: string = '/reports/marketings/daily-marketing') { + super(basePath); + } + + async getAllDailyMarketingFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async exportDailyMarketingToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('limit', '9999999'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClientFetcher< + BaseApiResponse + >(`${this.basePath}${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + return; + } + + const rows = dailyMarketingsReport.data.rows; + + const formattedRows = []; + + for (let i = 0; i < rows.length; i++) { + formattedRows.push({ + ...rows[i], + created_user: rows[i].created_user.name, + created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), + updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + warehouse: rows[i].warehouse.name, + customer: rows[i].customer.name, + product: rows[i].product.name, + }); + } + + const ws = XLSX.utils.json_to_sheet(formattedRows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'laporan-penjualan-harian'); + + // triggers download in browser + XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx'); + } catch (error) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + } + } +} + +export const MarketingReportApi = new MarketingReportApiService( + '/reports/marketings/daily-marketing' +); From 2fe4ec981cca760979890947696eeaa9330d2c05 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:18:25 +0700 Subject: [PATCH 173/174] chore: remove double declaration --- src/types/api/master-data/kandang.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/api/master-data/kandang.d.ts b/src/types/api/master-data/kandang.d.ts index c9c14882..eafa0334 100644 --- a/src/types/api/master-data/kandang.d.ts +++ b/src/types/api/master-data/kandang.d.ts @@ -10,7 +10,6 @@ export type BaseKandang = { capacity: number; pic: BaseUser; project_flock_kandang_id?: number; - capacity: number; }; export type Kandang = BaseMetadata & BaseKandang; From 7e0aa4f790e73d0e5701eae0bc6a16d72376a346 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 18 Dec 2025 16:19:30 +0700 Subject: [PATCH 174/174] feat(FE-365): create marketing report types --- src/types/api/report/marketing.d.ts | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/types/api/report/marketing.d.ts diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts new file mode 100644 index 00000000..d1e81f77 --- /dev/null +++ b/src/types/api/report/marketing.d.ts @@ -0,0 +1,61 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; +import { + BaseWarehouseArea, + BaseWarehouseKandang, + BaseWarehouseLocation, + Warehouse, +} from '@/types/api/master-data/warehouse'; +import { Location } from '@/types/api/master-data/location'; +import { Area } from '@/types/api/master-data/area'; +import { BaseProduct } from '@/types/api/master-data/product'; + +export type BaseDailyMarketingRow = { + no: number; + so_date: string; // e.g. "01-Dec-2025" + do_date: string; // e.g. "08-Dec-2025" + aging_days: number; + + warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang; + customer: BaseCustomer; + sales: string; + product: BaseProduct; + + do_number: string; + vehicle_number: string; + marketing_type: string; + + qty: number; + average_weight_kg: number; + total_weight_kg: number; + + sales_price_per_kg: number; + hpp_price_per_kg: number; + + sales_amount: number; + hpp_amount: number; +}; + +export type DailyMarketingRow = BaseMetadata & BaseDailyMarketingRow; + +export interface SalesSummary { + total_qty: number; + total_weight_kg: number; + total_sales_amount: number; + total_hpp_amount: number; +} + +export type DailyMarketingReport = { + rows: DailyMarketingRow[]; + summary: SalesSummary; +}; + +export type MarketingReportFilters = { + area_id?: number; + location_id?: number; + warehouse_id?: number; + customer_id?: number; + start_date?: string; + end_date?: string; + date_type?: 'realized' | 'transaction'; +};