From 9af140e58d0f3737dbfe8940a1083e24591abaf2 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 16:56:25 +0700 Subject: [PATCH 1/7] fix(FE): fix merge conflict --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0187471..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -160,6 +160,6 @@ deploy:dev: # variables: # S3_BUCKET: "lti-erp.mbugroup.id" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: +# environment: # name: production From 4ec455b3b723cbd548de0938d73a69fbb4977e61 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 10 Dec 2025 23:54:59 +0700 Subject: [PATCH 2/7] feat(FE): Add credit_term to purchase forms and types --- .gitlab-ci.yml | 2 +- .../order/PurchaseOrderStaffApprovalForm.tsx | 8 +- .../request/PurchaseRequestForm.schema.ts | 6 ++ .../form/request/PurchaseRequestForm.tsx | 79 ++++++++++++++++++- .../purchase/order/PurchaseOrderDetail.tsx | 2 - .../purchase/order/PurchaseOrderInvoice.tsx | 3 + src/types/api/purchase/purchase.d.ts | 10 ++- 7 files changed, 99 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f0187471..6028a8cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -160,6 +160,6 @@ deploy:dev: # variables: # S3_BUCKET: "lti-erp.mbugroup.id" # CLOUDFRONT_DISTRIBUTION_ID: "ddfd" -# environment: +# environment: # name: production diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 94998a37..1fcd7a94 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -314,7 +314,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { @@ -362,7 +364,9 @@ const PurchaseOrderStaffApprovalForm = ({ const isNewItemForm = !formItem.purchase_item_id || formItem.purchase_item_id === 0; - let cleanPayload: UpdateStaffApprovalRequestPayload['items'][0]; + let cleanPayload: NonNullable< + UpdateStaffApprovalRequestPayload['items'] + >[0]; if (isNewItemForm) { cleanPayload = { diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts index 05167715..67a694bc 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.schema.ts @@ -7,6 +7,7 @@ type PurchaseRequestFormSchemaType = { label: string; } | null; supplier_id: number; + credit_term: number; area?: { value: number; label: string; @@ -81,6 +82,10 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema ({ warehouse_id: Number(item.warehouse_id) || 0, @@ -338,6 +342,27 @@ const PurchaseRequestForm = ({ }; // ===== UTILITY FUNCTIONS ===== + const updateCreditTermBasedOnSupplier = useCallback( + (supplierId: number) => { + if (supplierId > 0 && isResponseSuccess(supplierRawData)) { + const supplierData = supplierRawData.data.find( + (s: Supplier) => s.id === supplierId + ); + if (supplierData?.due_date) { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', supplierData.due_date.toString()); + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + } + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + } + }, + [supplierRawData] + ); + const resetPurchaseItems = useCallback(() => { if (formik.values.items) { formik.values.items.forEach((_, idx) => { @@ -352,6 +377,16 @@ const PurchaseRequestForm = ({ }, []); // ===== SIDE EFFECTS ===== + useEffect(() => { + if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) { + updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id)); + resetPurchaseItems(); + } else { + formik.setFieldTouched('credit_term', false); + formik.setFieldValue('credit_term', ''); + resetPurchaseItems(); + } + }, [formik.values.supplier_id]); // ===== FORM HANDLERS ===== const handleSupplierChange = useCallback( @@ -367,6 +402,23 @@ const PurchaseRequestForm = ({ [] ); + const handleCreditTermChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + + formik.setFieldTouched('credit_term', true); + formik.setFieldValue('credit_term', value); + }, + [] + ); + + const handleCreditTermBlur = useCallback( + (e: React.FocusEvent) => { + formik.handleBlur(e); + }, + [formik] + ); + const handleAreaChange = useCallback( (val: OptionType | OptionType[] | null) => { const area = val as OptionType | null; @@ -447,7 +499,7 @@ const PurchaseRequestForm = ({ body: 'flex flex-col gap-6', }} > -
+
+ + -
+
{ {purchaseData?.supplier?.alias || ''}) {purchaseData?.supplier?.category || '-'} + + Credit Term: {purchaseData?.credit_term || 0} hari + Due Date:{' '} {purchaseData?.due_date diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 2dcde2d9..e4de565b 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -51,6 +51,7 @@ export type BasePurchase = { po_document_path?: string | null; po_date: string; supplier: Supplier; + credit_term?: number; due_date: string; notes?: string | null; deleted_at?: string | null; @@ -66,8 +67,9 @@ export type Purchase = BaseMetadata & BasePurchase; export type CreatePurchaseRequestPayload = { supplier_id: number; + credit_term: number; notes?: string | null; - items: { + items?: { warehouse_id: number; product_id: number; qty: number; @@ -77,7 +79,7 @@ export type CreatePurchaseRequestPayload = { export type CreateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: { + items?: { purchase_item_id: number; qty: number; price: number; @@ -88,7 +90,7 @@ export type CreateStaffApprovalRequestPayload = { export type UpdateStaffApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: Array<{ + items?: Array<{ purchase_item_id?: number; product_id?: number; warehouse_id?: number; @@ -106,7 +108,7 @@ export type CreateManagerApprovalRequestPayload = { export type CreateAcceptApprovalRequestPayload = { action: 'APPROVED' | 'REJECTED'; notes?: string | null; - items: { + items?: { purchase_item_id: number; received_date: string; travel_number: string; From a73f9a1acd6c19284810dc1c96c99aff491020f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 11 Dec 2025 10:46:21 +0700 Subject: [PATCH 3/7] fix(FE): Update recording detail links to include production path --- .husky/pre-commit | 3 --- 1 file changed, 3 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 3782914b..e69de29b 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +0,0 @@ -npm run format -npm run lint -npm run build \ No newline at end of file From 69206d45248f4a664c6ae1f2e25f4aa7452f9388 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 11 Dec 2025 10:46:38 +0700 Subject: [PATCH 4/7] fix(FE): Update recording detail links to include production path --- .../pages/production/recording/RecordingTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 27b2d5c6..965e6c9e 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -55,7 +55,7 @@ const RowOptionsMenu = ({ return ( - {!isApproved && ( + {!isApproved && !isRejected && ( - {type === 'detail' && !isRecordingApproved(initialValues) && ( -
- + {type === 'detail' && + initialValues?.approval && + !isRecordingApproved(initialValues) && + !isRecordingRejected(initialValues) && ( +
+ - -
- )} + +
+ )}

@@ -2803,7 +2818,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Approve Confirmation Modal */} {(type as 'add' | 'edit' | 'detail') === 'detail' && - !isRecordingApproved(initialValues) && ( + !isRecordingApproved(initialValues) && + !isRecordingRejected(initialValues) && ( Date: Tue, 16 Dec 2025 14:24:52 +0700 Subject: [PATCH 7/7] fix(FE): revert auth component --- src/components/helper/RequireAuth.tsx | 238 +++++++------------------- 1 file changed, 64 insertions(+), 174 deletions(-) diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..65adf48c 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -1,197 +1,87 @@ 'use client'; import { ReactNode, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import useSWRImmutable from 'swr/immutable'; +import useSWR from 'swr'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { GetMeResponse } from '@/types/api/api-general'; - -// TODO: delete this later, DONT HARDCODE USER DATA -const DUMMY_USER = { - id: 1, - email: 'admin@mbugroup.id', - npk: '0001', - name: 'Super Admin', - image: null, - created_at: '2025-09-30T03:24:20.899229Z', - updated_at: '2025-09-30T03:24:20.899229Z', - roles: [ - { - id: 1, - key: 'mbu.super_admin', - name: 'MBU Administrator', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - permissions: [ - { - id: 1, - name: 'mbu:purchase:read', - action: 'read', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 2, - name: 'mbu:purchase:create', - action: 'create', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 3, - name: 'mbu:purchase:approve', - action: 'approve', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - ], - }, - { - id: 2, - key: 'lti.super_admin', - name: 'LTI Administrator', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - permissions: [ - { - id: 4, - name: 'lti:purchase:read', - action: 'read', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 5, - name: 'lti:purchase:create', - action: 'create', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 6, - name: 'lti:purchase:approve', - action: 'approve', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - ], - }, - { - id: 3, - key: 'manbu.super_admin', - name: 'MANBU Administrator', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - permissions: [ - { - id: 7, - name: 'manbu:purchase:read', - action: 'read', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 8, - name: 'manbu:purchase:create', - action: 'create', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 9, - name: 'manbu:purchase:approve', - action: 'approve', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; +import { redirectToSSO } from '@/lib/auth-helper'; interface RequireAuthProps { children?: ReactNode; } const RequireAuth = ({ children }: RequireAuthProps) => { - const router = useRouter(); - const { setUser, setIsLoadingUser } = useAuth(); + const { user, setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); - - useEffect(() => { - setIsLoadingUser(isLoadingUserResponse); - }, [isLoadingUserResponse, setIsLoadingUser]); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWR< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + }); useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + // Explicitly handle 401 redirect from the component level + useEffect(() => { + if ( + isResponseError(userResponse) && + userErrorResponse?.response?.status === 401 + ) { + // Clear cache to prevent stale data from rendering children + // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate + setUser(undefined); + redirectToSSO(); + } + }, [userErrorResponse, setUser, userResponse]); - return <>{children}; + useEffect(() => { + setIsLoadingUser(isLoadingUserResponse); + }, [isLoadingUserResponse]); + + if ( + (isLoadingUserResponse && !userResponse && !userErrorResponse) || + (!userResponse && !userErrorResponse) + ) { + return ( +
+ +
+ ); + } + + if (userErrorResponse) { + return ( +
+

Authentication Failed

+

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

+ +
+ ); + } + + return <>{isResponseSuccess(userResponse) && user && children}; }; export default RequireAuth;