mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
fix(FE): resolve merge conflict
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user