diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c37bfd35..6028a8cb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -127,6 +127,7 @@ build:dev:
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
+ NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:dev:
<<: *deploy_template
diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx
index 4848e92a..1b4ebc45 100644
--- a/src/app/closing/detail/page.tsx
+++ b/src/app/closing/detail/page.tsx
@@ -4,7 +4,6 @@ import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
-import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
import { ClosingApi } from '@/services/api/closing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -20,9 +19,9 @@ const ClosingDetailPage = () => {
(id: number) => ClosingApi.getGeneralInfo(id)
);
- const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR(
- closingId,
- (id: number) => ClosingApi.getPenjualan(id)
+ const { data: salesData, isLoading: isLoadingSales } = useSWR(
+ closingId ? `sales-${closingId}` : null,
+ () => ClosingApi.getPenjualan(Number(closingId))
);
if (!closingId) {
@@ -40,14 +39,18 @@ const ClosingDetailPage = () => {
return;
}
+ const isLoading = isLoadingClosing || isLoadingSales;
+
return (
- {isLoadingClosing && (
-
- )}
+ {isLoading && }
- {!isLoadingClosing && isResponseSuccess(closing) && (
-
+ {!isLoading && isResponseSuccess(closing) && (
+
)}
);
diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx
index dbd4b6bc..40e13321 100644
--- a/src/components/helper/RequireAuth.tsx
+++ b/src/components/helper/RequireAuth.tsx
@@ -1,186 +1,46 @@
'use client';
import { ReactNode, useEffect } from 'react';
-import { useRouter } from 'next/navigation';
-import useSWRImmutable from 'swr/immutable';
+import useSWR from 'swr';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
-import { 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 { 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]);
+
+ // Explicitly handle 401 redirect from the component level
+ useEffect(() => {
+ if (userErrorResponse?.response?.status === 401) {
+ redirectToSSO();
+ }
+ }, [userErrorResponse]);
// TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) {
@@ -191,7 +51,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
// );
// }
- return <>{children}>;
+ if (userErrorResponse) {
+ return (
+
+
Authentication Failed
+
+ Please try refreshing the page or contact support if the problem
+ persists.
+
+
+
+ );
+ }
+
+ return <>{isResponseSuccess(userResponse) && children}>;
};
export default RequireAuth;
diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx
index eff26135..fd88fa49 100644
--- a/src/components/pages/closing/ClosingDetail.tsx
+++ b/src/components/pages/closing/ClosingDetail.tsx
@@ -7,17 +7,26 @@ import Button from '@/components/Button';
import Tabs from '@/components/Tabs';
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
-import { ClosingGeneralInformation } from '@/types/api/closing';
+import {
+ ClosingGeneralInformation,
+ BaseClosingSales,
+} from '@/types/api/closing';
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
+import SalesReportTable from './sale/SalesReportTable';
interface ClosingDetailProps {
id: number;
initialValue?: ClosingGeneralInformation;
+ salesData?: BaseClosingSales;
}
-const ClosingDetail: React.FC = ({ id, initialValue }) => {
+const ClosingDetail: React.FC = ({
+ id,
+ initialValue,
+ salesData,
+}) => {
const [activeTab, setActiveTab] = useState('sapronak');
const closingDetailTabs = useMemo(() => {
@@ -35,7 +44,7 @@ const ClosingDetail: React.FC = ({ id, initialValue }) => {
{
id: 'penjualan',
label: 'Penjualan',
- content: 'Penjualan',
+ content: ,
},
{
id: 'overhead',
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx
index e509eb7d..89cb6615 100644
--- a/src/components/pages/closing/sale/SalesReportTable.tsx
+++ b/src/components/pages/closing/sale/SalesReportTable.tsx
@@ -263,7 +263,7 @@ const SalesReportTable = ({
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerColumnClassName:
- 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
+ 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
bodyColumnClassName:
diff --git a/src/lib/auth-helper.ts b/src/lib/auth-helper.ts
new file mode 100644
index 00000000..97d31a9f
--- /dev/null
+++ b/src/lib/auth-helper.ts
@@ -0,0 +1,25 @@
+/**
+ * Redirects the user to the SSO login page with loop protection.
+ *
+ * This function checks a session storage timestamp to ensure that redirects
+ * do not happen too frequently (blocking infinite redirect loops).
+ */
+export const redirectToSSO = () => {
+ if (typeof window === 'undefined') return;
+
+ const lastRedirect = sessionStorage.getItem('auth_redirect_timestamp');
+ const now = Date.now();
+
+ // Loop protection: allow redirect only if last one was > 2 seconds ago
+ // or if no redirect has happened yet.
+ if (!lastRedirect || now - parseInt(lastRedirect, 10) > 2000) {
+ sessionStorage.setItem('auth_redirect_timestamp', now.toString());
+ // const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`;
+
+ const ltiSsoStart = `${process.env.NEXT_PUBLIC_API_BASE_URL as string}/sso/start?client_id=${process.env.NEXT_PUBLIC_CLIENT_ID as string}&redirect_url=${window.location.href}`;
+ const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${ltiSsoStart}`;
+ window.location.href = ssoLoginUrl;
+ } else {
+ console.error('Redirect loop detected. Aborting redirect.');
+ }
+};
diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts
index 5c1911f0..cc2ad4e1 100644
--- a/src/services/api/closing.ts
+++ b/src/services/api/closing.ts
@@ -63,10 +63,11 @@ export class ClosingApiService extends BaseApiService {
id: number
): Promise | undefined> {
try {
- const getPenjualanPath = `${id}/penjualan`;
- return await this.customRequest>(
- getPenjualanPath
- );
+ const getPenjualanPath = `${this.basePath}/${id}/penjualan`;
+ const getPenjualanRes =
+ await httpClient>(getPenjualanPath);
+
+ return getPenjualanRes;
} catch (error) {
if (axios.isAxiosError>(error)) {
return error.response?.data;
diff --git a/src/services/http/client.ts b/src/services/http/client.ts
index f9389a16..68b5282a 100644
--- a/src/services/http/client.ts
+++ b/src/services/http/client.ts
@@ -2,6 +2,8 @@ import axios from 'axios';
import type { AxiosError, AxiosRequestConfig } from 'axios';
import { RequestOptions } from '@/services/http/base';
+import { redirectToSSO } from '@/lib/auth-helper';
+
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 });
@@ -9,8 +11,7 @@ axiosClient.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
- const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`;
- window.location.href = ssoLoginUrl;
+ redirectToSSO();
}
return Promise.reject(error);