Compare commits
12 Commits
306080f592
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 643dd15252 | |||
| 9b5941956f | |||
| bf8daa74fa | |||
| bb2aa0e3cd | |||
| 8a7dfc2c86 | |||
| 0ec6ef1841 | |||
| c9e28043e4 | |||
| fb11249d3f | |||
| 3f9452cf8e | |||
| ea1a678a25 | |||
| 8ca596af86 | |||
| 46736523fe |
@ -1,10 +1,12 @@
|
|||||||
VITE_APP_NAME=tpay-dashboard-tl
|
VITE_APP_NAME=revenue-tt-dashboard
|
||||||
VITE_APP_VERSION=1=9.1.1
|
VITE_APP_VERSION=1=9.1.1
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
# VITE_APP_API_URL=
|
# VITE_APP_API_URL=
|
||||||
# VITE_APP_API_URL=http://127.0.0.1:4003/apitest
|
# VITE_APP_API_URL=http://127.0.0.1:4003/apitest
|
||||||
VITE_APP_API_URL=https://tpay.shiblysolution.id/api
|
# VITE_APP_API_URL=https://tpay.shiblysolution.id/api
|
||||||
|
VITE_APP_API_URL=http://127.0.0.1:7001/api
|
||||||
|
VITE_APP_API_URL_MASTER_DATA=http://127.0.0.1:7002/api
|
||||||
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
||||||
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
||||||
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting
|
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
VITE_APP_NAME=tpay-dashboard-tl
|
VITE_APP_NAME=revenue-tt-dashboard
|
||||||
VITE_APP_VERSION=1=9.1.1
|
VITE_APP_VERSION=1=9.1.1
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
VITE_APP_API_URL=https://api-v2.tpay.tl/api
|
VITE_APP_API_URL=https://api-v2.tpay.tl/api
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
VITE_APP_NAME=tpay-dashboard-tl
|
VITE_APP_NAME=revenue-tt-dashboard
|
||||||
VITE_APP_VERSION=1=9.1.1
|
VITE_APP_VERSION=1=9.1.1
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
VITE_APP_API_URL=https://tpay.shiblysolution.id/api
|
VITE_APP_API_URL=http://127.0.0.1:7001/api
|
||||||
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
||||||
VITE_BASE_URL=/dashboard/
|
VITE_BASE_URL=/dashboard/
|
||||||
VITE_ENV=staging
|
VITE_ENV=staging
|
||||||
@ -2,8 +2,8 @@
|
|||||||
<html class="h-full" lang="en" dir="ltr">
|
<html class="h-full" lang="en" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/media/app/favicon.ico" />
|
<!-- <link rel="icon" href="/media/app/favicon.ico" /> -->
|
||||||
<!-- <link rel="icon" href="/media/app/app-logo.png.png" /> -->
|
<link rel="icon" href="/media/app/logo-tt.jpeg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="description" content="" />
|
<meta name="description" content="" />
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"@firebase/firestore": "^4.7.4",
|
"@firebase/firestore": "^4.7.4",
|
||||||
"@formatjs/intl-pluralrules": "^5.3.4",
|
"@formatjs/intl-pluralrules": "^5.3.4",
|
||||||
"@formatjs/intl-relativetimeformat": "^11.4.4",
|
"@formatjs/intl-relativetimeformat": "^11.4.4",
|
||||||
|
"@googlemaps/markerclusterer": "^2.6.2",
|
||||||
"@mui/base": "5.0.0-beta.40",
|
"@mui/base": "5.0.0-beta.40",
|
||||||
"@mui/icons-material": "^6.4.6",
|
"@mui/icons-material": "^6.4.6",
|
||||||
"@mui/material": "^6.1.6",
|
"@mui/material": "^6.1.6",
|
||||||
@ -59,6 +60,7 @@
|
|||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"lucide-react": "^0.456.0",
|
"lucide-react": "^0.456.0",
|
||||||
"metronic-tailwind-react": "file:",
|
"metronic-tailwind-react": "file:",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
@ -97,7 +99,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/leaflet": "^1.9.14",
|
"@types/google.maps": "^3.65.2",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.6",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
"@types/papaparse": "^5.3.16",
|
"@types/papaparse": "^5.3.16",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
|
|||||||
BIN
public/media/app/logo-tt.jpeg
Normal file
BIN
public/media/app/logo-tt.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@ -8,6 +8,8 @@ const RequireAuth = () => {
|
|||||||
const { auth, loading } = useAuthContext();
|
const { auth, loading } = useAuthContext();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// console.log('RequireAuth render: loading =', loading, ' auth =', auth);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ScreenLoader />;
|
return <ScreenLoader />;
|
||||||
}
|
}
|
||||||
@ -15,4 +17,4 @@ const RequireAuth = () => {
|
|||||||
return auth ? <Outlet /> : <Navigate to="/auth/login" replace />;
|
return auth ? <Outlet /> : <Navigate to="/auth/login" replace />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { RequireAuth };
|
export { RequireAuth };
|
||||||
@ -2,14 +2,21 @@ import { type TLanguageCode } from '@/i18n';
|
|||||||
|
|
||||||
export interface AuthModel {
|
export interface AuthModel {
|
||||||
id: string;
|
id: string;
|
||||||
access_token: string;
|
// access_token: string;
|
||||||
refresh_token: string;
|
// refresh_token: string;
|
||||||
role_name: string;
|
role_name: string;
|
||||||
roles_list: [];
|
// roles_list: [];
|
||||||
company: any;
|
// company: any;
|
||||||
user: any;
|
// user: any;
|
||||||
// api_token: string;
|
// // api_token: string;
|
||||||
statusbalance: string;
|
// statusbalance: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
user?: UserModel;
|
||||||
|
statusbalance?: string;
|
||||||
|
access_token?: string;
|
||||||
|
token_type?: string;
|
||||||
|
// language?: TLanguageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserModel {
|
export interface UserModel {
|
||||||
@ -18,18 +25,21 @@ export interface UserModel {
|
|||||||
name: string;
|
name: string;
|
||||||
password: string | undefined;
|
password: string | undefined;
|
||||||
email: string;
|
email: string;
|
||||||
id_role: string;
|
customer?: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
// id_role: string;
|
||||||
role_name: string;
|
role_name: string;
|
||||||
roles_list: [];
|
// roles_list: [];
|
||||||
token: AuthModel;
|
// token: AuthModel;
|
||||||
|
|
||||||
first_name: string;
|
// first_name: string;
|
||||||
last_name: string;
|
// last_name: string;
|
||||||
fullname?: string;
|
// fullname?: string;
|
||||||
occupation?: string;
|
// occupation?: string;
|
||||||
companyName?: string;
|
// companyName?: string;
|
||||||
phone?: string;
|
// phone?: string;
|
||||||
roles?: number[];
|
// roles?: number[];
|
||||||
pic?: string;
|
// pic?: string;
|
||||||
language?: TLanguageCode;
|
// language?: TLanguageCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ const Login = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>TPAY | Sign In</title>
|
<title>REVENUE | Sign In</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="card max-w-[390px] w-full">
|
<div className="card max-w-[390px] w-full">
|
||||||
<form
|
<form
|
||||||
@ -145,54 +145,6 @@ const Login = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="form-label text-gray-900 ps-2.5">Code Verify</label>
|
|
||||||
<label className="input">
|
|
||||||
<input
|
|
||||||
placeholder="Enter code verify"
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
autoComplete="off"
|
|
||||||
maxLength={6}
|
|
||||||
{...formik.getFieldProps('token')}
|
|
||||||
className={clsx('form-control', {
|
|
||||||
'is-invalid': formik.touched.token && formik.errors.token
|
|
||||||
})}
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (!/[0-9]/.test(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{formik.touched.token && formik.errors.token && (
|
|
||||||
<span role="alert" className="text-danger text-xs mt-1">
|
|
||||||
{formik.errors.token}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-1">
|
|
||||||
<label className="checkbox-group">
|
|
||||||
<input
|
|
||||||
className="checkbox checkbox-sm"
|
|
||||||
type="checkbox"
|
|
||||||
{...formik.getFieldProps('remember')}
|
|
||||||
/>
|
|
||||||
<span className="checkbox-label">Remember me</span>
|
|
||||||
</label>
|
|
||||||
{/* <Link
|
|
||||||
to={
|
|
||||||
currentLayout?.name === 'auth-branded'
|
|
||||||
? '/auth/reset-password'
|
|
||||||
: '/auth/classic/reset-password'
|
|
||||||
}
|
|
||||||
className="text-2sm link shrink-0"
|
|
||||||
>
|
|
||||||
Forgot Password?
|
|
||||||
</Link> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary flex justify-center grow"
|
className="btn btn-primary flex justify-center grow"
|
||||||
@ -202,7 +154,7 @@ const Login = () => {
|
|||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
|
<p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
|
||||||
Copyright {moment().year()} © Telkomcel All rights reserved.
|
Copyright {moment().year()} © Timor Telecom All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -14,12 +14,10 @@ import { type AuthModel, type UserModel } from '@/auth';
|
|||||||
import { apiConfig } from '@/config/api.config';
|
import { apiConfig } from '@/config/api.config';
|
||||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
const API_URL = apiConfig.service_user;
|
||||||
|
|
||||||
export const LOGIN_URL = `${API_URL}/login`;
|
export const LOGIN_URL = `${API_URL}/login`;
|
||||||
export const FORGOT_PASSWORD_URL = `${API_URL}/reset_password`;
|
export const GET_USER_URL = `${API_URL}/user/profile`;
|
||||||
export const RESET_PASSWORD_URL = `${API_URL}/update_password`;
|
|
||||||
export const GET_USER_URL = `${API_URL}/user/detail`;
|
|
||||||
|
|
||||||
interface AuthContextProps {
|
interface AuthContextProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -29,9 +27,6 @@ interface AuthContextProps {
|
|||||||
currentUser: UserModel | undefined;
|
currentUser: UserModel | undefined;
|
||||||
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
|
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
|
||||||
login: (email: string, password: string, token?: string) => Promise<void>;
|
login: (email: string, password: string, token?: string) => Promise<void>;
|
||||||
requestPasswordResetLink: (email: string) => Promise<void>;
|
|
||||||
changePassword: (token: string, password: string, password_confirmation: string) => Promise<void>;
|
|
||||||
getUser: () => Promise<AxiosResponse<any> | {}>;
|
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
verify: () => Promise<void>;
|
verify: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -55,15 +50,13 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const login = async (username: string, password: string, token?: string) => {
|
const login = async (username: string, password: string, token?: string) => {
|
||||||
try {
|
try {
|
||||||
const { data: auth } = await axios
|
const { data: auth } = await axios
|
||||||
.post(LOGIN_URL, { username, password, token })
|
.post(LOGIN_URL, { username, password })
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
|
||||||
const enhancedAuth: AuthModel = {
|
const enhancedAuth: AuthModel = {
|
||||||
...auth.token,
|
...auth.token,
|
||||||
id: auth.user.id,
|
id: auth.userid,
|
||||||
role_name: auth.role_name,
|
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
|
|
||||||
};
|
};
|
||||||
|
|
||||||
saveAuth(enhancedAuth);
|
saveAuth(enhancedAuth);
|
||||||
@ -81,20 +74,9 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestPasswordResetLink = async (email: string) => {
|
|
||||||
await axios.put(FORGOT_PASSWORD_URL + '/' + email);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changePassword = async (token: string, password: string, retype_password: string) => {
|
|
||||||
await axios.put(`${RESET_PASSWORD_URL}/${token}`, {
|
|
||||||
password,
|
|
||||||
retype_password
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUser = async () => {
|
const getUser = async () => {
|
||||||
let _axios = await axios
|
let _axios = await axios
|
||||||
.get(`${GET_USER_URL}/${authHelper.getAuth()?.id}`)
|
.get(`${GET_USER_URL}/${authHelper.getAuth()?.user?.id}`)
|
||||||
.then((response) => response.data.data);
|
.then((response) => response.data.data);
|
||||||
|
|
||||||
return { data: _axios };
|
return { data: _axios };
|
||||||
@ -105,18 +87,14 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|||||||
try {
|
try {
|
||||||
const { data: user } = await getUser();
|
const { data: user } = await getUser();
|
||||||
|
|
||||||
// Perbarui auth yang sekarang dengan statusbalance
|
|
||||||
saveAuth({
|
saveAuth({
|
||||||
...auth,
|
...auth,
|
||||||
statusbalance: user.role.status_balance
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createCacheUser = {
|
const createCacheUser = {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
role_name: auth.role_name,
|
|
||||||
statusbalance: user.role.status_balance
|
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(createCacheUser));
|
localStorage.setItem('user', JSON.stringify(createCacheUser));
|
||||||
@ -149,9 +127,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|||||||
currentUser,
|
currentUser,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
login,
|
login,
|
||||||
requestPasswordResetLink,
|
|
||||||
changePassword,
|
|
||||||
getUser,
|
|
||||||
logout,
|
logout,
|
||||||
verify
|
verify
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const LoaderTransparant = () => {
|
|||||||
<img
|
<img
|
||||||
// className="h-[30px] max-w-none"
|
// className="h-[30px] max-w-none"
|
||||||
className="h-14 w-auto drop-shadow-md"
|
className="h-14 w-auto drop-shadow-md"
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const ScreenLoader = () => {
|
|||||||
<img
|
<img
|
||||||
// className="h-[30px] max-w-none"
|
// className="h-[30px] max-w-none"
|
||||||
className="h-14 w-auto drop-shadow-md"
|
className="h-14 w-auto drop-shadow-md"
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||||
|
|||||||
@ -1,36 +1,41 @@
|
|||||||
interface apiConfigProps {
|
interface apiConfigProps {
|
||||||
service_dashboard: string;
|
service_user: string;
|
||||||
service_customer: string;
|
|
||||||
service_master_data: string;
|
service_master_data: string;
|
||||||
// service_master_data2: string;
|
|
||||||
service_transaction: string;
|
|
||||||
service_wallet: string;
|
|
||||||
transaction: string;
|
|
||||||
nationality: string;
|
|
||||||
service_disbursement: string;
|
|
||||||
service_notification: string;
|
|
||||||
service_feedback: string;
|
|
||||||
service_card: string;
|
|
||||||
api_dashboard:string
|
|
||||||
|
|
||||||
|
// service_dashboard: string;
|
||||||
|
// service_customer: string;
|
||||||
|
// // service_master_data2: string;
|
||||||
|
// service_transaction: string;
|
||||||
|
// service_wallet: string;
|
||||||
|
// transaction: string;
|
||||||
|
// nationality: string;
|
||||||
|
// service_disbursement: string;
|
||||||
|
// service_notification: string;
|
||||||
|
// service_feedback: string;
|
||||||
|
// service_card: string;
|
||||||
|
// api_dashboard:string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_API_URL;
|
const API_URL = import.meta.env.VITE_APP_API_URL;
|
||||||
|
const API_URL_MASTER_DATA = import.meta.env.VITE_APP_API_URL_MASTER_DATA;
|
||||||
|
|
||||||
const apiConfig: apiConfigProps = {
|
const apiConfig: apiConfigProps = {
|
||||||
service_feedback: `${API_URL}/k`,
|
service_user: `${API_URL}`,
|
||||||
|
service_master_data: `${API_URL_MASTER_DATA}`,
|
||||||
|
|
||||||
// service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`,
|
// service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`,
|
||||||
service_dashboard: `${API_URL}/d`,
|
// service_feedback: `${API_URL}/k`,
|
||||||
service_customer: `${API_URL}/c`,
|
// service_customer: `${API_URL}/c`,
|
||||||
// service_master_data2: `${API_URL}/m`,
|
// // service_master_data2: `${API_URL}/m`,
|
||||||
service_master_data: `${API_URL}/t`,
|
// // service_master_data: `${API_URL}/t`,
|
||||||
service_transaction: `${API_URL}/tt`,
|
// service_transaction: `${API_URL}/tt`,
|
||||||
service_wallet: `${API_URL}/w`,
|
// service_wallet: `${API_URL}/w`,
|
||||||
transaction: `${API_URL}/x`,
|
// transaction: `${API_URL}/x`,
|
||||||
service_disbursement: `${API_URL}/s`,
|
// service_disbursement: `${API_URL}/s`,
|
||||||
service_notification: `${API_URL}/n`,
|
// service_notification: `${API_URL}/n`,
|
||||||
api_dashboard: `${API_URL}/r`,
|
// api_dashboard: `${API_URL}/r`,
|
||||||
service_card: `${API_URL}/a`,
|
// service_card: `${API_URL}/a`,
|
||||||
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
// nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||||
};
|
};
|
||||||
|
|
||||||
export { apiConfig };
|
export { apiConfig };
|
||||||
|
|||||||
@ -30,19 +30,19 @@ const Layout = () => {
|
|||||||
<div className="flex flex-col p-8 lg:p-16 gap-4">
|
<div className="flex flex-col p-8 lg:p-16 gap-4">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="h-12 max-w-none"
|
className="h-20 max-w-none"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-2xl font-semibold text-gray-900">TPAY Dashboard Portal</h3>
|
<h3 className="text-2xl font-semibold text-gray-900">Revenue Dashboard Portal</h3>
|
||||||
<div className="text-base font-medium text-gray-600">
|
<div className="text-base font-medium text-gray-600">
|
||||||
A user-friendly interface providing seamless access to TPAY’s
|
{/* A user-friendly interface providing seamless access to TPAY’s */}
|
||||||
<span className="text-gray-900 font-semibold">
|
<span className="text-gray-900 font-semibold">
|
||||||
<br />
|
<br />
|
||||||
transaction management, reporting tools, and configuration settings.
|
{/* transaction management, reporting tools, and configuration settings. */}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const Header = () => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center transition-[height] shrink-0 h-[--tw-header-height] bg-[length:600px] bg-no-repeat bg-red-600',
|
'flex items-center transition-[height] shrink-0 h-[--tw-header-height] bg-[length:600px] bg-no-repeat bg-blue-600',
|
||||||
headerSticky &&
|
headerSticky &&
|
||||||
'transition-[height] fixed z-10 top-0 left-0 right-0 shadow-sm backdrop-blur-md bg-white/70 dark:bg-coal-500/70 dark:border-b dark:border-b-coal-100'
|
'transition-[height] fixed z-10 top-0 left-0 right-0 shadow-sm backdrop-blur-md bg-white/70 dark:bg-coal-500/70 dark:border-b dark:border-b-coal-100'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -53,12 +53,12 @@ const HeaderLogo = () => {
|
|||||||
alt="logo"
|
alt="logo"
|
||||||
/> */}
|
/> */}
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="dark:hidden h-14"
|
className="dark:hidden h-14"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="hidden dark:inline-block min-h-[42px]"
|
className="hidden dark:inline-block min-h-[42px]"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
@ -66,7 +66,7 @@ const HeaderLogo = () => {
|
|||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className={`text-xl hidden md:block ${isSticky ? 'text-black' : 'text-gray-50'}`}>
|
<h3 className={`text-xl hidden md:block ${isSticky ? 'text-black' : 'text-gray-50'}`}>
|
||||||
TPAY Dashboard Portal
|
Revenue Dashboard Portal
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,7 +10,6 @@ const HeaderTopbar = () => {
|
|||||||
const itemChatRef = useRef<any>(null);
|
const itemChatRef = useRef<any>(null);
|
||||||
const itemUserRef = useRef<any>(null);
|
const itemUserRef = useRef<any>(null);
|
||||||
const itemNotificationsRef = useRef<any>(null);
|
const itemNotificationsRef = useRef<any>(null);
|
||||||
|
|
||||||
const { isRTL } = useLanguage();
|
const { isRTL } = useLanguage();
|
||||||
|
|
||||||
const [isSticky, setIsSticky] = useState(false);
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
@ -32,10 +31,10 @@ const HeaderTopbar = () => {
|
|||||||
<span
|
<span
|
||||||
className={`font-semibold text-lg leading-tight ${isSticky ? 'text-black' : 'text-white'}`}
|
className={`font-semibold text-lg leading-tight ${isSticky ? 'text-black' : 'text-white'}`}
|
||||||
>
|
>
|
||||||
{getAuth()?.user.name}
|
{getAuth()?.user?.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
|
<span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
|
||||||
as {getAuth()?.role_name}
|
{getAuth()?.user?.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -8,56 +8,74 @@ import {
|
|||||||
MenuSub,
|
MenuSub,
|
||||||
MenuTitle
|
MenuTitle
|
||||||
} from '@/components/menu';
|
} from '@/components/menu';
|
||||||
import { useMenus } from '@/providers';
|
// import { useMenus } from '@/providers';
|
||||||
// import { useLocation } from 'react-router';
|
// import { useLocation } from 'react-router';
|
||||||
import { useLanguage } from '@/i18n';
|
import { useLanguage } from '@/i18n';
|
||||||
import { doGetNavbarMenu } from '@/actions/NavbarMenuActions';
|
import { MappedMenu } from '@/types/GlobalTypes';
|
||||||
import { RoleList, MappedMenu } from '@/types/GlobalTypes';
|
|
||||||
import { useEffect, useState } from 'react';
|
// ---------------------------------------------------------------------------
|
||||||
|
// Static menu structure (previously fetched from the API via
|
||||||
|
// doGetNavbarMenu). Update `path` values to match your actual route
|
||||||
|
// definitions:
|
||||||
|
// - "Dashboard" points to the real dashboard route.
|
||||||
|
// - Every other item points to "/coming-soon", a shared placeholder page
|
||||||
|
// that just renders "This page will be available soon".
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const COMING_SOON_PATH = '/coming-soon';
|
||||||
|
|
||||||
|
const STATIC_MENU: MappedMenu[] = [
|
||||||
|
{
|
||||||
|
title: 'Main Dashboard',
|
||||||
|
path: '-',
|
||||||
|
children: [{ title: 'Dashboard', path: '/' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manage Sites',
|
||||||
|
path: '-',
|
||||||
|
children: [{ title: 'Manage Sites', path: '/sites/manage-sites' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Revenue',
|
||||||
|
path: '-',
|
||||||
|
children: [
|
||||||
|
{ title: 'SMS', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Voice', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Digital Content', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Refill Packet', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Refill Voucher', path: COMING_SOON_PATH }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dealer & Outlet',
|
||||||
|
path: '-',
|
||||||
|
children: [
|
||||||
|
{ title: 'Dealer', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Outlet', path: COMING_SOON_PATH }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'User Management',
|
||||||
|
path: '-',
|
||||||
|
children: [
|
||||||
|
{ title: 'User List', path: '/settings/user-management/manage-user' },
|
||||||
|
{ title: 'Group Role List', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Role List', path: COMING_SOON_PATH }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manage Profile',
|
||||||
|
path: '-',
|
||||||
|
children: [{ title: 'Manage Cell', path: COMING_SOON_PATH }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const NavbarMenu = () => {
|
const NavbarMenu = () => {
|
||||||
// const { pathname } = useLocation();
|
// const { pathname } = useLocation();
|
||||||
// const { getMenuConfig } = useMenus();
|
// const { getMenuConfig } = useMenus();
|
||||||
// const primaryMenu = getMenuConfig('primary');
|
// const primaryMenu = getMenuConfig('primary');
|
||||||
const { isRTL } = useLanguage();
|
const { isRTL } = useLanguage();
|
||||||
const [menuData, setMenuData] = useState<RoleList[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
let navbarMenu;
|
|
||||||
|
|
||||||
useEffect(() => {
|
const navbarMenu: TMenuConfig = STATIC_MENU;
|
||||||
const fetchNavbarMenu = async () => {
|
|
||||||
try {
|
|
||||||
const result = await doGetNavbarMenu();
|
|
||||||
if (result.status && result.data) {
|
|
||||||
setMenuData(result.data.roles_list);
|
|
||||||
} else {
|
|
||||||
setError(result.message);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to fetch navbar menu');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchNavbarMenu();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const mapMenuData = (data: RoleList[]): MappedMenu[] => {
|
|
||||||
return data.map((item) => {
|
|
||||||
const mappedItem: MappedMenu = {
|
|
||||||
title: item.name,
|
|
||||||
path: item.link !== '-' ? item.link : '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
mappedItem.children = mapMenuData(item.children);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedItem;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
navbarMenu = mapMenuData(menuData);
|
|
||||||
// navbarMenu = primaryMenu?.[0].children;
|
|
||||||
|
|
||||||
const buildMenu = (items: TMenuConfig) => {
|
const buildMenu = (items: TMenuConfig) => {
|
||||||
return items.map((item, index) => {
|
return items.map((item, index) => {
|
||||||
@ -149,11 +167,11 @@ const NavbarMenu = () => {
|
|||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div className="scrollable-x-auto">
|
<div className="scrollable-x-auto">
|
||||||
<Menu highlight={true} className="gap-5 lg:gap-7.5">
|
<Menu highlight={true} className="gap-5 lg:gap-7.5">
|
||||||
{navbarMenu && navbarMenu && buildMenu(navbarMenu)}
|
{navbarMenu && buildMenu(navbarMenu)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { NavbarMenu };
|
export { NavbarMenu };
|
||||||
20
src/pages/ComingSoonPage.tsx
Normal file
20
src/pages/ComingSoonPage.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Container } from '@/components';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
const ComingSoonPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>TPAY | Coming Soon</title>
|
||||||
|
</Helmet>
|
||||||
|
<Container>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center" style={{ minHeight: '60vh' }}>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-2">This page will be available soon</h2>
|
||||||
|
<p className="text-sm text-gray-500">We're still working on this feature. Please check back later.</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComingSoonPage;
|
||||||
@ -25,7 +25,7 @@ const AccountUserProfileContent = () => {
|
|||||||
|
|
||||||
{/* Right Column - MFA Setup */}
|
{/* Right Column - MFA Setup */}
|
||||||
<div className="xl:col-span-1">
|
<div className="xl:col-span-1">
|
||||||
<GenerateQr idCustomer={parsedUser.id} />
|
{/* <GenerateQr idCustomer={parsedUser.id} /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccountUserProfileContextProvider>
|
</AccountUserProfileContextProvider>
|
||||||
|
|||||||
@ -13,43 +13,43 @@ interface IBasicSettingsProps {
|
|||||||
const BasicSettings = () => {
|
const BasicSettings = () => {
|
||||||
// const user = localStorage.getItem('user');
|
// const user = localStorage.getItem('user');
|
||||||
// const parsedUser = user ? JSON.parse(user) : null;
|
// const parsedUser = user ? JSON.parse(user) : null;
|
||||||
const parsedUser = getAuth()?.user;
|
// const parsedUser = getAuth()?.user;
|
||||||
// console.log('parsedUser :', parsedUser);
|
// console.log('parsedUser :', parsedUser);
|
||||||
const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
// const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
||||||
const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
// const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
||||||
const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
// const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
||||||
|
|
||||||
const { setProfile } = useContext(AccountUserProfileContext);
|
const { setProfile } = useContext(AccountUserProfileContext);
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const isChanged =
|
// const isChanged =
|
||||||
newUsername !== parsedUser?.username ||
|
// newUsername !== parsedUser?.username ||
|
||||||
newEmail !== parsedUser?.email ||
|
// newEmail !== parsedUser?.email ||
|
||||||
newName !== parsedUser?.name;
|
// newName !== parsedUser?.name;
|
||||||
|
|
||||||
const handleChangeProfile = async () => {
|
// const handleChangeProfile = async () => {
|
||||||
setIsSubmitting(true);
|
// setIsSubmitting(true);
|
||||||
try {
|
// try {
|
||||||
await setProfile({ name: newName, username: newUsername, email: newEmail });
|
// // await setProfile({ name: newName, username: newUsername, email: newEmail });
|
||||||
|
|
||||||
const newCache = {
|
// const newCache = {
|
||||||
name: newName,
|
// name: newName,
|
||||||
email: newEmail,
|
// email: newEmail,
|
||||||
username: newUsername
|
// username: newUsername
|
||||||
};
|
// };
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(newCache));
|
// localStorage.setItem('user', JSON.stringify(newCache));
|
||||||
} catch (error: any) {
|
// } catch (error: any) {
|
||||||
const errorMessage =
|
// const errorMessage =
|
||||||
error?.response?.data?.message ||
|
// error?.response?.data?.message ||
|
||||||
error?.message ||
|
// error?.message ||
|
||||||
'An error occurred while resetting the password.';
|
// 'An error occurred while resetting the password.';
|
||||||
toast.error(errorMessage);
|
// toast.error(errorMessage);
|
||||||
} finally {
|
// } finally {
|
||||||
setIsSubmitting(false);
|
// setIsSubmitting(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card pb-2.5">
|
<div className="card pb-2.5">
|
||||||
@ -65,7 +65,7 @@ const BasicSettings = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder='Name'
|
placeholder='Name'
|
||||||
// value={newName}
|
// value={newName}
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
// onChange={(e) => setNewName(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +76,7 @@ const BasicSettings = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder='Username'
|
placeholder='Username'
|
||||||
// value={newUsername}
|
// value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
// onChange={(e) => setNewUsername(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@ const BasicSettings = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder='Email'
|
placeholder='Email'
|
||||||
// value={newEmail}
|
// value={newEmail}
|
||||||
onChange={(e) => setNewEmail(e.target.value)}
|
// onChange={(e) => setNewEmail(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -99,8 +99,8 @@ const BasicSettings = () => {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleChangeProfile}
|
// onClick={handleChangeProfile}
|
||||||
disabled={isSubmitting || !isChanged}
|
// disabled={isSubmitting || !isChanged}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'loading...' : 'Save Change'}
|
{isSubmitting ? 'loading...' : 'Save Change'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,408 +1,20 @@
|
|||||||
import { Container, KeenIcon, DefaultTooltip } from '@/components';
|
import { Container } from '@/components';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { Card, Chart, YearPicker } from './blocks';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { useFetchCardData, useFetchChartData } from './hooks';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useFetchYear } from './hooks/useFetchYear';
|
|
||||||
import { get5LastYear } from '@/utils/Date';
|
|
||||||
import { staticChartData } from './staticChart';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import BalanceCard from './blocks/BalanceCard';
|
import SitesSatelliteMap from './blocks/SitesateliteMap';
|
||||||
import { getAuth } from '@/auth';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import TransactionValue from './blocks/TransactionValue';
|
|
||||||
import TransactionPieChart from './blocks/TransactionPieChart';
|
|
||||||
import MemberActivity from './blocks/MemberActivity';
|
|
||||||
import BankSaldo from './blocks/BankSaldo';
|
|
||||||
|
|
||||||
// sum -> nominal, count-> total
|
|
||||||
type CountType = 'sum' | 'count';
|
|
||||||
type ChartType = 'line' | 'bar';
|
|
||||||
type ChartLegend = 'true' | 'false';
|
|
||||||
|
|
||||||
const DashboardHomePage = () => {
|
const DashboardHomePage = () => {
|
||||||
const selectYear = get5LastYear();
|
|
||||||
const [initialYear, setInitialYear] = useState<string>('');
|
|
||||||
const [selectedYear, setSelectedYear] = useState<string>('');
|
|
||||||
const [count, setCount] = useState<CountType>('sum');
|
|
||||||
const [chartType, setChartType] = useState<ChartType>('line');
|
|
||||||
const [chartLegend, setChartLegend] = useState<ChartLegend>('true');
|
|
||||||
const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
|
|
||||||
from: new Date(),
|
|
||||||
to: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
const getFirstDayOfMonth = () => {
|
|
||||||
const now = new Date();
|
|
||||||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getToday = () => {
|
|
||||||
return new Date();
|
|
||||||
};
|
|
||||||
|
|
||||||
const [fromDate, setFromDate] = useState(getFirstDayOfMonth());
|
|
||||||
const [toDate, setToDate] = useState(getToday());
|
|
||||||
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
|
|
||||||
const API_URL_BANK = apiConfig.service_wallet;
|
|
||||||
|
|
||||||
const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
const res = await GetData(`${API_URL}/card-statistic`, {});
|
|
||||||
setResponseStatisticCard(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [bankaccount, setbankaccount] = useState<any>(null);
|
|
||||||
|
|
||||||
const fetchDataBankAccount = async () => {
|
|
||||||
const res = await GetData(`${API_URL_BANK}/dashboard/balance/account/${getAuth()?.user?.customer?.id}`, {});
|
|
||||||
setbankaccount(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDataBankAccount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [responseGraphic, setresponseGraphic] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataGraphic = async () => {
|
|
||||||
const res = await GetData(`${API_URL}/cashin-vs-cashout`, {
|
|
||||||
date_from: fromDate.toISOString(),
|
|
||||||
date_to: toDate.toISOString()
|
|
||||||
});
|
|
||||||
setresponseGraphic(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataGraphic();
|
|
||||||
}, [fromDate, toDate]);
|
|
||||||
|
|
||||||
let staticChartDataApiFetch = [];
|
|
||||||
|
|
||||||
// Make map for date => { cashin: 0, cashout: 0 }
|
|
||||||
const dataMap: Record<string, { cashin: number; cashout: number }> = {};
|
|
||||||
const current = moment(fromDate);
|
|
||||||
const end = moment(toDate);
|
|
||||||
|
|
||||||
// Inisialization dataMap with all dates
|
|
||||||
while (current.isSameOrBefore(end, 'day')) {
|
|
||||||
const dateStr = current.format('DD-MM-YYYY');
|
|
||||||
dataMap[dateStr] = { cashin: 0, cashout: 0 };
|
|
||||||
current.add(1, 'day');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cashin
|
|
||||||
if (Array.isArray(responseGraphic?.data.total_cashin)) {
|
|
||||||
responseGraphic.data.total_cashin.forEach((item: any) => {
|
|
||||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
|
||||||
// Only update cashin if it's not already set (meaning no value was added before)
|
|
||||||
if (dataMap[date]) {
|
|
||||||
dataMap[date].cashin = Number(item.total_count ?? 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cashout
|
|
||||||
if (Array.isArray(responseGraphic?.data.total_cashout)) {
|
|
||||||
responseGraphic.data.total_cashout.forEach((item: any) => {
|
|
||||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
|
||||||
// Only update cashout if it's not already set (meaning no value was added before)
|
|
||||||
if (dataMap[date]) {
|
|
||||||
dataMap[date].cashout = Number(item.total_count ?? 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change to array for chart
|
|
||||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
|
||||||
month,
|
|
||||||
cashin: values.cashin,
|
|
||||||
cashout: values.cashout
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Change to array for chart
|
|
||||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
|
||||||
month,
|
|
||||||
cashin: values.cashin,
|
|
||||||
cashout: values.cashout
|
|
||||||
}));
|
|
||||||
|
|
||||||
const currentRole = getAuth()?.role_name;
|
|
||||||
const idCustomer = getAuth()?.user.customer?.id;
|
|
||||||
// console.log(currentRole);
|
|
||||||
|
|
||||||
// Menyusun tanggal awal dan akhir berdasarkan selectedYear
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectYear.length > 0 && !selectedYear) {
|
|
||||||
let latestYear = Math.max(...selectYear.map((item) => parseInt(item, 10))).toString();
|
|
||||||
// console.log('latestYear :', latestYear);
|
|
||||||
setSelectedYear(latestYear);
|
|
||||||
setInitialYear(latestYear);
|
|
||||||
}
|
|
||||||
}, [selectYear, selectedYear]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedYear) {
|
|
||||||
setDateRange({
|
|
||||||
from: moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
|
||||||
to: moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedYear]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialYear) {
|
|
||||||
setDateRange({
|
|
||||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
|
||||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [initialYear]);
|
|
||||||
|
|
||||||
const handleYearChange = (year: string) => {
|
|
||||||
setSelectedYear(year);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCountType = (value: CountType) => {
|
|
||||||
setCount(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChartType = (value: ChartType) => {
|
|
||||||
setChartType(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChartLegend = (value: ChartLegend) => {
|
|
||||||
setChartLegend(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilter = useCallback(() => {
|
|
||||||
const firstDay = getFirstDayOfMonth();
|
|
||||||
const today = getToday();
|
|
||||||
|
|
||||||
setFromDate(firstDay);
|
|
||||||
setToDate(today);
|
|
||||||
|
|
||||||
// reset filter yang lain kalau perlu
|
|
||||||
setDateRange({ from: firstDay, to: today });
|
|
||||||
setSelectedYear(initialYear);
|
|
||||||
setCount('sum');
|
|
||||||
setChartType('line');
|
|
||||||
setChartLegend('true');
|
|
||||||
}, [initialYear]);
|
|
||||||
|
|
||||||
const { cardData } = useFetchCardData(
|
|
||||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
|
||||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
|
||||||
count
|
|
||||||
);
|
|
||||||
|
|
||||||
const { chartData } = useFetchChartData(
|
|
||||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
|
||||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
|
||||||
count
|
|
||||||
);
|
|
||||||
|
|
||||||
const toolbar = (
|
|
||||||
<div className="flex gap-3 items-center w-1/2">
|
|
||||||
<div className="w-auto min-w-[120px]">
|
|
||||||
<Select value={chartType} onValueChange={handleChartType}>
|
|
||||||
<SelectTrigger size="sm">
|
|
||||||
<SelectValue placeholder="Select Chart Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="w-32">
|
|
||||||
<SelectItem value="line">Line</SelectItem>
|
|
||||||
<SelectItem value="bar">Bar</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="w-auto min-w-[120px]">
|
|
||||||
<Select value={chartLegend} onValueChange={handleChartLegend}>
|
|
||||||
<SelectTrigger size="sm">
|
|
||||||
<SelectValue placeholder="Select Legend Visibility" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="w-full">
|
|
||||||
<SelectItem value="true">Show Legend</SelectItem>
|
|
||||||
<SelectItem value="false">Hide Legend</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const number: number = responseStatisticCard?.data.total_cash_in ?? 0;
|
|
||||||
const formattedNumber: number = parseFloat(number.toFixed(2));
|
|
||||||
|
|
||||||
const numbercashout: number = responseStatisticCard?.data.total_cash_out ?? 0;
|
|
||||||
const formattedNumbercashout: number = parseFloat(numbercashout.toFixed(2));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>TPAY | Dashboard</title>
|
<title>REVENUE | Dashboard</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Container>
|
<Container>
|
||||||
{/* Account Balance Cards */}
|
<div className="mt-5">
|
||||||
{/* {currentRole === 'Escrow' || currentRole === 'Master Agent' ? (
|
<SitesSatelliteMap />
|
||||||
<div className="grid gap-5 lg:gap-7.5 mb-14 mt-7">
|
|
||||||
<BalanceCard id={idCustomer} />
|
|
||||||
</div>
|
|
||||||
) : null} */}
|
|
||||||
|
|
||||||
<div className="flex space-x-4 mt-5">
|
|
||||||
{bankaccount?.data && bankaccount?.data.length > 0
|
|
||||||
&& getAuth()?.statusbalance=='Y'
|
|
||||||
? (
|
|
||||||
bankaccount?.data.map((bankaccountdatas: { id_balance:string,amount: string, credit_limit: string, monthly_limit: string; wallet: string; }, index: number) => (
|
|
||||||
<BankSaldo
|
|
||||||
title={bankaccountdatas.wallet}
|
|
||||||
balance={bankaccountdatas.amount}
|
|
||||||
creditLimit={bankaccountdatas.credit_limit}
|
|
||||||
monthlyLimit={bankaccountdatas.monthly_limit}
|
|
||||||
idbalance={bankaccountdatas.id_balance}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cards */}
|
|
||||||
<div className="flex gap-6 overflow-x-auto pb-2 mt-5">
|
|
||||||
<Card
|
|
||||||
title="Registered Users"
|
|
||||||
total={responseStatisticCard?.data.total_registered ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.registered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.registered_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Unregistered Users"
|
|
||||||
total={responseStatisticCard?.data.total_unregistered ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.unregistered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.unregistered_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Cash-in"
|
|
||||||
total={formattedNumber.toFixed(2)} // now a number: 972.80
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.cash_in_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.cash_in_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Cash-out"
|
|
||||||
total={formattedNumbercashout.toFixed(2)} // now a number: 972.80
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.cash_out_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.cash_out_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Active Event"
|
|
||||||
total={responseStatisticCard?.data.total_event ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.event_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.event_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Billing"
|
|
||||||
total={responseStatisticCard?.data.total_billing ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.billing_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.billing_last_week.surplus ?? false}
|
|
||||||
icon='test'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 items-center mb-6 mt-6">
|
|
||||||
<div className="flex gap-3 items-center w-full md:w-auto">
|
|
||||||
<label className="input input-sm w-[160px]">
|
|
||||||
From
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="from"
|
|
||||||
value={moment(fromDate).format('YYYY-MM-DD')}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
setFromDate(new Date(e.target.value));
|
|
||||||
} else {
|
|
||||||
// Reset to default value (e.g. first day of current month)
|
|
||||||
setFromDate(getFirstDayOfMonth());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="input input-sm w-[160px]">
|
|
||||||
To
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="to"
|
|
||||||
value={moment(toDate).format('YYYY-MM-DD')}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
setToDate(new Date(e.target.value));
|
|
||||||
} else {
|
|
||||||
// Reset to default value (e.g. today)
|
|
||||||
setToDate(getToday());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DefaultTooltip title="Reset Filter" placement="top">
|
|
||||||
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
|
||||||
<KeenIcon icon="arrow-circle-left" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Chart */}
|
|
||||||
<div className="grid gap-5 lg:gap-7.5 mt-5">
|
|
||||||
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<Chart
|
|
||||||
title="Overview"
|
|
||||||
count={12}
|
|
||||||
toolbar={toolbar}
|
|
||||||
chartData={staticChartDataApiFetch}
|
|
||||||
chartType={chartType}
|
|
||||||
chartLegend={chartLegend}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-4 mt-5">
|
|
||||||
<TransactionValue startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
<TransactionPieChart startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
<MemberActivity startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardHomePage;
|
export default DashboardHomePage;
|
||||||
@ -1,100 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { CreditCard, Wallet } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
type WalletResponse = {
|
|
||||||
amount: number;
|
|
||||||
id_wallet: string;
|
|
||||||
wallet: string;
|
|
||||||
credit_limit: number;
|
|
||||||
monthly_limit: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BalanceCardProps {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL_WALLET = apiConfig.service_wallet;
|
|
||||||
|
|
||||||
const BalanceCard = ({ id }: BalanceCardProps) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [wallets, setWallets] = useState<WalletResponse[]>([]);
|
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD'
|
|
||||||
}).format(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumberWithDots = (value: number) => {
|
|
||||||
return value.toLocaleString('id-ID');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColorClass = (amount: number, walletType: string) => {
|
|
||||||
if (walletType.toLowerCase().includes('point account')) return 'bg-amber-300';
|
|
||||||
// if (amount > 3125) return 'bg-amber-300';
|
|
||||||
return 'bg-red-500';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchAccountBalances() {
|
|
||||||
try {
|
|
||||||
const response = await GetData(`${API_URL_WALLET}/dashboard/balance/account/${id}`, {
|
|
||||||
id: id
|
|
||||||
});
|
|
||||||
if (response?.status === true) {
|
|
||||||
setWallets(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching account balances', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountBalances();
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid lg:grid-cols-4 md:grid-cols-2 gap-5 items-stretch">
|
|
||||||
{wallets.map((wallet) => {
|
|
||||||
const colorClass = getColorClass(wallet.amount, wallet.wallet);
|
|
||||||
const isNegative = wallet.amount < 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={wallet.id_wallet}
|
|
||||||
className="rounded-lg overflow-hidden shadow-lg border border-gray-200 hover:shadow-xl transition-shadow duration-300"
|
|
||||||
>
|
|
||||||
<div className={`${colorClass} h-2`} />
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="font-bold text-lg text-gray-800">{wallet.wallet}</h3>
|
|
||||||
{wallet.wallet.includes('Credit') ? (
|
|
||||||
<CreditCard className="text-gray-600" size={20} />
|
|
||||||
) : (
|
|
||||||
<Wallet className="text-gray-600" size={20} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className={`text-xl font-bold ${isNegative ? 'text-red-600' : 'text-gray-800'}`}>
|
|
||||||
{formatCurrency(wallet.amount)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200 flex items-center justify-between">
|
|
||||||
<div className="text-xs text-gray-500">Credit Limit:</div>
|
|
||||||
<div className="text-sm">{formatNumberWithDots(wallet.credit_limit)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex items-center justify-between">
|
|
||||||
<div className="text-xs text-gray-500">Monthly Limit:</div>
|
|
||||||
<div className="text-sm">{formatNumberWithDots(wallet.monthly_limit)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BalanceCard;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Wallet } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import ShowDetailWalletDialog from './ShowDetailWalletDialog';
|
|
||||||
|
|
||||||
interface AccountCardProps {
|
|
||||||
title: string;
|
|
||||||
balance: string;
|
|
||||||
creditLimit: string;
|
|
||||||
monthlyLimit: string;
|
|
||||||
idbalance: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BankSaldo: React.FC<AccountCardProps> = ({
|
|
||||||
title,
|
|
||||||
balance,
|
|
||||||
creditLimit,
|
|
||||||
monthlyLimit,
|
|
||||||
idbalance,
|
|
||||||
}) => {
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full max-w-xs bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
|
|
||||||
<div className="h-1 bg-red-500" />
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h2 className="text-lg font-semibold">{title}</h2>
|
|
||||||
<Wallet className="h-5 w-5 text-gray-500" />
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-gray-800">
|
|
||||||
{new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD',
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(parseFloat(balance))}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Credit Limit:</span>
|
|
||||||
<span>{creditLimit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Monthly Limit:</span>
|
|
||||||
<span>{monthlyLimit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-7.5"
|
|
||||||
onClick={() => setShowDialog(true)}
|
|
||||||
>
|
|
||||||
Detail Wallet
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showDialog && (
|
|
||||||
<ShowDetailWalletDialog
|
|
||||||
open={showDialog}
|
|
||||||
onClose={() => setShowDialog(false)}
|
|
||||||
idbalance={idbalance}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default BankSaldo;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { useLanguage } from '@/i18n';
|
|
||||||
import { fCurrency } from '@/utils/FormatNumber';
|
|
||||||
import { toAbsoluteUrl } from '@/utils/Assets';
|
|
||||||
|
|
||||||
interface CardDataProduct {
|
|
||||||
title: string;
|
|
||||||
total: string;
|
|
||||||
growth: number;
|
|
||||||
surplus: boolean;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Card = ({ title, total, growth, surplus, icon }: CardDataProduct) => {
|
|
||||||
const { isRTL } = useLanguage();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card h-full w-full bg-[length:85%] [background-position:9rem_-4rem] rtl:[background-position:-4rem_-4rem] bg-no-repeat channel-stats-bg">
|
|
||||||
<div className="card-body flex flex-col gap-4 p-5 lg:p-b-7.5 lg:pt-4">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<span className="text-sm font-normal text-gray-600 mb-5">{title}</span>
|
|
||||||
<span className="text-3xl font-semibold text-gray-900 mb-0">{total}</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold mt-2 flex items-center gap-1 ${growth === 0 ? 'text-primary' : surplus ? 'text-green-500' : 'text-red-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{growth === 0 ? '−' : surplus ? '▲' : '▼'} {growth} % From Last Week
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Card };
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import ApexChart from 'react-apexcharts';
|
|
||||||
import { ApexOptions } from 'apexcharts';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { fCurrency } from '@/utils/FormatNumber';
|
|
||||||
|
|
||||||
interface Series {
|
|
||||||
name: string;
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chart = ({
|
|
||||||
title,
|
|
||||||
toolbar,
|
|
||||||
chartType,
|
|
||||||
chartLegend,
|
|
||||||
subtitle,
|
|
||||||
number,
|
|
||||||
count,
|
|
||||||
chartData = []
|
|
||||||
}: any) => {
|
|
||||||
const [series, setSeries] = useState<Series[]>([]);
|
|
||||||
const [categories, setCategories] = useState<string[]>([]);
|
|
||||||
|
|
||||||
let legendOpt = null;
|
|
||||||
if (chartLegend == 'true') {
|
|
||||||
legendOpt = true;
|
|
||||||
} else {
|
|
||||||
legendOpt = false;
|
|
||||||
}
|
|
||||||
const options: ApexOptions = {
|
|
||||||
chart: {
|
|
||||||
type: 'area',
|
|
||||||
toolbar: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: '30%'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: true,
|
|
||||||
offsetY: -10,
|
|
||||||
offsetX: chartType === 'bar' ? 1.5 : 0,
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
markers: {
|
|
||||||
size: 0,
|
|
||||||
shape: 'circle'
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: categories,
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
colors: 'var(--tw-gray-500)',
|
|
||||||
fontSize: '12px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
colors: 'var(--tw-gray-500)',
|
|
||||||
fontSize: '12px'
|
|
||||||
},
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
borderColor: 'var(--tw-gray-200)',
|
|
||||||
strokeDashArray: 5,
|
|
||||||
padding: {
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 20,
|
|
||||||
left: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
shared: true,
|
|
||||||
intersect: false,
|
|
||||||
y: {
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
curve: 'smooth',
|
|
||||||
lineCap: 'butt',
|
|
||||||
colors: undefined,
|
|
||||||
width: 3,
|
|
||||||
dashArray: 0
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: legendOpt,
|
|
||||||
position: 'right',
|
|
||||||
floating: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (chartData && chartData.length !== 0) {
|
|
||||||
const categories = chartData.map((item: any) => item.month);
|
|
||||||
const statusNames = Object.keys(chartData[0]).filter((key) => key !== 'month');
|
|
||||||
const series = statusNames.map((status) => {
|
|
||||||
const cleanName = status
|
|
||||||
.replace('_count', '')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/(^|\s)\S/g, (match) => match.toUpperCase());
|
|
||||||
return {
|
|
||||||
name: cleanName,
|
|
||||||
data: chartData.map((item: any) => item[status])
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setCategories(categories);
|
|
||||||
setSeries(series);
|
|
||||||
}
|
|
||||||
}, [chartData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card h-full">
|
|
||||||
<div className="card-header border-0 ps-5 pb-0">
|
|
||||||
<h3 className="card-title">{title}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body flex flex-col gap-4 p-2 lg:p-b7.5 lg:pt-4">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="ps-3">{toolbar && <div>{toolbar}</div>}</div>
|
|
||||||
<span className="text-sm font-normal text-gray-700 mb-1">{subtitle}</span>
|
|
||||||
<span className="text-3xl font-semibold text-gray-900 mb-0">{number}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ApexChart
|
|
||||||
id="earnings_chart" //
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type={chartType}
|
|
||||||
legend={chartLegend}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Chart };
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { KeenIcon } from '@/components/keenicons';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
interface DateRangePickerProps {
|
|
||||||
date: DateRange | undefined;
|
|
||||||
setDate: (date: DateRange | undefined) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DateRangePicker = ({ date, setDate }: DateRangePickerProps) => {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<button
|
|
||||||
id="date"
|
|
||||||
className={cn(
|
|
||||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
|
||||||
!date && 'text-gray-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<KeenIcon icon="calendar" className="me-0.5" />
|
|
||||||
{date?.from ? (
|
|
||||||
date.to ? (
|
|
||||||
<>
|
|
||||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
format(date.from, 'LLL dd, y')
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>Pick a date range</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="end">
|
|
||||||
<Calendar
|
|
||||||
initialFocus
|
|
||||||
mode="range"
|
|
||||||
defaultMonth={date?.from}
|
|
||||||
selected={date}
|
|
||||||
onSelect={setDate}
|
|
||||||
numberOfMonths={2}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DateRangePicker };
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { UsersIcon, UserCheckIcon } from "lucide-react";
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const polarToCartesian = (cx: number, cy: number, radius: number, angleInDegrees: number) => {
|
|
||||||
const angleInRadians = (angleInDegrees * Math.PI) / 180.0;
|
|
||||||
return {
|
|
||||||
x: cx + radius * Math.cos(angleInRadians),
|
|
||||||
y: cy + radius * Math.sin(angleInRadians),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const describeArc = (
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
radius: number,
|
|
||||||
startAngle: number,
|
|
||||||
endAngle: number
|
|
||||||
) => {
|
|
||||||
const start = polarToCartesian(x, y, radius, endAngle);
|
|
||||||
const end = polarToCartesian(x, y, radius, startAngle);
|
|
||||||
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
|
|
||||||
|
|
||||||
return [
|
|
||||||
"M", start.x, start.y,
|
|
||||||
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y,
|
|
||||||
].join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemberActivity = ({ startdate, enddate }: Props) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/active-user`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
const percentage = parseFloat((responseTransactionValue?.data?.total_customer_active_percentage ?? 0).toFixed(2));
|
|
||||||
const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0;
|
|
||||||
const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0;
|
|
||||||
const reguler = responseTransactionValue?.data?.reguler ?? 0;
|
|
||||||
const premium = responseTransactionValue?.data?.premium ?? 0;
|
|
||||||
const agent = responseTransactionValue?.data?.agent ?? 0;
|
|
||||||
|
|
||||||
// Calculate pointer angle
|
|
||||||
const angle = (percentage / 100) * 180;
|
|
||||||
const pointerX = 50 + 25 * Math.cos((angle - 180) * Math.PI / 180);
|
|
||||||
const pointerY = 50 + 25 * Math.sin((angle - 180) * Math.PI / 180);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-white rounded-lg shadow-md w-full max-w-md">
|
|
||||||
<div className="pb-3 mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700">Member Activity</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-6">
|
|
||||||
{/* Sidebar Info */}
|
|
||||||
<ul className="space-y-4 w-1/2 text-gray-700 text-sm">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UsersIcon className="w-4 h-4" />
|
|
||||||
<span>Total Customer: {totalCustomer}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" />
|
|
||||||
<span>Active Customer: {activeCustomer}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='blue' />
|
|
||||||
<span>Reguler: {reguler}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='green' />
|
|
||||||
<span>Premium: {premium}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='orange' />
|
|
||||||
<span>Agent: {agent}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{/* Gauge Chart */}
|
|
||||||
<div className="w-1/2">
|
|
||||||
<div className="border border-gray-200 rounded-md p-4 text-center">
|
|
||||||
<h3 className="text-sm text-gray-600 font-medium mb-2">Active User</h3>
|
|
||||||
<div className="relative h-24 w-full">
|
|
||||||
<svg viewBox="0 0 100 50" className="w-full h-full">
|
|
||||||
{/* Background arc */}
|
|
||||||
<path
|
|
||||||
d="M 10 50 A 40 40 0 0 1 90 50"
|
|
||||||
fill="none"
|
|
||||||
stroke="#e5e7eb"
|
|
||||||
strokeWidth="10"
|
|
||||||
/>
|
|
||||||
{/* Active arc */}
|
|
||||||
{percentage > 0 && (
|
|
||||||
<path
|
|
||||||
d={describeArc(50, 50, 40, 180, 180 + (percentage * 180 / 100))}
|
|
||||||
fill="none"
|
|
||||||
stroke="#34d399"
|
|
||||||
strokeWidth="10"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* Pointer */}
|
|
||||||
<line
|
|
||||||
x1="50"
|
|
||||||
y1="50"
|
|
||||||
x2={pointerX}
|
|
||||||
y2={pointerY}
|
|
||||||
stroke="#111827"
|
|
||||||
strokeWidth="4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="flex justify-between text-xs text-gray-500 px-1 mt-1">
|
|
||||||
<span>{percentage}%</span>
|
|
||||||
<span>100%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemberActivity;
|
|
||||||
@ -1,489 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogBody,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { DataGridColumnHeader, DataGridProvider } from '@/components';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { DefaultTooltip, KeenIcon } from '@/components';
|
|
||||||
import { toAbsoluteUrl } from '@/utils';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
import ExcelJS from 'exceljs';
|
|
||||||
import { exportToExcel } from '@/pages/wallet/wallet-statement/details_wallet_statement/ExportData';
|
|
||||||
|
|
||||||
interface ShowDetailWalletDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
idbalance: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransferType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date: Date) => date.toLocaleDateString('sv-SE');
|
|
||||||
const getDefaultDateRange = () => {
|
|
||||||
const today = new Date();
|
|
||||||
const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
return {
|
|
||||||
from: formatDate(sevenDaysAgo),
|
|
||||||
to: formatDate(today)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShowDetailWalletDialog: React.FC<ShowDetailWalletDialogProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
idbalance
|
|
||||||
}) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState(getDefaultDateRange());
|
|
||||||
const [selectedTransferType, setSelectedTransferType] = useState<string | null>(null);
|
|
||||||
const [transferType, setTransferType] = useState<TransferType[]>([]);
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [category, setCategory] = useState<string[]>([]);
|
|
||||||
const [transaction, setTransaction] = useState<any[]>([]);
|
|
||||||
const [filters, setFilters] = useState<any>({});
|
|
||||||
|
|
||||||
const fetchTransferType = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
|
|
||||||
limit: 100,
|
|
||||||
page: 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'ASC'
|
|
||||||
});
|
|
||||||
setTransferType(response?.data?.list || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transfer types', error);
|
|
||||||
}
|
|
||||||
}, [GetData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTransferType();
|
|
||||||
}, [fetchTransferType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const categoryMap: Record<string, string> = {
|
|
||||||
T: 'TRANSFER',
|
|
||||||
P: 'PURCHASE',
|
|
||||||
W: 'WITHDRAW',
|
|
||||||
U: 'TOP UP',
|
|
||||||
R: 'RETURN',
|
|
||||||
N: 'TOP UP PARTNER',
|
|
||||||
E: 'REWARD',
|
|
||||||
L: 'PURCHASE LOJA',
|
|
||||||
B: 'TOP UP P24',
|
|
||||||
A: 'TRANSFER AGENT',
|
|
||||||
M: 'WITHDRAWAL AGENT',
|
|
||||||
O: 'TOP UP AGENT',
|
|
||||||
S: 'TRANSFER P24',
|
|
||||||
I: 'WIJTDRAW MERCHANT',
|
|
||||||
D: 'DONATION',
|
|
||||||
F: 'FEE',
|
|
||||||
V: 'REVERSAL',
|
|
||||||
C: 'CASHBACK CASH',
|
|
||||||
H: 'CASHBACK POINT'
|
|
||||||
};
|
|
||||||
|
|
||||||
const uniqueCategories = Array.from(
|
|
||||||
new Set(
|
|
||||||
transaction
|
|
||||||
.map((tx) => categoryMap[tx.category] || '_') // map code to label or default to '_'
|
|
||||||
.filter((cat) => !!cat && cat !== '_') // remove undefined/null or underscore
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
setCategory(uniqueCategories);
|
|
||||||
}, [transaction]);
|
|
||||||
|
|
||||||
const handleTransferTypeChange = (value: string) => {
|
|
||||||
setSelectedTransferType(value === 'all' ? null : value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCategoryChange = (value: string) => {
|
|
||||||
setSelectedCategory(value === '' ? null : value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAllFilters = () => {
|
|
||||||
setDateRange(getDefaultDateRange());
|
|
||||||
setSelectedTransferType(null);
|
|
||||||
setSelectedCategory(null);
|
|
||||||
setSearchValue('');
|
|
||||||
setFilters({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyFilters = () => {
|
|
||||||
const appliedFilters: any = {};
|
|
||||||
|
|
||||||
if (dateRange.from && dateRange.to) {
|
|
||||||
const fromDate = new Date(`${dateRange.from}T00:00:00Z`);
|
|
||||||
const toDate = new Date(`${dateRange.to}T23:59:59Z`);
|
|
||||||
appliedFilters.date = {
|
|
||||||
from: fromDate.toISOString(),
|
|
||||||
to: toDate.toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTransferType) {
|
|
||||||
appliedFilters.transaction_type_id = selectedTransferType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCategory) {
|
|
||||||
appliedFilters.category = selectedCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchValue) {
|
|
||||||
appliedFilters.transaction_code = searchValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilters(appliedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
accessorKey: 'transaction_code',
|
|
||||||
header: ({ column }: any) => (
|
|
||||||
<DataGridColumnHeader title="Transaction Code" column={column} />
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[120px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'msisdn_reff',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="MSISDN Reffer" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[120px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transaction_type.name',
|
|
||||||
header: ({ column }: any) => (
|
|
||||||
<DataGridColumnHeader title="Transaction Type" column={column} />
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[180px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'type',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Type" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[80px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'pre_amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Pre Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'post_amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Post Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row: any) => {
|
|
||||||
switch (row.category) {
|
|
||||||
case 'T':
|
|
||||||
return 'TRANSFER';
|
|
||||||
case 'P':
|
|
||||||
return 'PURCHASE';
|
|
||||||
case 'W':
|
|
||||||
return 'WITHDRAW';
|
|
||||||
case 'U':
|
|
||||||
return 'TOP UP';
|
|
||||||
case 'R':
|
|
||||||
return 'RETURN';
|
|
||||||
case 'N':
|
|
||||||
return 'TOP UP PARTNER';
|
|
||||||
case 'E':
|
|
||||||
return 'REWARD';
|
|
||||||
case 'L':
|
|
||||||
return 'PURCHASE LOJA';
|
|
||||||
case 'B':
|
|
||||||
return 'TOP UP P24';
|
|
||||||
case 'A':
|
|
||||||
return 'TRANSFER AGENT';
|
|
||||||
case 'M':
|
|
||||||
return 'WITHDRAWAL AGENT';
|
|
||||||
case 'O':
|
|
||||||
return 'TOP UP AGENT';
|
|
||||||
case 'S':
|
|
||||||
return 'TRANSFER P24';
|
|
||||||
case 'I':
|
|
||||||
return 'WIJTDRAW MERCHANT';
|
|
||||||
case 'D':
|
|
||||||
return 'DONATION';
|
|
||||||
case 'F':
|
|
||||||
return 'FEE';
|
|
||||||
case 'V':
|
|
||||||
return 'REVERSAL';
|
|
||||||
case 'C':
|
|
||||||
return 'CASHBACK CASH';
|
|
||||||
case 'H':
|
|
||||||
return 'CASHBACK POINT';
|
|
||||||
default:
|
|
||||||
return '_';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessorKey: 'category',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Category" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
meta: {
|
|
||||||
headerClassName: 'w-[250px]'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'date',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Date" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => {
|
|
||||||
const val = info.getValue();
|
|
||||||
if (!val) return '-';
|
|
||||||
return new Date(val).toLocaleString('id-ID', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
meta: { headerClassName: 'min-w-[150px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'purpose',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Purpose" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue() || '-',
|
|
||||||
meta: { headerClassName: 'min-w-[200px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'notes',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Notes" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue() || '-',
|
|
||||||
meta: { headerClassName: 'min-w-[200px]' }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const getTransactionLists = useCallback(
|
|
||||||
async (page: number, limit: number, sorting: any, _columnFilters: any) => {
|
|
||||||
// console.log(idbalance);
|
|
||||||
try {
|
|
||||||
const response = await GetData(
|
|
||||||
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`,
|
|
||||||
{
|
|
||||||
limit,
|
|
||||||
page: page + 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'DESC',
|
|
||||||
filter: JSON.stringify(filters)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// console.log(response);
|
|
||||||
|
|
||||||
if (!response || !response.data) {
|
|
||||||
console.warn('No data received:', response);
|
|
||||||
return { data: [], totalCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransaction(response.data.list);
|
|
||||||
return { data: response.data.list, totalCount: response.data.total_count };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction', error);
|
|
||||||
return { data: [], totalCount: 0 };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[GetData, idbalance, filters]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAllTransactionData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await GetData(
|
|
||||||
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`,
|
|
||||||
{
|
|
||||||
limit: 10000,
|
|
||||||
page: 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'DESC',
|
|
||||||
filter: JSON.stringify(filters)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response?.data?.list || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching all transaction data', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tambahkan fungsi handle export excel
|
|
||||||
const handleExportExcel = async () => {
|
|
||||||
try {
|
|
||||||
const allData = await getAllTransactionData();
|
|
||||||
|
|
||||||
// Buat objek wallet dummy untuk kompatibilitas dengan fungsi exportToExcel
|
|
||||||
const walletData = {
|
|
||||||
ID: idbalance,
|
|
||||||
name: 'Wallet Details',
|
|
||||||
msisdn: '-'
|
|
||||||
};
|
|
||||||
|
|
||||||
await exportToExcel(allData, walletData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting to Excel:', error);
|
|
||||||
toast.error('Failed to export Excel');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
|
||||||
<DialogContent className="w-screen max-w-screen max-h-screen p-4 overflow-hidden">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Details Wallet Statement</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription />
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full mb-4">
|
|
||||||
<div className="flex flex-wrap gap-3 w-full">
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
From
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={dateRange.from}
|
|
||||||
onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
To
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={dateRange.to}
|
|
||||||
onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="w-full sm:w-[160px]">
|
|
||||||
<Select
|
|
||||||
value={selectedTransferType || 'all'}
|
|
||||||
onValueChange={handleTransferTypeChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-[32px]">
|
|
||||||
<SelectValue placeholder="Transaction Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Select Transaction</SelectItem>
|
|
||||||
{transferType.map((transfer) => (
|
|
||||||
<SelectItem key={transfer.id} value={transfer.id}>
|
|
||||||
{transfer.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full sm:w-[160px]">
|
|
||||||
<Select value={selectedCategory ?? ''} onValueChange={handleCategoryChange}>
|
|
||||||
<SelectTrigger className="h-[32px]">
|
|
||||||
<SelectValue placeholder="Select Category" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{category.length === 0 ? (
|
|
||||||
<div className="text-gray-500 px-4 py-2">No data</div>
|
|
||||||
) : (
|
|
||||||
category.map((cat) => (
|
|
||||||
<SelectItem key={cat} value={cat}>
|
|
||||||
{cat}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
<KeenIcon icon="magnifier" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Transaction Code"
|
|
||||||
className="overflow-hidden text-ellipsis w-full"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<DefaultTooltip title="Reset Filter" placement="top">
|
|
||||||
<Button variant="outline" className="h-8" onClick={handleClearAllFilters}>
|
|
||||||
<KeenIcon icon="arrow-circle-left" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
|
|
||||||
<Button variant="default" className="h-8" onClick={handleApplyFilters}>
|
|
||||||
Filter Data
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DefaultTooltip title={'Export to Excel'} placement={'top'}>
|
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
className="h-7.5 min-w-[58px]"
|
|
||||||
onClick={handleExportExcel}
|
|
||||||
>
|
|
||||||
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="Excel" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<DataGridProvider
|
|
||||||
key={JSON.stringify(filters)}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{ size: 5 }}
|
|
||||||
layout={{ card: true }}
|
|
||||||
sorting={[{ id: 'id', desc: false }]}
|
|
||||||
serverSide={true}
|
|
||||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
|
||||||
getTransactionLists(pageIndex, pageSize, sorting, columnFilters)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShowDetailWalletDialog;
|
|
||||||
429
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
429
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { getAuth } from '@/auth';
|
||||||
|
|
||||||
|
interface SitePoint {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
cell: string;
|
||||||
|
sector: string;
|
||||||
|
site: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- KONFIGURASI API ---
|
||||||
|
const API_MASTER_DATA = apiConfig.service_master_data;
|
||||||
|
|
||||||
|
const fetchSitePoints = async (): Promise<SitePoint[]> => {
|
||||||
|
const token = getAuth()?.access_token;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_MASTER_DATA}/site-points/all-data`, {
|
||||||
|
headers: {
|
||||||
|
accept: '*/*',
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch site points: ${res.status}`);
|
||||||
|
}
|
||||||
|
const json = await res.json();
|
||||||
|
return json.data?.list ?? json;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TAMBAHAN DATA MOCK POLYGON UNTUK AREA KLASTER ---
|
||||||
|
// Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster
|
||||||
|
const CLUSTER_AREAS = [
|
||||||
|
{
|
||||||
|
id: 'area-surabaya-pusat',
|
||||||
|
name: 'Tunjungan & Pusat',
|
||||||
|
color: '#be123c', // Warna merah muda transparan seperti di screenshot
|
||||||
|
regionalName: 'JATIM',
|
||||||
|
siteId: 'SBY095',
|
||||||
|
neId: 'SBY095ML',
|
||||||
|
siteName: 'N_SBY095ML_WESTIN',
|
||||||
|
bscrncName: '-',
|
||||||
|
lacCi: '232095 - 11',
|
||||||
|
node: '4G',
|
||||||
|
coords: [
|
||||||
|
[-8.5550, 125.5650],
|
||||||
|
[-8.5550, 125.5800],
|
||||||
|
[-8.5650, 125.5850],
|
||||||
|
[-8.5680, 125.5700],
|
||||||
|
] as [number, number][]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'area-surabaya-timur',
|
||||||
|
name: 'Gubeng & Manyar',
|
||||||
|
color: '#be123c',
|
||||||
|
regionalName: 'JATIM',
|
||||||
|
siteId: 'SBY102',
|
||||||
|
neId: 'SBY102ML',
|
||||||
|
siteName: 'N_SBY102ML_GUBENG',
|
||||||
|
bscrncName: '-',
|
||||||
|
lacCi: '232102 - 12',
|
||||||
|
node: '4G',
|
||||||
|
coords: [
|
||||||
|
[-8.5500, 125.5850],
|
||||||
|
[-8.5480, 125.5950],
|
||||||
|
[-8.5580, 125.5980],
|
||||||
|
[-8.5600, 125.5880],
|
||||||
|
] as [number, number][]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const DILI_CENTER: [number, number] = [-8.5586, 125.5736];
|
||||||
|
const ZOOM_THRESHOLD = 14; // Batas tingkat zoom untuk menyembunyikan/menampilkan pin biru
|
||||||
|
|
||||||
|
const groupByCoordinate = (points: SitePoint[]) => {
|
||||||
|
const map = new Map<string, SitePoint[]>();
|
||||||
|
points.forEach((p) => {
|
||||||
|
const key = `${p.lat.toFixed(6)},${p.lng.toFixed(6)}`;
|
||||||
|
if (!map.has(key)) map.set(key, []);
|
||||||
|
map.get(key)!.push(p);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pinIcon = () => {
|
||||||
|
const size = 28;
|
||||||
|
const bg = '#2563eb';
|
||||||
|
return L.divIcon({
|
||||||
|
className: '',
|
||||||
|
html: `
|
||||||
|
<div style="width:${size}px;height:${size}px;border-radius:50%;background:${bg};border:2px solid white;box-shadow:0 1px 3px rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="${size * 0.62}" height="${size * 0.62}" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="21" x2="12" y2="10"></line>
|
||||||
|
<circle cx="12" cy="7" r="2" fill="white" stroke="none"></circle>
|
||||||
|
<path d="M8.5 10.5c0-2 1.5-3.5 3.5-3.5s3.5 1.5 3.5 3.5"></path>
|
||||||
|
<path d="M5.5 12.5c0-4 3-6.5 6.5-6.5s6.5 2.5 6.5 6.5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>`,
|
||||||
|
iconSize: [size, size],
|
||||||
|
iconAnchor: [size / 2, size / 2],
|
||||||
|
popupAnchor: [0, -size / 2]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TEMPLATE TAB DIALOG UNTUK AREA KLASTER (SESUAI SCREENSHOT) ---
|
||||||
|
const buildClusterPopupHtml = (area: typeof CLUSTER_AREAS[0]) => {
|
||||||
|
return `
|
||||||
|
<div class="cluster-popup" style="width:320px; font-family: Arial, sans-serif; color: #333;">
|
||||||
|
<div style="display:flex; border-bottom: 2px solid #e5e7eb; margin-bottom: 12px; font-size: 12px; font-weight: bold;">
|
||||||
|
<div data-cluster-tab="general" class="cluster-tab active" style="padding: 6px 12px; cursor: pointer; color: #2563eb; border-bottom: 2px solid #2563eb; margin-bottom: -2px;">GENERAL</div>
|
||||||
|
<div data-cluster-tab="revenue" class="cluster-tab" style="padding: 6px 12px; cursor: pointer; color: #64748b;">REVENUE</div>
|
||||||
|
<div data-cluster-tab="network" class="cluster-tab" style="padding: 6px 12px; cursor: pointer; color: #64748b;">NETWORK</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-cluster-content="general" class="cluster-content" style="display: block;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||||
|
<tr style="background: #f8fafc;"><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">Regional Name</td><td style="padding: 6px 8px;">${area.regionalName}</td></tr>
|
||||||
|
<tr><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">Site ID</td><td style="padding: 6px 8px;">${area.siteId}</td></tr>
|
||||||
|
<tr style="background: #f8fafc;"><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">NeId</td><td style="padding: 6px 8px;">${area.neId}</td></tr>
|
||||||
|
<tr><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">Site Name</td><td style="padding: 6px 8px; font-weight: 600;">${area.siteName}</td></tr>
|
||||||
|
<tr style="background: #f8fafc;"><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">BSCRNC Name</td><td style="padding: 6px 8px;">${area.bscrncName}</td></tr>
|
||||||
|
<tr><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">LAC - CI</td><td style="padding: 6px 8px;">${area.lacCi}</td></tr>
|
||||||
|
<tr style="background: #f8fafc;"><td style="padding: 6px 8px; font-weight: 600; color: #64748b;">Node</td><td style="padding: 6px 8px;">${area.node}</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-cluster-content="revenue" class="cluster-content" style="display: none; padding: 10px 8px; font-size: 12px; color: #64748b;">
|
||||||
|
Informasi tren revenue klaster ${area.name} di sini.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-cluster-content="network" class="cluster-content" style="display: none; padding: 10px 8px; font-size: 12px; color: #64748b;">
|
||||||
|
Informasi performa network klaster ${area.name} di sini.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wires up tab switching untuk Popup Area Klaster
|
||||||
|
const wireClusterPopup = (container: HTMLElement) => {
|
||||||
|
const tabs = container.querySelectorAll<HTMLElement>('.cluster-tab');
|
||||||
|
const contents = container.querySelectorAll<HTMLElement>('.cluster-content');
|
||||||
|
|
||||||
|
tabs.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const target = btn.dataset.clusterTab;
|
||||||
|
tabs.forEach((t) => {
|
||||||
|
if (t === btn) {
|
||||||
|
t.style.color = '#2563eb';
|
||||||
|
t.style.borderBottom = '2px solid #2563eb';
|
||||||
|
} else {
|
||||||
|
t.style.color = '#64748b';
|
||||||
|
t.style.borderBottom = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
contents.forEach((c) => {
|
||||||
|
c.style.display = c.dataset.clusterContent === target ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- POPUP PIN BIRU INDIVIDUAL (Bawaan Sebelumnya) ---
|
||||||
|
const QUALITY_LEVELS = ['Low', 'Moderate', 'Good', 'Very Good', 'Excellent'];
|
||||||
|
const placeholderQuality = (seed: string, offset: number) => {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
|
||||||
|
const level = (hash + offset) % QUALITY_LEVELS.length;
|
||||||
|
return { level: level + 1, label: QUALITY_LEVELS[level] };
|
||||||
|
};
|
||||||
|
const signalBarsHtml = (level: number) => {
|
||||||
|
const heights = [5, 9, 13, 17];
|
||||||
|
return `<div style="display:flex;align-items:flex-end;gap:2px;height:17px;">
|
||||||
|
${heights.map((h, i) => `<div style="width:4px;height:${h}px;border-radius:1px;background:${i < level ? '#2563eb' : '#dbeafe'};"></div>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
const signalCardHtml = (title: string, desc: string, seed: string, offset: number) => {
|
||||||
|
const q = placeholderQuality(seed, offset);
|
||||||
|
return `
|
||||||
|
<div style="flex:1;min-width:0;text-align:center;padding:8px 4px;border:1px solid #e5e7eb;border-radius:8px;">
|
||||||
|
<div style="font-weight:700;font-size:12px;color:#1e293b;margin-bottom:4px;">${title}</div>
|
||||||
|
<div style="display:flex;justify-content:center;margin-bottom:4px;">${signalBarsHtml(q.level)}</div>
|
||||||
|
<div style="font-size:11px;font-weight:600;color:#2563eb;margin-bottom:4px;">${q.label}</div>
|
||||||
|
<div style="font-size:10px;color:#64748b;line-height:1.3;">${desc}</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
const cellListHtml = (points: SitePoint[]) => {
|
||||||
|
if (points.length === 0) return `<div style="padding:10px 0;text-align:center;font-size:12px;color:#94a3b8;">No cells in this band.</div>`;
|
||||||
|
const rows = points.map((p) => `<tr><td style="padding:2px 8px 2px 0;">${p.cell}</td><td>${p.sector}</td></tr>`).join('');
|
||||||
|
return `<table style="width:100%;border-collapse:collapse;font-size:12px;margin-top:6px;">${rows}</table>`;
|
||||||
|
};
|
||||||
|
const buildSignalPopupHtml = (points: SitePoint[], lat: number, lng: number) => {
|
||||||
|
const site = points[0].site;
|
||||||
|
const location = points[0].location;
|
||||||
|
const ran2g = points.filter((p) => p.cell.trim().toUpperCase().endsWith('E'));
|
||||||
|
const ran3g = points.filter((p) => p.cell.trim().toUpperCase().endsWith('F'));
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="signal-popup" style="width:290px;font-family:Arial, sans-serif;">
|
||||||
|
<div style="margin-bottom:8px;">
|
||||||
|
<div style="font-weight:700;font-size:14px;color:#0f172a;">${site}</div>
|
||||||
|
<div style="font-size:11px;color:#64748b;">${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="signal-tabs" style="display:flex;gap:4px;margin-bottom:10px;">
|
||||||
|
<button data-tab="signal" class="signal-tab signal-tab-active" style="flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;border:none;cursor:pointer;background:#2563eb;color:#ffffff;">Signal</button>
|
||||||
|
<button data-tab="ran2g" class="signal-tab" style="flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;border:none;cursor:pointer;background:#f1f5f9;color:#475569;">RAN 2G</button>
|
||||||
|
<button data-tab="ran3g" class="signal-tab" style="flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;border:none;cursor:pointer;background:#f1f5f9;color:#475569;">RAN 3G</button>
|
||||||
|
<button data-tab="alarm" class="signal-tab" style="flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;border:none;cursor:pointer;background:#f1f5f9;color:#475569;">Alarm</button>
|
||||||
|
</div>
|
||||||
|
<div data-content="signal" class="signal-content">
|
||||||
|
<div style="display:flex;gap:6px;">
|
||||||
|
${signalCardHtml('4G', 'Browsing, email, video call', site, 0)}
|
||||||
|
${signalCardHtml('3G', 'Laptop browsing & downloads', site, 1)}
|
||||||
|
${signalCardHtml('2G', 'Calling and texting', site, 2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SitesSatelliteMap = () => {
|
||||||
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const mapRef = useRef<L.Map | null>(null);
|
||||||
|
|
||||||
|
const markerLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
||||||
|
const polygonLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
||||||
|
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [sitePoints, setSitePoints] = useState<SitePoint[]>([]);
|
||||||
|
const [selectedCount, setSelectedCount] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const grouped = useMemo(() => groupByCoordinate(sitePoints), [sitePoints]);
|
||||||
|
|
||||||
|
// --- 1. Fetch data site points dari API saat komponen mount ---
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const data = await fetchSitePoints();
|
||||||
|
if (isMounted) {
|
||||||
|
setSitePoints(data);
|
||||||
|
setSelectedCount(data.length);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isMounted) {
|
||||||
|
setError('Gagal memuat data site points');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// --- 2. Inisialisasi peta & render marker/polygon setelah data tersedia ---
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapContainerRef.current || sitePoints.length === 0) return;
|
||||||
|
|
||||||
|
// Kalau map sudah pernah dibuat sebelumnya, cukup refresh marker-nya saja
|
||||||
|
if (mapRef.current) {
|
||||||
|
markerLayerGroupRef.current.clearLayers();
|
||||||
|
markerByKeyRef.current.clear();
|
||||||
|
|
||||||
|
grouped.forEach((points, key) => {
|
||||||
|
const [lat, lng] = key.split(',').map(Number);
|
||||||
|
const marker = L.marker([lat, lng], { icon: pinIcon() });
|
||||||
|
marker.bindPopup(buildSignalPopupHtml(points, lat, lng), { maxWidth: 320, minWidth: 290 });
|
||||||
|
marker.addTo(markerLayerGroupRef.current);
|
||||||
|
markerByKeyRef.current.set(key, marker);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = L.map(mapContainerRef.current, {
|
||||||
|
center: DILI_CENTER,
|
||||||
|
zoom: 12,
|
||||||
|
zoomControl: true
|
||||||
|
});
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||||
|
attribution: 'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics',
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
|
||||||
|
maxZoom: 19,
|
||||||
|
opacity: 0.9
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Keduanya langsung dimasukkan secara permanen ke peta utama
|
||||||
|
markerLayerGroupRef.current.addTo(map);
|
||||||
|
polygonLayerGroupRef.current.addTo(map);
|
||||||
|
|
||||||
|
// 1. Render Seluruh Marker Pin Biru
|
||||||
|
grouped.forEach((points, key) => {
|
||||||
|
const [lat, lng] = key.split(',').map(Number);
|
||||||
|
const marker = L.marker([lat, lng], { icon: pinIcon() });
|
||||||
|
marker.bindPopup(buildSignalPopupHtml(points, lat, lng), { maxWidth: 320, minWidth: 290 });
|
||||||
|
marker.addTo(markerLayerGroupRef.current);
|
||||||
|
markerByKeyRef.current.set(key, marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Render Batas Klaster / Polygon (Selalu Menetap di Peta)
|
||||||
|
CLUSTER_AREAS.forEach((area) => {
|
||||||
|
const polygon = L.polygon(area.coords, {
|
||||||
|
color: '#b91c1c', // Warna merah tua garis batas luar
|
||||||
|
weight: 2,
|
||||||
|
fillColor: area.color, // Warna kemerahan transparan sesuai request gambar
|
||||||
|
fillOpacity: 0.25
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind dialog tab analisis ke area klaster ketika diklik
|
||||||
|
polygon.bindPopup(buildClusterPopupHtml(area), { maxWidth: 360, minWidth: 310 });
|
||||||
|
polygon.addTo(polygonLayerGroupRef.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logika pengontrol visibilitas Pin Biru berdasarkan tingkatan Zoom
|
||||||
|
const handleZoomVisibility = () => {
|
||||||
|
const currentZoom = map.getZoom();
|
||||||
|
if (currentZoom >= ZOOM_THRESHOLD) {
|
||||||
|
// Mode Zoom In: Tampilkan Pin Biru Individual di atas Polygon
|
||||||
|
if (!map.hasLayer(markerLayerGroupRef.current)) map.addLayer(markerLayerGroupRef.current);
|
||||||
|
} else {
|
||||||
|
// Mode Zoom Out: Sembunyikan Pin Biru, biarkan Area Klaster saja
|
||||||
|
if (map.hasLayer(markerLayerGroupRef.current)) map.removeLayer(markerLayerGroupRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleZoomVisibility();
|
||||||
|
map.on('zoomend', handleZoomVisibility);
|
||||||
|
|
||||||
|
// Delegasi inisialisasi Event Listener interaktif Tab saat popup dibuka
|
||||||
|
const onPopupOpen = (e: L.PopupEvent) => {
|
||||||
|
const el = e.popup.getElement();
|
||||||
|
|
||||||
|
// Kasus A: Jika yang dibuka adalah Dialog Klaster Area
|
||||||
|
const clusterContent = el?.querySelector<HTMLElement>('.cluster-popup');
|
||||||
|
if (clusterContent) {
|
||||||
|
wireClusterPopup(clusterContent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
map.on('popupopen', onPopupOpen);
|
||||||
|
}, [grouped, sitePoints]);
|
||||||
|
|
||||||
|
// --- 3. Cleanup peta saat komponen benar-benar unmount ---
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.off();
|
||||||
|
mapRef.current.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
}
|
||||||
|
markerByKeyRef.current.clear();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
if (!value) {
|
||||||
|
setSelectedCount(sitePoints.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const q = value.toLowerCase();
|
||||||
|
const matches = sitePoints.filter(
|
||||||
|
(p) => p.cell.toLowerCase().includes(q) || p.sector.toLowerCase().includes(q) || p.site.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
setSelectedCount(matches.length);
|
||||||
|
|
||||||
|
if (matches.length > 0 && mapRef.current) {
|
||||||
|
const first = matches[0];
|
||||||
|
const key = `${first.lat.toFixed(6)},${first.lng.toFixed(6)}`;
|
||||||
|
mapRef.current.setView([first.lat, first.lng], ZOOM_THRESHOLD + 2);
|
||||||
|
const marker = markerByKeyRef.current.get(key);
|
||||||
|
if (marker) marker.openPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{isLoading
|
||||||
|
? 'Memuat data...'
|
||||||
|
: `Showing ${selectedCount} from ${sitePoints.length} points (Cell/Sector)`}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
placeholder="Cari Cell, Sector, atau nama Site..."
|
||||||
|
className="input input-sm w-full sm:w-72"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative w-full rounded-lg overflow-hidden border border-gray-200" style={{ height: '70vh' }}>
|
||||||
|
<div ref={mapContainerRef} className="w-full h-full" />
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-white/60">
|
||||||
|
<span className="text-sm text-gray-500">Memuat peta...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SitesSatelliteMap;
|
||||||
@ -1,103 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell,
|
|
||||||
ResponsiveContainer,
|
|
||||||
} from 'recharts';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransactionPieChart = ({ startdate, enddate }: Props) => {
|
|
||||||
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataTransactionValue = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/transaction-chart`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction value:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataTransactionValue();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{ name: 'Transfer', value: parseFloat((responseTransactionValue?.data?.T ?? 0).toFixed(2)), color: '#3490dc' },
|
|
||||||
{ name: 'Return', value: parseFloat((responseTransactionValue?.data?.R ?? 0).toFixed(2)), color: '#a0aec0' },
|
|
||||||
{ name: 'Topup', value: parseFloat((responseTransactionValue?.data?.U ?? 0).toFixed(2)), color: '#9f7aea' },
|
|
||||||
{ name: 'Purchase', value: parseFloat((responseTransactionValue?.data?.P ?? 0).toFixed(2)), color: '#baf7c5' },
|
|
||||||
{ name: 'Withdraw', value: parseFloat((responseTransactionValue?.data?.W ?? 0).toFixed(2)), color: '#f56565' },
|
|
||||||
{ name: 'Top Up Partner', value: parseFloat((responseTransactionValue?.data?.N ?? 0).toFixed(2)), color: '#f7fa52' },
|
|
||||||
{ name: 'Reward', value: parseFloat((responseTransactionValue?.data?.E ?? 0).toFixed(2)), color: '#f50a19' },
|
|
||||||
{ name: 'Purchase Loja', value: parseFloat((responseTransactionValue?.data?.L ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Top Up P24', value: parseFloat((responseTransactionValue?.data?.B ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Transfer Agent', value: parseFloat((responseTransactionValue?.data?.A ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Withdrawal Agent', value: parseFloat((responseTransactionValue?.data?.M ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
{ name: 'Top Up Agent', value: parseFloat((responseTransactionValue?.data?.O ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Transfer P24', value: parseFloat((responseTransactionValue?.data?.S ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Withdraw Merchant', value: parseFloat((responseTransactionValue?.data?.I ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Donation', value: parseFloat((responseTransactionValue?.data?.D ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
{ name: 'Fee', value: parseFloat((responseTransactionValue?.data?.F ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Reversal', value: parseFloat((responseTransactionValue?.data?.V ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Cashback Cash', value: parseFloat((responseTransactionValue?.data?.C ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Cashback Point', value: parseFloat((responseTransactionValue?.data?.H ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 w-1/3">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700">Transaction Chart</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-6">
|
|
||||||
<div className="w-40 h-40">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={data}
|
|
||||||
dataKey="value"
|
|
||||||
innerRadius={40}
|
|
||||||
outerRadius={60}
|
|
||||||
paddingAngle={3}
|
|
||||||
>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm space-y-2">
|
|
||||||
<div className="font-semibold text-gray-500">Active Transaction</div>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between w-48">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: entry.color }}></div>
|
|
||||||
<span className="text-gray-700">{entry.name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-700">{entry.value}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransactionPieChart;
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransactionValue = ({ startdate, enddate }: Props) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataTransactionValue = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/transaction-value`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction value:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataTransactionValue();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
let transactionData: any[] = [];
|
|
||||||
if (responseTransactionValue?.data?.length > 0) {
|
|
||||||
for (let i = 0; i < responseTransactionValue.data.length; i++) {
|
|
||||||
let type = "";
|
|
||||||
let unit = "";
|
|
||||||
|
|
||||||
const amount = responseTransactionValue.data[i].total_amount;
|
|
||||||
|
|
||||||
if (amount >= 1000 && amount < 1000000) {
|
|
||||||
unit = "K";
|
|
||||||
} else if (amount >= 1000000 && amount < 1000000000) {
|
|
||||||
unit = "M";
|
|
||||||
} else if (amount >= 1000000000) {
|
|
||||||
unit = "B";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (responseTransactionValue.data[i].kind) {
|
|
||||||
case "P":
|
|
||||||
type = "Purchase";
|
|
||||||
break;
|
|
||||||
case "T":
|
|
||||||
type = "Transfer";
|
|
||||||
break;
|
|
||||||
case "U":
|
|
||||||
type = "Top Up";
|
|
||||||
break;
|
|
||||||
case "W":
|
|
||||||
type = "Withdraw";
|
|
||||||
break;
|
|
||||||
case "R":
|
|
||||||
type = "Return";
|
|
||||||
break;
|
|
||||||
case "N":
|
|
||||||
type = "Top Up Partner";
|
|
||||||
break;
|
|
||||||
case "E":
|
|
||||||
type = "Reward";
|
|
||||||
break;
|
|
||||||
case "L":
|
|
||||||
type = "Purchase Loja"
|
|
||||||
break;
|
|
||||||
case "B":
|
|
||||||
type = "Top Up P24";
|
|
||||||
break;
|
|
||||||
case "A":
|
|
||||||
type = " Transfer Agent";
|
|
||||||
break;
|
|
||||||
case "M":
|
|
||||||
type = "Withdrawal Agent";
|
|
||||||
break;
|
|
||||||
case 'O':
|
|
||||||
type = 'Top Up Agent';
|
|
||||||
break;
|
|
||||||
case 'S':
|
|
||||||
type = 'Transfer P24';
|
|
||||||
break;
|
|
||||||
case 'I':
|
|
||||||
type = 'Withdraw Merchant';
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
type = 'Donation';
|
|
||||||
break;
|
|
||||||
case 'F':
|
|
||||||
type = 'Fee';
|
|
||||||
break;
|
|
||||||
case 'V':
|
|
||||||
type = 'Reversal';
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
type = 'Cashback Cash';
|
|
||||||
break;
|
|
||||||
case 'H':
|
|
||||||
type = 'Cashback Point';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
type = "Unknown";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
transactionData.push({
|
|
||||||
label: type,
|
|
||||||
value: amount,
|
|
||||||
unit: unit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl shadow p-6 w-1/3">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
|
|
||||||
</div>
|
|
||||||
{transactionData.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{transactionData.map((item, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between">
|
|
||||||
<span className="w-24 text-sm text-gray-700">{item.label}</span>
|
|
||||||
<div className="flex-1 mx-2">
|
|
||||||
<div className="w-full h-3 bg-gray-200 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-3 bg-teal-500 rounded-full"
|
|
||||||
style={{ width: `${(item.value / maxValue) * 100}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="w-24 text-right text-sm text-gray-700 break-words">
|
|
||||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-sm text-gray-500 mt-4">
|
|
||||||
No Data Available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransactionValue;
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { get5LastYear } from '@/utils/Date';
|
|
||||||
|
|
||||||
interface YearPickerProps {
|
|
||||||
selectedYear: string;
|
|
||||||
setSelectedYear: (year: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Komponen YearPicker
|
|
||||||
const YearPicker = ({ selectedYear, setSelectedYear }: YearPickerProps) => {
|
|
||||||
const selectYear = get5LastYear();
|
|
||||||
|
|
||||||
const handleYearChange = (year: string) => {
|
|
||||||
setSelectedYear(year);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Select value={selectedYear} onValueChange={handleYearChange}>
|
|
||||||
<SelectTrigger size="sm" className="w-28">
|
|
||||||
<SelectValue placeholder="Pilih Tahun" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{selectYear.length > 0 ? (
|
|
||||||
selectYear.map((year, index) => (
|
|
||||||
<SelectItem key={index} value={year}>
|
|
||||||
{year}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem value="no-data" disabled>
|
|
||||||
No data available
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { YearPicker };
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './Card';
|
|
||||||
export * from './DateRangePicker';
|
|
||||||
export * from './Chart';
|
|
||||||
export * from './YearPicker';
|
|
||||||
@ -1,5 +1,2 @@
|
|||||||
export * from './DashboardHomeContext';
|
export * from './DashboardHomeContext';
|
||||||
export * from './useDashboardHomeContext';
|
export * from './useDashboardHomeContext';
|
||||||
export * from './useFetchCardData';
|
|
||||||
export * from './useFetchChartData';
|
|
||||||
// export * from './useFetchYear';
|
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { formatDate } from 'date-fns';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface UseFetchCardDataResult {
|
|
||||||
cardData: any;
|
|
||||||
isCardLoading: boolean;
|
|
||||||
cardError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchCardData = (
|
|
||||||
start_date: string,
|
|
||||||
end_date: string,
|
|
||||||
count: string
|
|
||||||
): UseFetchCardDataResult => {
|
|
||||||
const [cardData, setCardData] = useState<[]>([]);
|
|
||||||
const [isCardLoading, setIsCardLoading] = useState<boolean>(false);
|
|
||||||
const [cardError, setCardError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsCardLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/card`, {
|
|
||||||
params: {
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
aggregate: count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setCardData(response.data.data);
|
|
||||||
} catch (err) {
|
|
||||||
setCardError(err);
|
|
||||||
} finally {
|
|
||||||
setIsCardLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
|
||||||
|
|
||||||
return { cardData, isCardLoading, cardError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchCardData };
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { formatDate } from 'date-fns';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface UseFetchChartDataResult {
|
|
||||||
chartData: any[];
|
|
||||||
isChartLoading: boolean;
|
|
||||||
chartError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchChartData = (
|
|
||||||
start_date: string,
|
|
||||||
end_date: string,
|
|
||||||
count: string
|
|
||||||
): UseFetchChartDataResult => {
|
|
||||||
const [chartData, setChartData] = useState<any[]>([]);
|
|
||||||
const [isChartLoading, setIsChartLoading] = useState<boolean>(false);
|
|
||||||
const [chartError, setChartError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsChartLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/chart`, {
|
|
||||||
params: {
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
aggregate: count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setChartData(response.data.data);
|
|
||||||
} catch (err) {
|
|
||||||
setChartError(err);
|
|
||||||
} finally {
|
|
||||||
setIsChartLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
|
||||||
|
|
||||||
return { chartData, isChartLoading, chartError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchChartData };
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface YearData {
|
|
||||||
year: string;
|
|
||||||
}
|
|
||||||
interface UseFetchselectYearResult {
|
|
||||||
selectYear: YearData[];
|
|
||||||
isselectYearLoading: boolean;
|
|
||||||
selectYearError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchYear = (
|
|
||||||
path: string = '',
|
|
||||||
start_date: string = '',
|
|
||||||
end_date: string = '',
|
|
||||||
year: string = ''
|
|
||||||
): UseFetchselectYearResult => {
|
|
||||||
const [selectYear, setSelectYear] = useState<YearData[]>([]);
|
|
||||||
const [isselectYearLoading, setIsselectYearLoading] = useState<boolean>(false);
|
|
||||||
const [selectYearError, setSelectYearError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsselectYearLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/year`);
|
|
||||||
setSelectYear(response.data.data.map((item: { year: string }) => ({ year: item.year })));
|
|
||||||
} catch (err) {
|
|
||||||
setSelectYearError(err);
|
|
||||||
} finally {
|
|
||||||
setIsselectYearLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [path, start_date, end_date, year]);
|
|
||||||
|
|
||||||
return { selectYear, isselectYearLoading, selectYearError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchYear };
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
export const staticChartData = {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
categories: 'Jan',
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 01',
|
|
||||||
y: 50
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 02',
|
|
||||||
y: 75
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 03',
|
|
||||||
y: 200
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 04',
|
|
||||||
y: 125
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 05',
|
|
||||||
y: 150
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: 'Jan 06',
|
|
||||||
y: 175
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
xaxis: {
|
|
||||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -10,7 +10,7 @@ export default function ManageUserPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>TPAY | Manage User</title>
|
<title>REVENUE | Manage User</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<ManageUserContextProvider>
|
<ManageUserContextProvider>
|
||||||
<Container>
|
<Container>
|
||||||
@ -20,9 +20,9 @@ export default function ManageUserPage() {
|
|||||||
<span className="text-sm hover:underline">Dashboard</span>
|
<span className="text-sm hover:underline">Dashboard</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link underline="none" color="inherit">
|
{/* <Link underline="none" color="inherit">
|
||||||
<span className="text-sm">Settings</span>
|
<span className="text-sm">Settings</span>
|
||||||
</Link>
|
</Link> */}
|
||||||
|
|
||||||
<Link underline="none" color="inherit">
|
<Link underline="none" color="inherit">
|
||||||
<span className="text-sm">User Management</span>
|
<span className="text-sm">User Management</span>
|
||||||
|
|||||||
@ -41,8 +41,7 @@ import {
|
|||||||
validateFormCreateUser
|
validateFormCreateUser
|
||||||
} from './Types';
|
} from './Types';
|
||||||
|
|
||||||
const API_URL_CUSTOMER = apiConfig.service_customer;
|
const API_URL_USER = apiConfig.service_user;
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
type PasswordType = 'password' | 'retype_password';
|
type PasswordType = 'password' | 'retype_password';
|
||||||
|
|
||||||
@ -116,7 +115,7 @@ const AddDialog = () => {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await PostData(`${API_URL}/user/create`, formField);
|
const response = await PostData(`${API_URL_USER}/user/create`, formField);
|
||||||
|
|
||||||
if (response?.status) {
|
if (response?.status) {
|
||||||
handleAddDialog(false);
|
handleAddDialog(false);
|
||||||
@ -143,74 +142,74 @@ const AddDialog = () => {
|
|||||||
[formField]
|
[formField]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchRoles = useCallback(async () => {
|
// const fetchRoles = useCallback(async () => {
|
||||||
const params = {
|
// const params = {
|
||||||
limit: 100,
|
// limit: 100,
|
||||||
page: 1,
|
// page: 1,
|
||||||
with_deleted: false,
|
// with_deleted: false,
|
||||||
order_field: 'name',
|
// order_field: 'name',
|
||||||
order_direction: 'ASC',
|
// order_direction: 'ASC',
|
||||||
filter: JSON.stringify({
|
// filter: JSON.stringify({
|
||||||
status: 'Y'
|
// status: 'Y'
|
||||||
})
|
// })
|
||||||
};
|
// };
|
||||||
const response = await GetData(`${API_URL}/user_role/list`, params);
|
// const response = await GetData(`${API_URL}/user_role/list`, params);
|
||||||
if (response?.status) {
|
// if (response?.status) {
|
||||||
const roleList = response.data?.list || [];
|
// const roleList = response.data?.list || [];
|
||||||
setRoles(roleList);
|
// setRoles(roleList);
|
||||||
} else {
|
// } else {
|
||||||
setRoles(() => []);
|
// setRoles(() => []);
|
||||||
}
|
// }
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
const getCustomerList = async (sorting: any, filterValue: string) => {
|
// const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||||
const filter: any =
|
// const filter: any =
|
||||||
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
// filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
||||||
|
|
||||||
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
// sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||||
|
|
||||||
const query: any = {
|
// const query: any = {
|
||||||
limit: 100,
|
// limit: 100,
|
||||||
page: 1,
|
// page: 1,
|
||||||
with_deleted: false,
|
// with_deleted: false,
|
||||||
order_field: sorting[0].id,
|
// order_field: sorting[0].id,
|
||||||
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
// order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||||
};
|
// };
|
||||||
|
|
||||||
if (filter && Object.keys(filter).length > 0) {
|
// if (filter && Object.keys(filter).length > 0) {
|
||||||
query.filter = JSON.stringify(filter);
|
// query.filter = JSON.stringify(filter);
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
// const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||||
|
|
||||||
if (response?.status) {
|
// if (response?.status) {
|
||||||
setCustomers(response?.data.list);
|
// setCustomers(response?.data.list);
|
||||||
} else {
|
// } else {
|
||||||
toast.error(response?.message);
|
// toast.error(response?.message);
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
toast.error('Failed to fetch customer list');
|
// toast.error('Failed to fetch customer list');
|
||||||
} finally {
|
// } finally {
|
||||||
setIsLoading(false);
|
// setIsLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsLoading(true);
|
// setIsLoading(true);
|
||||||
setSearchTerm(e.target.value);
|
// setSearchTerm(e.target.value);
|
||||||
setSelectedCustomerName(e.target.value);
|
// setSelectedCustomerName(e.target.value);
|
||||||
setDropdownOpen(true);
|
// setDropdownOpen(true);
|
||||||
const timer = setTimeout(() => {
|
// const timer = setTimeout(() => {
|
||||||
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
// getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||||
}, 500);
|
// }, 500);
|
||||||
return () => clearTimeout(timer);
|
// return () => clearTimeout(timer);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleCustomerSelect = (customer: CustomerProps) => {
|
// const handleCustomerSelect = (customer: CustomerProps) => {
|
||||||
setFormField({ ...formField, customerid: customer.id });
|
// setFormField({ ...formField, customerid: customer.id });
|
||||||
setSelectedCustomerName(customer.fullname);
|
// setSelectedCustomerName(customer.fullname);
|
||||||
setDropdownOpen(false);
|
// setDropdownOpen(false);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -230,9 +229,9 @@ const AddDialog = () => {
|
|||||||
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
fetchRoles();
|
// fetchRoles();
|
||||||
}, [fetchRoles]);
|
// }, [fetchRoles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validation = validatePassword(formField.password, formField.retype_password);
|
const validation = validatePassword(formField.password, formField.retype_password);
|
||||||
@ -248,20 +247,20 @@ const AddDialog = () => {
|
|||||||
}
|
}
|
||||||
}, [showAddDialog]);
|
}, [showAddDialog]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
getCustomerList([{ id: 'created_at', desc: false }], '');
|
// getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||||
|
|
||||||
const handleClickOutside = (event: any) => {
|
// const handleClickOutside = (event: any) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
// if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
setDropdownOpen(false);
|
// setDropdownOpen(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
// document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => {
|
// return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
// document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
// };
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
|
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -331,7 +330,7 @@ const AddDialog = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Customer */}
|
{/* Customer */}
|
||||||
<div className="flex gap-2.5">
|
{/* <div className="flex gap-2.5">
|
||||||
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
|
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
|
||||||
<div className="grow flex flex-col" ref={dropdownRef}>
|
<div className="grow flex flex-col" ref={dropdownRef}>
|
||||||
<Input
|
<Input
|
||||||
@ -366,7 +365,7 @@ const AddDialog = () => {
|
|||||||
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
@ -387,7 +386,7 @@ const AddDialog = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Role */}
|
{/* Role */}
|
||||||
<div className="flex gap-2.5">
|
{/* <div className="flex gap-2.5">
|
||||||
<label className="form-label flex items-center gap-1 max-w-56">Role</label>
|
<label className="form-label flex items-center gap-1 max-w-56">Role</label>
|
||||||
<div className="grow flex flex-col">
|
<div className="grow flex flex-col">
|
||||||
<Select
|
<Select
|
||||||
@ -414,7 +413,7 @@ const AddDialog = () => {
|
|||||||
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { useCallApi } from '@/hooks';
|
|||||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||||
import { EnforceSwitch } from '@/components/switch';
|
import { EnforceSwitch } from '@/components/switch';
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
const API_URL = apiConfig.service_user;
|
||||||
|
|
||||||
const DeleteDialog = () => {
|
const DeleteDialog = () => {
|
||||||
const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext();
|
const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext();
|
||||||
|
|||||||
@ -39,8 +39,8 @@ import {
|
|||||||
validateFormCreateUser
|
validateFormCreateUser
|
||||||
} from './Types';
|
} from './Types';
|
||||||
|
|
||||||
const API_URL_CUSTOMER = apiConfig.service_customer;
|
// const API_URL_CUSTOMER = apiConfig.service_customer;
|
||||||
const API_URL = apiConfig.service_dashboard;
|
const API_URL = apiConfig.service_user;
|
||||||
|
|
||||||
const EditDialog = () => {
|
const EditDialog = () => {
|
||||||
const parentRef = useRef<any | null>(null);
|
const parentRef = useRef<any | null>(null);
|
||||||
@ -109,59 +109,59 @@ const EditDialog = () => {
|
|||||||
[selectedUser, formField]
|
[selectedUser, formField]
|
||||||
);
|
);
|
||||||
|
|
||||||
const doFetchUserRole = useCallback(async (sorting: any) => {
|
// const doFetchUserRole = useCallback(async (sorting: any) => {
|
||||||
try {
|
// try {
|
||||||
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
// sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
||||||
const response = await GetData(`${API_URL}/user_role/list`, {
|
// const response = await GetData(`${API_URL}/user_role/list`, {
|
||||||
limit: 100,
|
// limit: 100,
|
||||||
page: 1,
|
// page: 1,
|
||||||
with_deleted: false,
|
// with_deleted: false,
|
||||||
order_field: sorting[0].id,
|
// order_field: sorting[0].id,
|
||||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
|
// order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
|
||||||
});
|
// });
|
||||||
|
|
||||||
setRoles(response?.data.list);
|
// setRoles(response?.data.list);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('Error fetching role', error);
|
// console.error('Error fetching role', error);
|
||||||
}
|
// }
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
const getCustomerList = async (sorting: any, filterValue: string) => {
|
// const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||||
const filter: any =
|
// const filter: any =
|
||||||
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
// filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
||||||
|
|
||||||
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
// sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
|
||||||
|
|
||||||
const query: any = {
|
// const query: any = {
|
||||||
limit: 100,
|
// limit: 100,
|
||||||
page: 1,
|
// page: 1,
|
||||||
with_deleted: false,
|
// with_deleted: false,
|
||||||
order_field: sorting[0].id,
|
// order_field: sorting[0].id,
|
||||||
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
// order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||||
};
|
// };
|
||||||
|
|
||||||
if (filter && Object.keys(filter).length > 0) {
|
// if (filter && Object.keys(filter).length > 0) {
|
||||||
query.filter = JSON.stringify(filter);
|
// query.filter = JSON.stringify(filter);
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
// const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||||
|
|
||||||
if (response?.status) {
|
// if (response?.status) {
|
||||||
setCustomers(response?.data.list);
|
// setCustomers(response?.data.list);
|
||||||
} else {
|
// } else {
|
||||||
toast.error(response?.message);
|
// toast.error(response?.message);
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
toast.error('Failed to fetch customer list');
|
// toast.error('Failed to fetch customer list');
|
||||||
} finally {
|
// } finally {
|
||||||
setIsLoadingCustomer(false);
|
// setIsLoadingCustomer(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const doFetchUserData = useCallback(async (id: string) => {
|
const doFetchUserData = useCallback(async (id: string) => {
|
||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
|
|
||||||
const response = await GetData(`${API_URL}/user/detail/${id}`, { id });
|
const response = await GetData(`${API_URL}/user/profile/${id}`, { id });
|
||||||
|
|
||||||
if (response?.status) {
|
if (response?.status) {
|
||||||
setFormField((prev) => ({
|
setFormField((prev) => ({
|
||||||
@ -169,9 +169,9 @@ const EditDialog = () => {
|
|||||||
name: response.data.name,
|
name: response.data.name,
|
||||||
username: response.data.username,
|
username: response.data.username,
|
||||||
email: response.data.email,
|
email: response.data.email,
|
||||||
id_role: response.data.idRole,
|
// id_role: response.data.idRole,
|
||||||
status: response.data.status,
|
status: response.data.status,
|
||||||
customerid: response.data.customer?.id || ''
|
// customerid: response.data.customer?.id || ''
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setFormField((prev) => ({
|
setFormField((prev) => ({
|
||||||
@ -179,30 +179,30 @@ const EditDialog = () => {
|
|||||||
name: '',
|
name: '',
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
id_role: '',
|
// id_role: '',
|
||||||
status: '',
|
status: '',
|
||||||
customerid: ''
|
// customerid: ''
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsLoadingCustomer(true);
|
// setIsLoadingCustomer(true);
|
||||||
setSearchTerm(e.target.value);
|
// setSearchTerm(e.target.value);
|
||||||
setSelectedCustomerName(e.target.value);
|
// setSelectedCustomerName(e.target.value);
|
||||||
setDropdownOpen(true);
|
// setDropdownOpen(true);
|
||||||
const timer = setTimeout(() => {
|
// const timer = setTimeout(() => {
|
||||||
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
// getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||||
}, 500);
|
// }, 500);
|
||||||
return () => clearTimeout(timer);
|
// return () => clearTimeout(timer);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleCustomerSelect = (customer: CustomerProps) => {
|
// const handleCustomerSelect = (customer: CustomerProps) => {
|
||||||
setFormField({ ...formField, customerid: customer.id });
|
// setFormField({ ...formField, customerid: customer.id });
|
||||||
setSelectedCustomerName(customer.fullname);
|
// setSelectedCustomerName(customer.fullname);
|
||||||
setDropdownOpen(false);
|
// setDropdownOpen(false);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const filteredCustomer = customers
|
const filteredCustomer = customers
|
||||||
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
@ -216,8 +216,8 @@ const EditDialog = () => {
|
|||||||
}, [showEditDialog]);
|
}, [showEditDialog]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCustomerList([{ id: 'created_at', desc: false }], '');
|
// getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||||
doFetchUserRole([{ id: 'name', desc: false }]);
|
// doFetchUserRole([{ id: 'name', desc: false }]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -316,7 +316,7 @@ const EditDialog = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Customer */}
|
{/* Customer */}
|
||||||
<div className="flex gap-2.5">
|
{/* <div className="flex gap-2.5">
|
||||||
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
|
<label className="form-label flex items-center gap-1 max-w-56">Customer</label>
|
||||||
<div className="grow flex flex-col" ref={dropdownRef}>
|
<div className="grow flex flex-col" ref={dropdownRef}>
|
||||||
<Input
|
<Input
|
||||||
@ -351,7 +351,7 @@ const EditDialog = () => {
|
|||||||
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
@ -374,7 +374,7 @@ const EditDialog = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Role */}
|
{/* Role */}
|
||||||
<div className="flex gap-2.5">
|
{/* <div className="flex gap-2.5">
|
||||||
<label className="form-label flex items-center gap-1 max-w-56">Role</label>
|
<label className="form-label flex items-center gap-1 max-w-56">Role</label>
|
||||||
<div className="grow flex flex-col">
|
<div className="grow flex flex-col">
|
||||||
<Select
|
<Select
|
||||||
@ -401,7 +401,7 @@ const EditDialog = () => {
|
|||||||
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export interface CreateUserParams {
|
|||||||
password: string;
|
password: string;
|
||||||
retype_password: string;
|
retype_password: string;
|
||||||
name: string;
|
name: string;
|
||||||
id_role: string;
|
// id_role: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,18 +31,18 @@ export const initialStateCreateUser: {
|
|||||||
password: string;
|
password: string;
|
||||||
retype_password: string;
|
retype_password: string;
|
||||||
name: string;
|
name: string;
|
||||||
id_role: string;
|
// id_role: string;
|
||||||
status: string;
|
status: string;
|
||||||
customerid: string;
|
// customerid: string;
|
||||||
} = {
|
} = {
|
||||||
email: '',
|
email: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
retype_password: '',
|
retype_password: '',
|
||||||
name: '',
|
name: '',
|
||||||
id_role: '',
|
// id_role: '',
|
||||||
status: '',
|
status: '',
|
||||||
customerid: ''
|
// customerid: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateFormCreateUser = (
|
export const validateFormCreateUser = (
|
||||||
@ -56,7 +56,7 @@ export const validateFormCreateUser = (
|
|||||||
{ key: 'name', label: 'Name' },
|
{ key: 'name', label: 'Name' },
|
||||||
{ key: 'username', label: 'Username' },
|
{ key: 'username', label: 'Username' },
|
||||||
{ key: 'email', label: 'Email' },
|
{ key: 'email', label: 'Email' },
|
||||||
{ key: 'id_role', label: 'Role' },
|
// { key: 'id_role', label: 'Role' },
|
||||||
{ key: 'status', label: 'Status' },
|
{ key: 'status', label: 'Status' },
|
||||||
{ key: 'password', label: 'Password' },
|
{ key: 'password', label: 'Password' },
|
||||||
{ key: 'retype_password', label: 'Retype Password' }
|
{ key: 'retype_password', label: 'Retype Password' }
|
||||||
@ -65,7 +65,7 @@ export const validateFormCreateUser = (
|
|||||||
{ key: 'name', label: 'Name' },
|
{ key: 'name', label: 'Name' },
|
||||||
{ key: 'username', label: 'Username' },
|
{ key: 'username', label: 'Username' },
|
||||||
{ key: 'email', label: 'Email' },
|
{ key: 'email', label: 'Email' },
|
||||||
{ key: 'id_role', label: 'Role' },
|
// { key: 'id_role', label: 'Role' },
|
||||||
{ key: 'status', label: 'Status' }
|
{ key: 'status', label: 'Status' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -35,10 +35,10 @@ interface SelectedUser {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
role: string;
|
// role: string;
|
||||||
new_password: string;
|
new_password: string;
|
||||||
check_new_password: string;
|
check_new_password: string;
|
||||||
customer: string;
|
// customer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialProps: ContextProps = {
|
const initialProps: ContextProps = {
|
||||||
@ -57,7 +57,7 @@ const initialProps: ContextProps = {
|
|||||||
|
|
||||||
const ManageUserContext = createContext<ContextProps>(initialProps);
|
const ManageUserContext = createContext<ContextProps>(initialProps);
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
const API_URL = apiConfig.service_user;
|
||||||
|
|
||||||
const ManageUserContextProvider = ({ children }: { children: React.ReactNode }) => {
|
const ManageUserContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
/* state */
|
/* state */
|
||||||
@ -98,16 +98,16 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: false
|
enableHiding: false
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
accessorFn: (row) => row.customer?.username,
|
// accessorFn: (row) => row.customer?.username,
|
||||||
id: 'Users.customer',
|
// id: 'Users.customer',
|
||||||
header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />,
|
// header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />,
|
||||||
enableSorting: true,
|
// enableSorting: true,
|
||||||
enableHiding: false,
|
// enableHiding: false,
|
||||||
meta: {
|
// meta: {
|
||||||
headerClassName: 'w-[300px]'
|
// headerClassName: 'w-[300px]'
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
accessorFn: (row) => row.email,
|
accessorFn: (row) => row.email,
|
||||||
id: 'email',
|
id: 'email',
|
||||||
@ -128,23 +128,23 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
headerClassName: 'w-[250px]'
|
headerClassName: 'w-[250px]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
accessorFn: (row) => row.role.name,
|
// accessorFn: (row) => row.role.name,
|
||||||
id: 'Users.role',
|
// id: 'Users.role',
|
||||||
header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
|
// header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
|
||||||
enableSorting: true,
|
// enableSorting: true,
|
||||||
enableHiding: false,
|
// enableHiding: false,
|
||||||
cell: (data: any) => {
|
// cell: (data: any) => {
|
||||||
const { role } = data.row.original;
|
// const { role } = data.row.original;
|
||||||
// console.log('role :', role);
|
// // console.log('role :', role);
|
||||||
let html = <p className="text-danger italic">Unassigned</p>;
|
// let html = <p className="text-danger italic">Unassigned</p>;
|
||||||
if (role && role.name) html = role.name;
|
// if (role && role.name) html = role.name;
|
||||||
return html;
|
// return html;
|
||||||
},
|
// },
|
||||||
meta: {
|
// meta: {
|
||||||
headerClassName: 'w-[350px]'
|
// headerClassName: 'w-[350px]'
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
accessorFn: (row) => row.status,
|
accessorFn: (row) => row.status,
|
||||||
id: 'status',
|
id: 'status',
|
||||||
@ -192,8 +192,8 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
setSelectedUser(row.id);
|
setSelectedUser(row.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<QrCode className="w-4 h-4 mr-2" />
|
{/* <QrCode className="w-4 h-4 mr-2" />
|
||||||
QR Code
|
QR Code */}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleEditDialog(true, row.id)}>
|
<DropdownMenuItem onClick={() => handleEditDialog(true, row.id)}>
|
||||||
<Pencil className="w-4 h-4 mr-2" />
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
@ -223,16 +223,16 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
// ➕ Tambahkan prefix ke field dari tabel Users
|
// ➕ Tambahkan prefix ke field dari tabel Users
|
||||||
const mappedSorting = sorting.map((sort: any) => ({
|
const mappedSorting = sorting.map((sort: any) => ({
|
||||||
...sort,
|
...sort,
|
||||||
id: USER_TABLE_COLUMNS.includes(sort.id) ? `Users.${sort.id}` : sort.id
|
id: USER_TABLE_COLUMNS.includes(sort.id) ? `${sort.id}` : sort.id
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const orderField = mappedSorting[0]?.id ?? 'Users.username';
|
const orderField = mappedSorting[0]?.id ?? 'username';
|
||||||
const orderDirection = mappedSorting[0]?.desc === false ? 'DESC' : 'ASC';
|
const orderDirection = mappedSorting[0]?.desc === false ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
filter =
|
filter =
|
||||||
filter.length == 0
|
filter.length == 0
|
||||||
? {}
|
? {}
|
||||||
: { 'Users.username': { like: `%${filter[0].value?.toLowerCase()}%` } };
|
: { 'username': { like: `%${filter[0].value?.toLowerCase()}%` } };
|
||||||
const response = await GetData(`${API_URL}/user/list`, {
|
const response = await GetData(`${API_URL}/user/list`, {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
page: page + 1,
|
page: page + 1,
|
||||||
@ -241,7 +241,7 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
order_direction: orderDirection,
|
order_direction: orderDirection,
|
||||||
filter: JSON.stringify(filter)
|
filter: JSON.stringify(filter)
|
||||||
});
|
});
|
||||||
// console.log('response api:', response);
|
console.log('response api:', response);
|
||||||
|
|
||||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||||
};
|
};
|
||||||
@ -267,17 +267,12 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
|||||||
pagination={{ size: 10 }}
|
pagination={{ size: 10 }}
|
||||||
toolbar={<ListToolBar />}
|
toolbar={<ListToolBar />}
|
||||||
layout={{ card: true }}
|
layout={{ card: true }}
|
||||||
sorting={[{ id: 'Users.created_at', desc: false }]}
|
sorting={[{ id: 'created_at', desc: false }]}
|
||||||
serverSide={true}
|
serverSide={true}
|
||||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Dialog open={showQr} onOpenChange={setShowQr}>
|
|
||||||
<DialogContent>
|
|
||||||
<GenerateQr idCustomer={selectedUser} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
{children}
|
{children}
|
||||||
</DataGridProvider>
|
</DataGridProvider>
|
||||||
</ManageUserContext.Provider>
|
</ManageUserContext.Provider>
|
||||||
|
|||||||
45
src/pages/sites/manage-sites/ManageSitesPage.tsx
Normal file
45
src/pages/sites/manage-sites/ManageSitesPage.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Container, DataGridInner } from '@/components';
|
||||||
|
import { EditDialog } from './blocks';
|
||||||
|
import { ManageSitesContextProvider } from './hooks';
|
||||||
|
import { AddDialog } from './blocks/AddDialog';
|
||||||
|
import { DeleteDialog } from './blocks/DeleteDialog';
|
||||||
|
import { Breadcrumbs, Link } from '@mui/material';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
export default function ManageSitesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>REVENUE | Manage Sites</title>
|
||||||
|
</Helmet>
|
||||||
|
<ManageSitesContextProvider>
|
||||||
|
<Container>
|
||||||
|
<h1 className="text-xl font-medium leading-none text-gray-900 mb-3">Manage Sites</h1>
|
||||||
|
<Breadcrumbs sx={{ mb: 2 }}>
|
||||||
|
<Link underline="none" color="inherit" href="/">
|
||||||
|
<span className="text-sm hover:underline">Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* <Link underline="none" color="inherit">
|
||||||
|
<span className="text-sm">Settings</span>
|
||||||
|
</Link> */}
|
||||||
|
|
||||||
|
{/* <Link underline="none" color="inherit">
|
||||||
|
<span className="text-sm">User Management</span>
|
||||||
|
</Link> */}
|
||||||
|
|
||||||
|
<Link underline="none" color="inherit">
|
||||||
|
<span className="text-sm">Manage Sites</span>
|
||||||
|
</Link>
|
||||||
|
</Breadcrumbs>
|
||||||
|
<div className="grid gap-5 lg:gap-7.5">
|
||||||
|
<DataGridInner />
|
||||||
|
</div>
|
||||||
|
<EditDialog />
|
||||||
|
<AddDialog />
|
||||||
|
<DeleteDialog />
|
||||||
|
</Container>
|
||||||
|
</ManageSitesContextProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
245
src/pages/sites/manage-sites/blocks/AddDialog.tsx
Normal file
245
src/pages/sites/manage-sites/blocks/AddDialog.tsx
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { useSitesContext } from '../hooks';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { KeenIcon, useDataGrid } from '@/components';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useCallApi } from '@/hooks';
|
||||||
|
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||||
|
import {
|
||||||
|
initialStateCreateSites,
|
||||||
|
validateFormCreateSites
|
||||||
|
} from './Types';
|
||||||
|
|
||||||
|
const API_URL = apiConfig.service_master_data;
|
||||||
|
|
||||||
|
const AddDialog = () => {
|
||||||
|
const parentRef = useRef<any | null>(null);
|
||||||
|
const { showAddDialog, handleAddDialog } = useSitesContext();
|
||||||
|
const { reload } = useDataGrid();
|
||||||
|
const { PostData } = useCallApi();
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const [formField, setFormField] = useState(initialStateCreateSites);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormField(initialStateCreateSites);
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* actions */
|
||||||
|
const doCreateSites = useCallback(
|
||||||
|
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await PostData(`${API_URL}/site-points/create`, formField);
|
||||||
|
|
||||||
|
if (response?.status) {
|
||||||
|
handleAddDialog(false);
|
||||||
|
resetForm();
|
||||||
|
reload();
|
||||||
|
const createActivity = {
|
||||||
|
module: 'Manage Sites',
|
||||||
|
description: `Create New Sites => ${formField.location}`,
|
||||||
|
action: 'C'
|
||||||
|
};
|
||||||
|
|
||||||
|
doSaveLogActivity(createActivity);
|
||||||
|
toast.success('Success Create Site');
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
console.log(err);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formField]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
if (!validateFormCreateSites(formField, setErrors, 'create')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doCreateSites(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAddDialog === false) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}, [showAddDialog]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showAddDialog} onOpenChange={(open) => handleAddDialog(open)}>
|
||||||
|
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-10 overflow-hidden [&>button]:hidden">
|
||||||
|
<DialogHeader className="p-0 border-0">
|
||||||
|
<DialogTitle></DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
<div className="flex items-center justify-between flex-wrap grow">
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h1 className="text-xl font-semibold leading-none text-gray-900">Site - Create</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer hover:opacity-100 opacity-50"
|
||||||
|
onClick={() => {
|
||||||
|
handleAddDialog(false);
|
||||||
|
resetForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeenIcon icon="cross" className="text-1.5xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody className="scrollable-y p-5 pb-0" ref={parentRef}>
|
||||||
|
<form onSubmit={handleSubmit} className="grid gap-5">
|
||||||
|
{/* Cell */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Cell</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.cell ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.cell}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, cell: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, cell: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.cell && <span className="text-red-500 text-xs mt-1">{errors.cell}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sector */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Sector</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.sector ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.sector}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, sector: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, sector: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.sector && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.sector}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Site</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.site ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.site}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, site: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, site: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.site && <span className="text-red-500 text-xs mt-1">{errors.site}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Latitude */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Latitude</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.lat ? 'border-red-500' : ''}`}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.lat}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
const value = target.value === '' ? 0 : Number(target.value);
|
||||||
|
setFormField((prev) => ({ ...prev, lat: value }));
|
||||||
|
setErrors((prev) => ({ ...prev, lat: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.lat && <span className="text-red-500 text-xs mt-1">{errors.lat}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Longitude */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Longitude</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.lng ? 'border-red-500' : ''}`}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.lng}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
const value = target.value === '' ? 0 : Number(target.value);
|
||||||
|
setFormField((prev) => ({ ...prev, lng: value }));
|
||||||
|
setErrors((prev) => ({ ...prev, lng: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.lng && <span className="text-red-500 text-xs mt-1">{errors.lng}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Location</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.location ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.location}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, location: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, location: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.location && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.location}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-end pt-2.5">
|
||||||
|
<Button className="btn btn-primary" type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AddDialog };
|
||||||
92
src/pages/sites/manage-sites/blocks/DeleteDialog.tsx
Normal file
92
src/pages/sites/manage-sites/blocks/DeleteDialog.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { useSitesContext } from '../hooks';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Alert, useDataGrid } from '@/components';
|
||||||
|
import { ChangeEvent, useCallback, useState } from 'react';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useCallApi } from '@/hooks';
|
||||||
|
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||||
|
import { EnforceSwitch } from '@/components/switch';
|
||||||
|
|
||||||
|
const API_URL = apiConfig.service_master_data;
|
||||||
|
|
||||||
|
const DeleteDialog = () => {
|
||||||
|
const { showDeleteDialog, handleDeleteDialog, selectedSites } = useSitesContext();
|
||||||
|
const { reload } = useDataGrid();
|
||||||
|
const [enforce, setEnforce] = useState(false);
|
||||||
|
|
||||||
|
const { DeleteData } = useCallApi();
|
||||||
|
const [alert, setAlert] = useState({
|
||||||
|
show: false,
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/* actions */
|
||||||
|
const doDeleteData = useCallback(async () => {
|
||||||
|
const response = await DeleteData(`${API_URL}/site-points/delete/${selectedSites?.id}/false`, {
|
||||||
|
id: selectedSites?.id
|
||||||
|
});
|
||||||
|
if (response?.status) {
|
||||||
|
setAlert((prev) => ({ ...prev, show: false, message: '' }));
|
||||||
|
handleDeleteDialog(false, null);
|
||||||
|
toast.success('Success Delete Sites');
|
||||||
|
reload();
|
||||||
|
const createActivity = {
|
||||||
|
module: 'Manage Sites',
|
||||||
|
description: `Delete Sites => ${selectedSites}`,
|
||||||
|
action: 'D'
|
||||||
|
};
|
||||||
|
|
||||||
|
doSaveLogActivity(createActivity);
|
||||||
|
} else {
|
||||||
|
setAlert((prev) => ({ ...prev, show: true, message: response?.message }));
|
||||||
|
}
|
||||||
|
}, [selectedSites, DeleteData, handleDeleteDialog, reload, enforce]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showDeleteDialog} onOpenChange={(open) => handleDeleteDialog(open, null)}>
|
||||||
|
<DialogContent className="container-fixed max-w-md flex flex-col p-5 overflow-hidden [&>button]:hidden">
|
||||||
|
<DialogHeader className="p-0 border-0 block">
|
||||||
|
<DialogTitle></DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
<Alert variant="warning">
|
||||||
|
<h3 className="text-lg">Are you sure?</h3>
|
||||||
|
<span className="text-sm">you will delete this data!</span>
|
||||||
|
{/* <div className="mt-2 flex items-center gap-x-2">
|
||||||
|
<label className="form-label max-w-56">Hard Delete</label>
|
||||||
|
<EnforceSwitch
|
||||||
|
enforce={enforce}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEnforce(e.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
</Alert>
|
||||||
|
{alert.show && (
|
||||||
|
<Alert variant="danger">
|
||||||
|
<h3>{alert.message}</h3>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter className="flex justify-end items-center gap-4 mt-3">
|
||||||
|
<Button variant={'outline'} onClick={() => handleDeleteDialog(false, null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant={'destructive'} onClick={() => doDeleteData()}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DeleteDialog };
|
||||||
261
src/pages/sites/manage-sites/blocks/EditDialog.tsx
Normal file
261
src/pages/sites/manage-sites/blocks/EditDialog.tsx
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { useSitesContext } from '../hooks';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { KeenIcon, useDataGrid } from '@/components';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useCallApi } from '@/hooks';
|
||||||
|
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||||
|
import {
|
||||||
|
initialStateCreateSites,
|
||||||
|
validateFormCreateSites
|
||||||
|
} from './Types';
|
||||||
|
|
||||||
|
const API_URL = apiConfig.service_master_data;
|
||||||
|
|
||||||
|
const EditDialog = () => {
|
||||||
|
const parentRef = useRef<any | null>(null);
|
||||||
|
const { showEditDialog, selectedSites, handleEditDialog } = useSitesContext();
|
||||||
|
const { reload } = useDataGrid();
|
||||||
|
const { PutData } = useCallApi();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [formField, setFormField] = useState(initialStateCreateSites);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFormField(initialStateCreateSites);
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* actions */
|
||||||
|
const doUpdateSites = useCallback(
|
||||||
|
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
if (!validateFormCreateSites(formField, setErrors, 'update')) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await PutData(`${API_URL}/site-points/update/${selectedSites?.id}`, {
|
||||||
|
...formField
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.status) {
|
||||||
|
resetForm();
|
||||||
|
handleEditDialog(false, null);
|
||||||
|
toast.success('Success Update Site');
|
||||||
|
reload();
|
||||||
|
const createActivity = {
|
||||||
|
module: 'Manage Sites',
|
||||||
|
description: `Edit Site => ${formField.location}`,
|
||||||
|
action: 'U'
|
||||||
|
};
|
||||||
|
|
||||||
|
doSaveLogActivity(createActivity);
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Something went wrong, please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedSites, formField]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showEditDialog === false) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}, [showEditDialog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSites) {
|
||||||
|
setFormField((prev) => ({
|
||||||
|
...prev,
|
||||||
|
cell: selectedSites.cell ?? '',
|
||||||
|
sector: selectedSites.sector ?? '',
|
||||||
|
site: selectedSites.site ?? '',
|
||||||
|
lat: selectedSites.lat ?? 0,
|
||||||
|
lng: selectedSites.lng ?? 0,
|
||||||
|
location: selectedSites.location ?? ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [selectedSites]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
|
||||||
|
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-5 overflow-hidden [&>button]:hidden">
|
||||||
|
<DialogHeader className="p-0 border-0">
|
||||||
|
<DialogTitle></DialogTitle>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
<div className="flex items-center justify-between flex-wrap grow">
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h1 className="text-xl font-semibold leading-none text-gray-900">Site - Update</h1>
|
||||||
|
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer hover:opacity-100 opacity-50"
|
||||||
|
onClick={() => handleEditDialog(false, null)}
|
||||||
|
>
|
||||||
|
<KeenIcon icon="cross" className="text-1.5xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogBody className="scrollable-y px-0 pb-0" ref={parentRef}>
|
||||||
|
<div className="flex flex-col px-0">
|
||||||
|
<form onSubmit={doUpdateSites}>
|
||||||
|
<div className="card-body grid gap-5 p-0">
|
||||||
|
{/* Cell */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Cell</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.cell ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.cell}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, cell: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, cell: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.cell && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.cell}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sector */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Sector</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.sector ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.sector}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, sector: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, sector: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.sector && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.sector}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Site</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.site ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.site}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, site: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, site: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.site && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.site}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Latitude */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Latitude</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.lat ? 'border-red-500' : ''}`}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.lat}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
const value = target.value === '' ? 0 : Number(target.value);
|
||||||
|
setFormField((prev) => ({ ...prev, lat: value }));
|
||||||
|
setErrors((prev) => ({ ...prev, lat: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.lat && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.lat}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Longitude */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Longitude</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.lng ? 'border-red-500' : ''}`}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.lng}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
const value = target.value === '' ? 0 : Number(target.value);
|
||||||
|
setFormField((prev) => ({ ...prev, lng: value }));
|
||||||
|
setErrors((prev) => ({ ...prev, lng: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.lng && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.lng}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location */}
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<label className="form-label flex items-center gap-1 max-w-56">Location</label>
|
||||||
|
<div className="grow flex flex-col">
|
||||||
|
<Input
|
||||||
|
className={`input ${errors.location ? 'border-red-500' : ''}`}
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={formField.location}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
setFormField((prev) => ({ ...prev, location: target.value }));
|
||||||
|
setErrors((prev) => ({ ...prev, location: '' }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.location && (
|
||||||
|
<span className="text-red-500 text-xs mt-1">{errors.location}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-2.5">
|
||||||
|
<Button className="btn btn-primary" type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EditDialog };
|
||||||
76
src/pages/sites/manage-sites/blocks/ListToolBar.tsx
Normal file
76
src/pages/sites/manage-sites/blocks/ListToolBar.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
|
||||||
|
import { useSitesContext } from '../hooks';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const ListToolBar = () => {
|
||||||
|
const { table, reload } = useDataGrid();
|
||||||
|
const { handleAddDialog } = useSitesContext();
|
||||||
|
const [searchValue, setSearchValue] = useState<string>(
|
||||||
|
(table.getColumn('cell')?.getFilterValue() as string) ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
table.getColumn('cell')?.setFilterValue(searchValue);
|
||||||
|
table.setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
table.getColumn('cell')?.setFilterValue(searchValue);
|
||||||
|
table.setPageIndex(0);
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchValue, table]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
|
||||||
|
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
|
||||||
|
<div className="flex justify-between w-full items-center">
|
||||||
|
<div className="flex w-[50%] gap-3 items-center">
|
||||||
|
<label className="input input-sm w-1/3 overflow-hidden">
|
||||||
|
<KeenIcon icon="magnifier" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Cell"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(event) => setSearchValue(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{/* <DefaultTooltip title={'Search'} placement={'top'}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-7.5 disabled:bg-gray-400"
|
||||||
|
onClick={handleSearch}
|
||||||
|
>
|
||||||
|
<KeenIcon icon="magnifier" />
|
||||||
|
</Button>
|
||||||
|
</DefaultTooltip> */}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-7.5 text-[0.8rem]"
|
||||||
|
onClick={() => handleAddDialog(true)}
|
||||||
|
>
|
||||||
|
Add Data
|
||||||
|
</Button>
|
||||||
|
<DefaultTooltip title={'Refresh'} placement={'top'}>
|
||||||
|
<Button variant="outline" className="h-7.5" onClick={() => reload()}>
|
||||||
|
<KeenIcon icon="arrows-circle" />
|
||||||
|
</Button>
|
||||||
|
</DefaultTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ListToolBar };
|
||||||
69
src/pages/sites/manage-sites/blocks/Types.ts
Normal file
69
src/pages/sites/manage-sites/blocks/Types.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export interface CreateSitesrParams {
|
||||||
|
cell: string;
|
||||||
|
sector: string;
|
||||||
|
site: string;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialStateCreateSites: {
|
||||||
|
cell: string;
|
||||||
|
sector: string;
|
||||||
|
site: string;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
location: string;
|
||||||
|
} = {
|
||||||
|
cell: '',
|
||||||
|
sector: '',
|
||||||
|
site: '',
|
||||||
|
lat: 0,
|
||||||
|
lng: 0,
|
||||||
|
location: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateFormCreateSites = (
|
||||||
|
formField: typeof initialStateCreateSites,
|
||||||
|
setErrors: React.Dispatch<React.SetStateAction<Record<string, string>>>,
|
||||||
|
mode: 'create' | 'update' = 'create'
|
||||||
|
) => {
|
||||||
|
const requiredFields =
|
||||||
|
mode === 'create'
|
||||||
|
? [
|
||||||
|
{ key: 'cell', label: 'Cell' },
|
||||||
|
{ key: 'sector', label: 'Sector' },
|
||||||
|
{ key: 'site', label: 'Site' },
|
||||||
|
{ key: 'lat', label: 'Lat' },
|
||||||
|
{ key: 'lng', label: 'Longitude' },
|
||||||
|
{ key: 'lat', label: 'Latitude' }
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ key: 'cell', label: 'Cell' },
|
||||||
|
{ key: 'sector', label: 'Sector' },
|
||||||
|
{ key: 'site', label: 'Site' },
|
||||||
|
{ key: 'lat', label: 'Lat' },
|
||||||
|
{ key: 'lng', label: 'Longitude' },
|
||||||
|
{ key: 'lat', label: 'Latitude' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
requiredFields.forEach(({ key, label }) => {
|
||||||
|
if (
|
||||||
|
formField[key as keyof typeof formField] === '' ||
|
||||||
|
formField[key as keyof typeof formField] === null ||
|
||||||
|
formField[key as keyof typeof formField] === undefined
|
||||||
|
) {
|
||||||
|
newErrors[key] = `${label} is required`;
|
||||||
|
toast.error(`${label} is required`);
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
2
src/pages/sites/manage-sites/blocks/index.ts
Normal file
2
src/pages/sites/manage-sites/blocks/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ListToolBar';
|
||||||
|
export * from './EditDialog';
|
||||||
246
src/pages/sites/manage-sites/hooks/ManageSitesContext.tsx
Normal file
246
src/pages/sites/manage-sites/hooks/ManageSitesContext.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { apiConfig } from '@/config/api.config';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components';
|
||||||
|
import { EnforceSwitch } from '@/components/switch';
|
||||||
|
import { ListToolBar } from '../blocks';
|
||||||
|
import { useCallApi } from '@/hooks';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { MoreHorizontal, MoreVertical, Pencil, QrCode, Trash } from 'lucide-react';
|
||||||
|
import GenerateQr from '@/pages/account/home/user-profile/blocks/GenerateQr';
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
|
||||||
|
// Objek data lengkap satu site (dipakai untuk Edit, Delete, dan QR)
|
||||||
|
interface SelectedSites {
|
||||||
|
id: string;
|
||||||
|
cell: string;
|
||||||
|
sector: string;
|
||||||
|
site: string;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextProps {
|
||||||
|
showSearchDialog: boolean;
|
||||||
|
handleSearchDialog: (show: boolean) => void;
|
||||||
|
showEditDialog: boolean;
|
||||||
|
handleEditDialog: (show: boolean, selected_sites: SelectedSites | null) => void;
|
||||||
|
showAddDialog: boolean;
|
||||||
|
handleAddDialog: (show: boolean) => void;
|
||||||
|
showDeleteDialog: boolean;
|
||||||
|
handleDeleteDialog: (show: boolean, selected_sites: SelectedSites | null) => void;
|
||||||
|
selectedSites: SelectedSites | null;
|
||||||
|
showQr: boolean;
|
||||||
|
setShowQr: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialProps: ContextProps = {
|
||||||
|
showSearchDialog: false,
|
||||||
|
handleSearchDialog: (show: boolean) => { },
|
||||||
|
showEditDialog: false,
|
||||||
|
handleEditDialog: () => { },
|
||||||
|
showAddDialog: false,
|
||||||
|
handleAddDialog: () => { },
|
||||||
|
showDeleteDialog: false,
|
||||||
|
handleDeleteDialog: () => { },
|
||||||
|
selectedSites: null,
|
||||||
|
showQr: false,
|
||||||
|
setShowQr: () => { }
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageSitesContext = createContext<ContextProps>(initialProps);
|
||||||
|
|
||||||
|
const API_URL = apiConfig.service_master_data;
|
||||||
|
|
||||||
|
const ManageSitesContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
/* state */
|
||||||
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||||
|
const [showSearchDialog, setShowSearchDialog] = useState(false);
|
||||||
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [showQr, setShowQr] = useState(false);
|
||||||
|
const [selectedSites, setselectedSites] = useState<SelectedSites | null>(null);
|
||||||
|
const { GetData } = useCallApi();
|
||||||
|
|
||||||
|
/* action */
|
||||||
|
const handleSearchDialog = useCallback((show: boolean) => {
|
||||||
|
setShowSearchDialog(show);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditDialog = useCallback((show: boolean, selected_sites: SelectedSites | null) => {
|
||||||
|
setselectedSites(show ? selected_sites : null);
|
||||||
|
setShowEditDialog(show);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddDialog = useCallback((show: boolean) => {
|
||||||
|
setShowAddDialog(show);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteDialog = useCallback((show: boolean, selected_sites: SelectedSites | null) => {
|
||||||
|
setselectedSites(show ? selected_sites : null);
|
||||||
|
setShowDeleteDialog(show);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/* Data Grid Options */
|
||||||
|
const columns = useMemo<ColumnDef<any>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'cell',
|
||||||
|
id: 'cell',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="cell" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sector',
|
||||||
|
id: 'sector',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="sector" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'site',
|
||||||
|
id: 'site',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="site" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'lat',
|
||||||
|
id: 'lat',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="lat" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'lng',
|
||||||
|
id: 'lng',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="lng" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'location',
|
||||||
|
id: 'location',
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="location" column={column} />,
|
||||||
|
enableSorting: true,
|
||||||
|
enableHiding: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="Action" column={column} />,
|
||||||
|
cell: (data: any) => {
|
||||||
|
const row = data.row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="btn btn-sm btn-icon btn-light">
|
||||||
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowQr(true);
|
||||||
|
setselectedSites(row);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleEditDialog(true, row)}>
|
||||||
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDeleteDialog(true, row)}>
|
||||||
|
<Trash className="w-4 h-4 mr-2 text-red-500" />
|
||||||
|
<span className="text-red-500">Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
headerClassName: 'w-[100px]',
|
||||||
|
cellClassName: 'text-center'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[handleEditDialog, handleDeleteDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const doGetListData = async (page: number, limit: number, sorting: any, filter: any) => {
|
||||||
|
const USER_TABLE_COLUMNS = ['cell', 'sector', 'site', 'lat', 'long', 'location'];
|
||||||
|
|
||||||
|
// ➕ Tambahkan prefix ke field dari tabel Users
|
||||||
|
const mappedSorting = sorting.map((sort: any) => ({
|
||||||
|
...sort,
|
||||||
|
id: USER_TABLE_COLUMNS.includes(sort.id) ? `${sort.id}` : sort.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
const orderField = mappedSorting[0]?.id ?? 'location';
|
||||||
|
const orderDirection = mappedSorting[0]?.desc === false ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
|
filter =
|
||||||
|
filter.length == 0
|
||||||
|
? {}
|
||||||
|
: { 'cell': { like: `%${filter[0].value?.toLowerCase()}%` } };
|
||||||
|
const response = await GetData(`${API_URL}/site-points/list`, {
|
||||||
|
limit: limit,
|
||||||
|
page: page + 1,
|
||||||
|
with_deleted: false,
|
||||||
|
order_field: orderField,
|
||||||
|
order_direction: orderDirection,
|
||||||
|
filter: JSON.stringify(filter)
|
||||||
|
});
|
||||||
|
// console.log('response api:', response);
|
||||||
|
|
||||||
|
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ManageSitesContext.Provider
|
||||||
|
value={{
|
||||||
|
showSearchDialog,
|
||||||
|
handleSearchDialog,
|
||||||
|
showEditDialog,
|
||||||
|
handleEditDialog,
|
||||||
|
selectedSites,
|
||||||
|
showAddDialog,
|
||||||
|
handleAddDialog,
|
||||||
|
showDeleteDialog,
|
||||||
|
handleDeleteDialog,
|
||||||
|
showQr,
|
||||||
|
setShowQr
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DataGridProvider
|
||||||
|
columns={columns}
|
||||||
|
pagination={{ size: 10 }}
|
||||||
|
toolbar={<ListToolBar />}
|
||||||
|
layout={{ card: true }}
|
||||||
|
sorting={[{ id: 'created_at', desc: false }]}
|
||||||
|
serverSide={true}
|
||||||
|
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||||
|
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DataGridProvider>
|
||||||
|
</ManageSitesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ManageSitesContextProvider, ManageSitesContext };
|
||||||
|
export type { SelectedSites };
|
||||||
2
src/pages/sites/manage-sites/hooks/index.ts
Normal file
2
src/pages/sites/manage-sites/hooks/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './ManageSitesContext';
|
||||||
|
export * from './useManageSitesContext';
|
||||||
12
src/pages/sites/manage-sites/hooks/useManageSitesContext.tsx
Normal file
12
src/pages/sites/manage-sites/hooks/useManageSitesContext.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { ManageSitesContext } from './ManageSitesContext';
|
||||||
|
|
||||||
|
const useSitesContext = () => {
|
||||||
|
const context = useContext(ManageSitesContext);
|
||||||
|
|
||||||
|
if (!context) throw new Error('useSitesContext must be used within AuthProvider');
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useSitesContext };
|
||||||
1
src/pages/sites/manage-sites/index.ts
Normal file
1
src/pages/sites/manage-sites/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './ManageSitesPage';
|
||||||
@ -45,9 +45,9 @@ const DropdownUser = ({ menuItemRef }: IDropdownUserProps) => {
|
|||||||
alt=""
|
alt=""
|
||||||
/> */}
|
/> */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-[14px] text-gray-800 font-semibold">{auth?.user.username}</p>
|
<p className="text-[14px] text-gray-800 font-semibold">{auth?.user?.username}</p>
|
||||||
<p className="text-xs text-gray-600 hover:text-primary font-medium leading-none">
|
<p className="text-xs text-gray-600 hover:text-primary font-medium leading-none">
|
||||||
{auth?.user.email}
|
{auth?.user?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { ErrorsRouting } from '@/errors';
|
|||||||
|
|
||||||
import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage';
|
import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage';
|
||||||
import ManageUserPage from '@/pages/settings/user/manage-user/ManageUserPage';
|
import ManageUserPage from '@/pages/settings/user/manage-user/ManageUserPage';
|
||||||
|
import ManageSitesPage from '@/pages/sites/manage-sites/ManageSitesPage';
|
||||||
|
|
||||||
import Transaction from '@/pages/transaction/history-transaction/Transaction';
|
import Transaction from '@/pages/transaction/history-transaction/Transaction';
|
||||||
import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction';
|
import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction';
|
||||||
import TransactionTopup from '@/pages/transaction/topup/TransactionTopup';
|
import TransactionTopup from '@/pages/transaction/topup/TransactionTopup';
|
||||||
@ -54,7 +56,7 @@ import WithdrawalEmoney from '@/pages/transaction/withdrawl-emoney/WithdrawalEmo
|
|||||||
import ManageCards from '@/pages/cards/manage-card/ManageCard';
|
import ManageCards from '@/pages/cards/manage-card/ManageCard';
|
||||||
import HistoryCard from '@/pages/cards/history-card/HistoryCard';
|
import HistoryCard from '@/pages/cards/history-card/HistoryCard';
|
||||||
|
|
||||||
// DISBURSEMENT
|
import ComingSoonPage from '@/pages/ComingSoonPage';
|
||||||
|
|
||||||
const AppRoutingSetup = (): ReactElement => {
|
const AppRoutingSetup = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
@ -62,8 +64,13 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
<Route element={<RequireAuth />}>
|
<Route element={<RequireAuth />}>
|
||||||
<Route element={<Demo2Layout />}>
|
<Route element={<Demo2Layout />}>
|
||||||
<Route path="/" element={<DashboardHomePage />} />
|
<Route path="/" element={<DashboardHomePage />} />
|
||||||
|
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
|
||||||
|
<Route path="/coming-soon" element={<ComingSoonPage />} />
|
||||||
|
<Route path="/settings/user-management/manage-user" element={<ManageUserPage />} />
|
||||||
|
|
||||||
<Route path="/master-data" element={<MasterData />} />
|
<Route path="/sites/manage-sites" element={<ManageSitesPage />} />
|
||||||
|
|
||||||
|
{/* <Route path="/master-data" element={<MasterData />} />
|
||||||
<Route path="/master-data/municipios" element={<Municipios />} />
|
<Route path="/master-data/municipios" element={<Municipios />} />
|
||||||
<Route path="/master-data/postoadms" element={<PostoAdmsMaster />} />
|
<Route path="/master-data/postoadms" element={<PostoAdmsMaster />} />
|
||||||
<Route path="/master-data/sucos" element={<SucosMaster />} />
|
<Route path="/master-data/sucos" element={<SucosMaster />} />
|
||||||
@ -87,7 +94,7 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
|
|
||||||
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
|
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
|
||||||
|
|
||||||
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
|
|
||||||
|
|
||||||
<Route path="/groups/group-management" element={<ManageGroups />} />
|
<Route path="/groups/group-management" element={<ManageGroups />} />
|
||||||
|
|
||||||
@ -98,10 +105,10 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
|
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
|
||||||
<Route path="/members/agent-balance" element={<AgentBalance />} />
|
<Route path="/members/agent-balance" element={<AgentBalance />} />
|
||||||
<Route path="/members/search-member" element={<SearchMember />} />
|
<Route path="/members/search-member" element={<SearchMember />} />
|
||||||
<Route path="/members/search-member" element={<SearchMember />} />
|
<Route path="/members/search-member" element={<SearchMember />} /> */}
|
||||||
|
|
||||||
{/* cards */}
|
{/* cards */}
|
||||||
<Route path="/cards/manage-card" element={<ManageCards />} />
|
{/* <Route path="/cards/manage-card" element={<ManageCards />} />
|
||||||
|
|
||||||
<Route path="/cards/card-history" element={<HistoryCard />} />
|
<Route path="/cards/card-history" element={<HistoryCard />} />
|
||||||
|
|
||||||
@ -126,18 +133,17 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
<Route path="/menu/welcome" element={<Welcome />} />
|
<Route path="/menu/welcome" element={<Welcome />} />
|
||||||
<Route path="/message/inbox" element={<Inbox />} />
|
<Route path="/message/inbox" element={<Inbox />} />
|
||||||
<Route path="/webservice/webservice-management" element={<ManageWebServices />} />
|
<Route path="/webservice/webservice-management" element={<ManageWebServices />} />
|
||||||
<Route path="/settings/user-management/manage-user" element={<ManageUserPage />} />
|
|
||||||
<Route path="/settings/user-management/log-activity" element={<LogActivityPage />} />
|
<Route path="/settings/user-management/log-activity" element={<LogActivityPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/settings/user-management/manage-position"
|
path="/settings/user-management/manage-position"
|
||||||
element={<ManagePositionPage />}
|
element={<ManagePositionPage />}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* DISBURSEMENT */}
|
{/* DISBURSEMENT */}
|
||||||
<Route
|
{/* <Route
|
||||||
path="/disbursement/transaction-history"
|
path="/disbursement/transaction-history"
|
||||||
element={<HistoryTransactionDisbursement />}
|
element={<HistoryTransactionDisbursement />}
|
||||||
/>
|
/> */}
|
||||||
{/* DISBURSEMENT */}
|
{/* DISBURSEMENT */}
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"types": ["google.maps", "vite/client"],
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
Reference in New Issue
Block a user