From 892bb19dfdb81a03802fa91b21a86b70579382d6 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 28 Nov 2025 16:41:01 +0700 Subject: [PATCH 01/89] 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 12/89] 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 13/89] 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 14/89] 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 15/89] 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 16/89] 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 17/89] 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 18/89] 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 19/89] 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 20/89] 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 21/89] 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 22/89] 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 23/89] 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 24/89] 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 25/89] 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 26/89] 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 27/89] 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 28/89] 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 29/89] 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 30/89] 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 31/89] 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 32/89] 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 33/89] 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 34/89] 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 35/89] 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 36/89] 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 37/89] 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 38/89] 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 39/89] 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 40/89] 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 41/89] 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< - ))} + {header.column.getCanSort() && ( +
    + + +
    + )} + + + ); + })} ))} @@ -290,6 +324,28 @@ const Table = ({ ))} + + {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 42/89] 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 43/89] 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 44/89] 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 45/89] 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 46/89] 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 47/89] 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 48/89] 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 49/89] 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 50/89] 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 51/89] 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 52/89] 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 53/89] 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 54/89] 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 55/89] 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 56/89] 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 57/89] 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 58/89] 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 59/89] 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 60/89] fix(FE): adjust chickin and closing after submit --- .../pages/production/chickin/form/ChickinForm.tsx | 3 +-- .../closing/ProjectFlockClosingForm.tsx | 14 +++++++++----- .../project-flock/form/ProjectFlockForm.tsx | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index d7210c8b..84c5b5a5 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -26,7 +26,6 @@ const ChickinFormKandang = ({ initialValues: ProjectFlockKandang; afterSubmit?: () => void; }) => { - const [activeTabId, setActiveTabId] = useState('formChickIn'); const [openChickin, setOpenChickin] = useState(false); const { @@ -41,7 +40,7 @@ const ChickinFormKandang = ({ }); const afterSubmitFormChickin = () => { - setActiveTabId('logsChickIn'); + setOpenChickin(true); afterSubmit && afterSubmit(); refreshApprovals(); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index a12e7369..7d5f1f9b 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -28,7 +28,7 @@ const ProjectFlockClosingForm = ({ projectFlockKandang: ProjectFlockKandang; }) => { const closeModal = useModal(); - const isCanClose = projectFlock.approval.step_number <= 2; + const isCanClose = projectFlock.approval?.step_number <= 1; const [isClosingLoading, setIsClosingLoading] = useState(false); const { data: closingData, isLoading } = useSWR( @@ -116,7 +116,7 @@ const ProjectFlockClosingForm = ({ > Area -
    {projectFlock.area.name}
    +
    {projectFlock.area?.name}
    {/* Lokasi */}
    Lokasi
    -
    {projectFlock.location.name}
    +
    {projectFlock.location?.name}
    {/* Kandang */}
    Kandang
    -
    {projectFlockKandang.kandang.name}
    +
    {projectFlockKandang.kandang?.name}
    {/* Jumlah DOC */}
    @@ -279,7 +279,11 @@ const ProjectFlockClosingForm = ({

    - Estimasi Aggaran Per Flock + Estimasi Anggaran Per Flock

    {formik.values.project_budgets && From b97cc398549d4253d564aa175c2553d233fdd0e7 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sat, 6 Dec 2025 13:10:03 +0700 Subject: [PATCH 61/89] 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 62/89] 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 63/89] 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 64/89] 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 65/89] 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 66/89] 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 67/89] 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 68/89] 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 71/89] 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 72/89] 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 73/89] 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 74/89] 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 75/89] 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 76/89] 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 77/89] 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 78/89] 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 79/89] 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 80/89] 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 7d8a6ff85247d81e7fe3975c009b8a560a88062a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 10:52:49 +0700 Subject: [PATCH 81/89] fix: update next.js version --- package-lock.json | 80 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec1316ae..1aafa758 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" ], @@ -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..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 98dfd4564c045eba1970de8abb6d95f481540557 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 9 Dec 2025 11:37:26 +0700 Subject: [PATCH 82/89] 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 83/89] 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 84/89] 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 e90c7d993c4ad3b379a1904172b9352ec1eb6ead Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 11:44:46 +0700 Subject: [PATCH 85/89] =?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() && ( -
    - - -
    - )} -
    -
    + {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 86/89] 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 87/89] 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 a116f7ca6672a400ee22a7e7a1a43e164467fa9e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 13:32:29 +0700 Subject: [PATCH 88/89] 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 89/89] 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;