fix(FE): resolve merge conflict

This commit is contained in:
randy-ar
2025-12-10 16:41:21 +07:00
8 changed files with 102 additions and 184 deletions
+1
View File
@@ -127,6 +127,7 @@ build:dev:
NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id' NEXT_PUBLIC_LTI_URL: 'https://dev-lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://dev-auth-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_API_BASE_URL: 'https://dev-api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:dev: deploy:dev:
<<: *deploy_template <<: *deploy_template
+12 -9
View File
@@ -4,7 +4,6 @@ import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import ClosingDetail from '@/components/pages/closing/ClosingDetail'; import ClosingDetail from '@/components/pages/closing/ClosingDetail';
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -20,9 +19,9 @@ const ClosingDetailPage = () => {
(id: number) => ClosingApi.getGeneralInfo(id) (id: number) => ClosingApi.getGeneralInfo(id)
); );
const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR( const { data: salesData, isLoading: isLoadingSales } = useSWR(
closingId, closingId ? `sales-${closingId}` : null,
(id: number) => ClosingApi.getPenjualan(id) () => ClosingApi.getPenjualan(Number(closingId))
); );
if (!closingId) { if (!closingId) {
@@ -40,14 +39,18 @@ const ClosingDetailPage = () => {
return; return;
} }
const isLoading = isLoadingClosing || isLoadingSales;
return ( return (
<div className='w-full p-4 flex flex-row justify-center'> <div className='w-full p-4 flex flex-row justify-center'>
{isLoadingClosing && ( {isLoading && <span className='loading loading-spinner loading-xl' />}
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingClosing && isResponseSuccess(closing) && ( {!isLoading && isResponseSuccess(closing) && (
<ClosingDetail id={Number(closingId)} initialValue={closing.data} /> <ClosingDetail
id={Number(closingId)}
initialValue={closing.data}
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
/>
)} )}
</div> </div>
); );
+43 -165
View File
@@ -1,186 +1,46 @@
'use client'; 'use client';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import useSWR from 'swr';
import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general'; import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// TODO: delete this later, DONT HARDCODE USER DATA import { redirectToSSO } from '@/lib/auth-helper';
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 { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
} }
const RequireAuth = ({ children }: RequireAuthProps) => { const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth(); const { setUser, setIsLoadingUser } = useAuth();
const { data: userResponse, isLoading: isLoadingUserResponse } = const {
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>( data: userResponse,
'/auth/sso/userinfo', isLoading: isLoadingUserResponse,
httpClientFetcher, error: userErrorResponse,
{ } = useSWR<
shouldRetryOnError: false, GetMeResponse & { ok?: boolean },
revalidateOnFocus: false, AxiosError<BaseApiResponse>,
revalidateOnReconnect: false, SWRHttpKey
refreshInterval: 0, >('/sso/userinfo', httpClientFetcher, {
} shouldRetryOnError: false,
); });
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); 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 // TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) { // if (isLoadingUserResponse && !userResponse) {
@@ -191,7 +51,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
// ); // );
// } // }
return <>{children}</>; if (userErrorResponse) {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
<p className='text-gray-600'>
Please try refreshing the page or contact support if the problem
persists.
</p>
<button
className='btn btn-primary'
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}
return <>{isResponseSuccess(userResponse) && children}</>;
}; };
export default RequireAuth; export default RequireAuth;
+12 -3
View File
@@ -7,17 +7,26 @@ import Button from '@/components/Button';
import Tabs from '@/components/Tabs'; import Tabs from '@/components/Tabs';
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; 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 ClosingSapronakTabContent from './ClosingSapronakTabContent';
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
import SalesReportTable from './sale/SalesReportTable';
interface ClosingDetailProps { interface ClosingDetailProps {
id: number; id: number;
initialValue?: ClosingGeneralInformation; initialValue?: ClosingGeneralInformation;
salesData?: BaseClosingSales;
} }
const ClosingDetail: React.FC<ClosingDetailProps> = ({ id, initialValue }) => { const ClosingDetail: React.FC<ClosingDetailProps> = ({
id,
initialValue,
salesData,
}) => {
const [activeTab, setActiveTab] = useState<string>('sapronak'); const [activeTab, setActiveTab] = useState<string>('sapronak');
const closingDetailTabs = useMemo(() => { const closingDetailTabs = useMemo(() => {
@@ -35,7 +44,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({ id, initialValue }) => {
{ {
id: 'penjualan', id: 'penjualan',
label: 'Penjualan', label: 'Penjualan',
content: 'Penjualan', content: <SalesReportTable initialValues={salesData} />,
}, },
{ {
id: 'overhead', id: 'overhead',
@@ -263,7 +263,7 @@ const SalesReportTable = ({
tableWrapperClassName: 'overflow-x-auto', tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm', tableClassName: 'w-full table-auto text-sm',
headerColumnClassName: 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: 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', '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: bodyColumnClassName:
+25
View File
@@ -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.');
}
};
+5 -4
View File
@@ -63,10 +63,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
id: number id: number
): Promise<BaseApiResponse<ClosingSales> | undefined> { ): Promise<BaseApiResponse<ClosingSales> | undefined> {
try { try {
const getPenjualanPath = `${id}/penjualan`; const getPenjualanPath = `${this.basePath}/${id}/penjualan`;
return await this.customRequest<BaseApiResponse<ClosingSales>>( const getPenjualanRes =
getPenjualanPath await httpClient<BaseApiResponse<ClosingSales>>(getPenjualanPath);
);
return getPenjualanRes;
} catch (error) { } catch (error) {
if (axios.isAxiosError<BaseApiResponse<ClosingSales>>(error)) { if (axios.isAxiosError<BaseApiResponse<ClosingSales>>(error)) {
return error.response?.data; return error.response?.data;
+3 -2
View File
@@ -2,6 +2,8 @@ import axios from 'axios';
import type { AxiosError, AxiosRequestConfig } from 'axios'; import type { AxiosError, AxiosRequestConfig } from 'axios';
import { RequestOptions } from '@/services/http/base'; import { RequestOptions } from '@/services/http/base';
import { redirectToSSO } from '@/lib/auth-helper';
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''; const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 }); const axiosClient = axios.create({ baseURL: BASE_URL, timeout: 10_000 });
@@ -9,8 +11,7 @@ axiosClient.interceptors.response.use(
(response) => response, (response) => response,
(error: AxiosError) => { (error: AxiosError) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
const ssoLoginUrl = `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`; redirectToSSO();
window.location.href = ssoLoginUrl;
} }
return Promise.reject(error); return Promise.reject(error);