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
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
# VITE_APP_API_URL=
|
||||
# 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_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
||||
# 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
|
||||
GENERATE_SOURCEMAP=false
|
||||
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
|
||||
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_BASE_URL=/dashboard/
|
||||
VITE_ENV=staging
|
||||
@ -2,8 +2,8 @@
|
||||
<html class="h-full" lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/media/app/favicon.ico" />
|
||||
<!-- <link rel="icon" href="/media/app/app-logo.png.png" /> -->
|
||||
<!-- <link rel="icon" href="/media/app/favicon.ico" /> -->
|
||||
<link rel="icon" href="/media/app/logo-tt.jpeg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"@firebase/firestore": "^4.7.4",
|
||||
"@formatjs/intl-pluralrules": "^5.3.4",
|
||||
"@formatjs/intl-relativetimeformat": "^11.4.4",
|
||||
"@googlemaps/markerclusterer": "^2.6.2",
|
||||
"@mui/base": "5.0.0-beta.40",
|
||||
"@mui/icons-material": "^6.4.6",
|
||||
"@mui/material": "^6.1.6",
|
||||
@ -59,6 +60,7 @@
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lucide-react": "^0.456.0",
|
||||
"metronic-tailwind-react": "file:",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
@ -97,7 +99,9 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@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/papaparse": "^5.3.16",
|
||||
"@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 location = useLocation();
|
||||
|
||||
// console.log('RequireAuth render: loading =', loading, ' auth =', auth);
|
||||
|
||||
if (loading) {
|
||||
return <ScreenLoader />;
|
||||
}
|
||||
|
||||
@ -2,14 +2,21 @@ import { type TLanguageCode } from '@/i18n';
|
||||
|
||||
export interface AuthModel {
|
||||
id: string;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
// access_token: string;
|
||||
// refresh_token: string;
|
||||
role_name: string;
|
||||
roles_list: [];
|
||||
company: any;
|
||||
user: any;
|
||||
// api_token: string;
|
||||
statusbalance: string;
|
||||
// roles_list: [];
|
||||
// company: any;
|
||||
// user: any;
|
||||
// // api_token: string;
|
||||
// statusbalance: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
user?: UserModel;
|
||||
statusbalance?: string;
|
||||
access_token?: string;
|
||||
token_type?: string;
|
||||
// language?: TLanguageCode;
|
||||
}
|
||||
|
||||
export interface UserModel {
|
||||
@ -18,18 +25,21 @@ export interface UserModel {
|
||||
name: string;
|
||||
password: string | undefined;
|
||||
email: string;
|
||||
id_role: string;
|
||||
customer?: {
|
||||
id: string;
|
||||
};
|
||||
// id_role: string;
|
||||
role_name: string;
|
||||
roles_list: [];
|
||||
token: AuthModel;
|
||||
// roles_list: [];
|
||||
// token: AuthModel;
|
||||
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
fullname?: string;
|
||||
occupation?: string;
|
||||
companyName?: string;
|
||||
phone?: string;
|
||||
roles?: number[];
|
||||
pic?: string;
|
||||
language?: TLanguageCode;
|
||||
// first_name: string;
|
||||
// last_name: string;
|
||||
// fullname?: string;
|
||||
// occupation?: string;
|
||||
// companyName?: string;
|
||||
// phone?: string;
|
||||
// roles?: number[];
|
||||
// pic?: string;
|
||||
// language?: TLanguageCode;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ const Login = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TPAY | Sign In</title>
|
||||
<title>REVENUE | Sign In</title>
|
||||
</Helmet>
|
||||
<div className="card max-w-[390px] w-full">
|
||||
<form
|
||||
@ -145,54 +145,6 @@ const Login = () => {
|
||||
)}
|
||||
</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
|
||||
type="submit"
|
||||
className="btn btn-primary flex justify-center grow"
|
||||
@ -202,7 +154,7 @@ const Login = () => {
|
||||
</button>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -14,12 +14,10 @@ import { type AuthModel, type UserModel } from '@/auth';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
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 FORGOT_PASSWORD_URL = `${API_URL}/reset_password`;
|
||||
export const RESET_PASSWORD_URL = `${API_URL}/update_password`;
|
||||
export const GET_USER_URL = `${API_URL}/user/detail`;
|
||||
export const GET_USER_URL = `${API_URL}/user/profile`;
|
||||
|
||||
interface AuthContextProps {
|
||||
loading: boolean;
|
||||
@ -29,9 +27,6 @@ interface AuthContextProps {
|
||||
currentUser: UserModel | undefined;
|
||||
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
|
||||
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;
|
||||
verify: () => Promise<void>;
|
||||
}
|
||||
@ -55,15 +50,13 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
const login = async (username: string, password: string, token?: string) => {
|
||||
try {
|
||||
const { data: auth } = await axios
|
||||
.post(LOGIN_URL, { username, password, token })
|
||||
.post(LOGIN_URL, { username, password })
|
||||
.then((response) => response.data);
|
||||
|
||||
const enhancedAuth: AuthModel = {
|
||||
...auth.token,
|
||||
id: auth.user.id,
|
||||
role_name: auth.role_name,
|
||||
id: auth.userid,
|
||||
user: auth.user,
|
||||
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
|
||||
};
|
||||
|
||||
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 () => {
|
||||
let _axios = await axios
|
||||
.get(`${GET_USER_URL}/${authHelper.getAuth()?.id}`)
|
||||
.get(`${GET_USER_URL}/${authHelper.getAuth()?.user?.id}`)
|
||||
.then((response) => response.data.data);
|
||||
|
||||
return { data: _axios };
|
||||
@ -105,18 +87,14 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
try {
|
||||
const { data: user } = await getUser();
|
||||
|
||||
// Perbarui auth yang sekarang dengan statusbalance
|
||||
saveAuth({
|
||||
...auth,
|
||||
statusbalance: user.role.status_balance
|
||||
});
|
||||
|
||||
const createCacheUser = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
role_name: auth.role_name,
|
||||
statusbalance: user.role.status_balance
|
||||
};
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(createCacheUser));
|
||||
@ -149,9 +127,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
login,
|
||||
requestPasswordResetLink,
|
||||
changePassword,
|
||||
getUser,
|
||||
logout,
|
||||
verify
|
||||
}}
|
||||
|
||||
@ -7,7 +7,7 @@ const LoaderTransparant = () => {
|
||||
<img
|
||||
// className="h-[30px] max-w-none"
|
||||
className="h-14 w-auto drop-shadow-md"
|
||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
||||
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||
alt="logo"
|
||||
/>
|
||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||
|
||||
@ -7,7 +7,7 @@ const ScreenLoader = () => {
|
||||
<img
|
||||
// className="h-[30px] max-w-none"
|
||||
className="h-14 w-auto drop-shadow-md"
|
||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
||||
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||
alt="logo"
|
||||
/>
|
||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||
|
||||
@ -1,36 +1,41 @@
|
||||
interface apiConfigProps {
|
||||
service_dashboard: string;
|
||||
service_customer: string;
|
||||
service_user: 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_MASTER_DATA = import.meta.env.VITE_APP_API_URL_MASTER_DATA;
|
||||
|
||||
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}/d`,
|
||||
service_customer: `${API_URL}/c`,
|
||||
// service_master_data2: `${API_URL}/m`,
|
||||
service_master_data: `${API_URL}/t`,
|
||||
service_transaction: `${API_URL}/tt`,
|
||||
service_wallet: `${API_URL}/w`,
|
||||
transaction: `${API_URL}/x`,
|
||||
service_disbursement: `${API_URL}/s`,
|
||||
service_notification: `${API_URL}/n`,
|
||||
api_dashboard: `${API_URL}/r`,
|
||||
service_card: `${API_URL}/a`,
|
||||
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||
// service_feedback: `${API_URL}/k`,
|
||||
// service_customer: `${API_URL}/c`,
|
||||
// // service_master_data2: `${API_URL}/m`,
|
||||
// // service_master_data: `${API_URL}/t`,
|
||||
// service_transaction: `${API_URL}/tt`,
|
||||
// service_wallet: `${API_URL}/w`,
|
||||
// transaction: `${API_URL}/x`,
|
||||
// service_disbursement: `${API_URL}/s`,
|
||||
// service_notification: `${API_URL}/n`,
|
||||
// api_dashboard: `${API_URL}/r`,
|
||||
// service_card: `${API_URL}/a`,
|
||||
// nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||
};
|
||||
|
||||
export { apiConfig };
|
||||
|
||||
@ -30,19 +30,19 @@ const Layout = () => {
|
||||
<div className="flex flex-col p-8 lg:p-16 gap-4">
|
||||
<Link to="/">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
||||
className="h-12 max-w-none"
|
||||
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||
className="h-20 max-w-none"
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<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">
|
||||
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">
|
||||
<br />
|
||||
transaction management, reporting tools, and configuration settings.
|
||||
{/* transaction management, reporting tools, and configuration settings. */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@ const Header = () => {
|
||||
return (
|
||||
<header
|
||||
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 &&
|
||||
'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"
|
||||
/> */}
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
||||
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||
className="dark:hidden h-14"
|
||||
alt="logo"
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
||||
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||
className="hidden dark:inline-block min-h-[42px]"
|
||||
alt="logo"
|
||||
/>
|
||||
@ -66,7 +66,7 @@ const HeaderLogo = () => {
|
||||
|
||||
<div className="flex items-center">
|
||||
<h3 className={`text-xl hidden md:block ${isSticky ? 'text-black' : 'text-gray-50'}`}>
|
||||
TPAY Dashboard Portal
|
||||
Revenue Dashboard Portal
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,6 @@ const HeaderTopbar = () => {
|
||||
const itemChatRef = useRef<any>(null);
|
||||
const itemUserRef = useRef<any>(null);
|
||||
const itemNotificationsRef = useRef<any>(null);
|
||||
|
||||
const { isRTL } = useLanguage();
|
||||
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
@ -32,10 +31,10 @@ const HeaderTopbar = () => {
|
||||
<span
|
||||
className={`font-semibold text-lg leading-tight ${isSticky ? 'text-black' : 'text-white'}`}
|
||||
>
|
||||
{getAuth()?.user.name}
|
||||
{getAuth()?.user?.name}
|
||||
</span>
|
||||
<span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
|
||||
as {getAuth()?.role_name}
|
||||
{getAuth()?.user?.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@ -8,56 +8,74 @@ import {
|
||||
MenuSub,
|
||||
MenuTitle
|
||||
} from '@/components/menu';
|
||||
import { useMenus } from '@/providers';
|
||||
// import { useMenus } from '@/providers';
|
||||
// import { useLocation } from 'react-router';
|
||||
import { useLanguage } from '@/i18n';
|
||||
import { doGetNavbarMenu } from '@/actions/NavbarMenuActions';
|
||||
import { RoleList, MappedMenu } from '@/types/GlobalTypes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MappedMenu } from '@/types/GlobalTypes';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 { pathname } = useLocation();
|
||||
// const { getMenuConfig } = useMenus();
|
||||
// const primaryMenu = getMenuConfig('primary');
|
||||
const { isRTL } = useLanguage();
|
||||
const [menuData, setMenuData] = useState<RoleList[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
let navbarMenu;
|
||||
|
||||
useEffect(() => {
|
||||
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 navbarMenu: TMenuConfig = STATIC_MENU;
|
||||
|
||||
const buildMenu = (items: TMenuConfig) => {
|
||||
return items.map((item, index) => {
|
||||
@ -149,7 +167,7 @@ const NavbarMenu = () => {
|
||||
<div className="grid">
|
||||
<div className="scrollable-x-auto">
|
||||
<Menu highlight={true} className="gap-5 lg:gap-7.5">
|
||||
{navbarMenu && navbarMenu && buildMenu(navbarMenu)}
|
||||
{navbarMenu && buildMenu(navbarMenu)}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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 */}
|
||||
<div className="xl:col-span-1">
|
||||
<GenerateQr idCustomer={parsedUser.id} />
|
||||
{/* <GenerateQr idCustomer={parsedUser.id} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</AccountUserProfileContextProvider>
|
||||
|
||||
@ -13,43 +13,43 @@ interface IBasicSettingsProps {
|
||||
const BasicSettings = () => {
|
||||
// const user = localStorage.getItem('user');
|
||||
// const parsedUser = user ? JSON.parse(user) : null;
|
||||
const parsedUser = getAuth()?.user;
|
||||
// const parsedUser = getAuth()?.user;
|
||||
// console.log('parsedUser :', parsedUser);
|
||||
const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
||||
const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
||||
const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
||||
// const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
||||
// const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
||||
// const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
||||
|
||||
const { setProfile } = useContext(AccountUserProfileContext);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const isChanged =
|
||||
newUsername !== parsedUser?.username ||
|
||||
newEmail !== parsedUser?.email ||
|
||||
newName !== parsedUser?.name;
|
||||
// const isChanged =
|
||||
// newUsername !== parsedUser?.username ||
|
||||
// newEmail !== parsedUser?.email ||
|
||||
// newName !== parsedUser?.name;
|
||||
|
||||
const handleChangeProfile = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await setProfile({ name: newName, username: newUsername, email: newEmail });
|
||||
// const handleChangeProfile = async () => {
|
||||
// setIsSubmitting(true);
|
||||
// try {
|
||||
// // await setProfile({ name: newName, username: newUsername, email: newEmail });
|
||||
|
||||
const newCache = {
|
||||
name: newName,
|
||||
email: newEmail,
|
||||
username: newUsername
|
||||
};
|
||||
// const newCache = {
|
||||
// name: newName,
|
||||
// email: newEmail,
|
||||
// username: newUsername
|
||||
// };
|
||||
|
||||
localStorage.setItem('user', JSON.stringify(newCache));
|
||||
} catch (error: any) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
'An error occurred while resetting the password.';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
// localStorage.setItem('user', JSON.stringify(newCache));
|
||||
// } catch (error: any) {
|
||||
// const errorMessage =
|
||||
// error?.response?.data?.message ||
|
||||
// error?.message ||
|
||||
// 'An error occurred while resetting the password.';
|
||||
// toast.error(errorMessage);
|
||||
// } finally {
|
||||
// setIsSubmitting(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="card pb-2.5">
|
||||
@ -65,7 +65,7 @@ const BasicSettings = () => {
|
||||
type="text"
|
||||
placeholder='Name'
|
||||
// value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
// onChange={(e) => setNewName(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
@ -76,7 +76,7 @@ const BasicSettings = () => {
|
||||
type="text"
|
||||
placeholder='Username'
|
||||
// value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
// onChange={(e) => setNewUsername(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
@ -88,7 +88,7 @@ const BasicSettings = () => {
|
||||
type="email"
|
||||
placeholder='Email'
|
||||
// value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
// onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
@ -99,8 +99,8 @@ const BasicSettings = () => {
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleChangeProfile}
|
||||
disabled={isSubmitting || !isChanged}
|
||||
// onClick={handleChangeProfile}
|
||||
// disabled={isSubmitting || !isChanged}
|
||||
>
|
||||
{isSubmitting ? 'loading...' : 'Save Change'}
|
||||
</button>
|
||||
|
||||
@ -1,405 +1,17 @@
|
||||
import { Container, KeenIcon, DefaultTooltip } 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 { Container } from '@/components';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import BalanceCard from './blocks/BalanceCard';
|
||||
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';
|
||||
import SitesSatelliteMap from './blocks/SitesateliteMap';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TPAY | Dashboard</title>
|
||||
<title>REVENUE | Dashboard</title>
|
||||
</Helmet>
|
||||
<Container>
|
||||
{/* Account Balance Cards */}
|
||||
{/* {currentRole === 'Escrow' || currentRole === 'Master Agent' ? (
|
||||
<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 className="mt-5">
|
||||
<SitesSatelliteMap />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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 './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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TPAY | Manage User</title>
|
||||
<title>REVENUE | Manage User</title>
|
||||
</Helmet>
|
||||
<ManageUserContextProvider>
|
||||
<Container>
|
||||
@ -20,9 +20,9 @@ export default function ManageUserPage() {
|
||||
<span className="text-sm hover:underline">Dashboard</span>
|
||||
</Link>
|
||||
|
||||
<Link underline="none" color="inherit">
|
||||
{/* <Link underline="none" color="inherit">
|
||||
<span className="text-sm">Settings</span>
|
||||
</Link>
|
||||
</Link> */}
|
||||
|
||||
<Link underline="none" color="inherit">
|
||||
<span className="text-sm">User Management</span>
|
||||
|
||||
@ -41,8 +41,7 @@ import {
|
||||
validateFormCreateUser
|
||||
} from './Types';
|
||||
|
||||
const API_URL_CUSTOMER = apiConfig.service_customer;
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
const API_URL_USER = apiConfig.service_user;
|
||||
|
||||
type PasswordType = 'password' | 'retype_password';
|
||||
|
||||
@ -116,7 +115,7 @@ const AddDialog = () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await PostData(`${API_URL}/user/create`, formField);
|
||||
const response = await PostData(`${API_URL_USER}/user/create`, formField);
|
||||
|
||||
if (response?.status) {
|
||||
handleAddDialog(false);
|
||||
@ -143,74 +142,74 @@ const AddDialog = () => {
|
||||
[formField]
|
||||
);
|
||||
|
||||
const fetchRoles = useCallback(async () => {
|
||||
const params = {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: 'name',
|
||||
order_direction: 'ASC',
|
||||
filter: JSON.stringify({
|
||||
status: 'Y'
|
||||
})
|
||||
};
|
||||
const response = await GetData(`${API_URL}/user_role/list`, params);
|
||||
if (response?.status) {
|
||||
const roleList = response.data?.list || [];
|
||||
setRoles(roleList);
|
||||
} else {
|
||||
setRoles(() => []);
|
||||
}
|
||||
}, []);
|
||||
// const fetchRoles = useCallback(async () => {
|
||||
// const params = {
|
||||
// limit: 100,
|
||||
// page: 1,
|
||||
// with_deleted: false,
|
||||
// order_field: 'name',
|
||||
// order_direction: 'ASC',
|
||||
// filter: JSON.stringify({
|
||||
// status: 'Y'
|
||||
// })
|
||||
// };
|
||||
// const response = await GetData(`${API_URL}/user_role/list`, params);
|
||||
// if (response?.status) {
|
||||
// const roleList = response.data?.list || [];
|
||||
// setRoles(roleList);
|
||||
// } else {
|
||||
// setRoles(() => []);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||
const filter: any =
|
||||
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
||||
// const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||
// const filter: any =
|
||||
// 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 = {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||
};
|
||||
// const query: any = {
|
||||
// limit: 100,
|
||||
// page: 1,
|
||||
// with_deleted: false,
|
||||
// order_field: sorting[0].id,
|
||||
// order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||
// };
|
||||
|
||||
if (filter && Object.keys(filter).length > 0) {
|
||||
query.filter = JSON.stringify(filter);
|
||||
}
|
||||
try {
|
||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||
// if (filter && Object.keys(filter).length > 0) {
|
||||
// query.filter = JSON.stringify(filter);
|
||||
// }
|
||||
// try {
|
||||
// const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||
|
||||
if (response?.status) {
|
||||
setCustomers(response?.data.list);
|
||||
} else {
|
||||
toast.error(response?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to fetch customer list');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// if (response?.status) {
|
||||
// setCustomers(response?.data.list);
|
||||
// } else {
|
||||
// toast.error(response?.message);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// toast.error('Failed to fetch customer list');
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoading(true);
|
||||
setSearchTerm(e.target.value);
|
||||
setSelectedCustomerName(e.target.value);
|
||||
setDropdownOpen(true);
|
||||
const timer = setTimeout(() => {
|
||||
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
// const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// setIsLoading(true);
|
||||
// setSearchTerm(e.target.value);
|
||||
// setSelectedCustomerName(e.target.value);
|
||||
// setDropdownOpen(true);
|
||||
// const timer = setTimeout(() => {
|
||||
// getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||
// }, 500);
|
||||
// return () => clearTimeout(timer);
|
||||
// };
|
||||
|
||||
const handleCustomerSelect = (customer: CustomerProps) => {
|
||||
setFormField({ ...formField, customerid: customer.id });
|
||||
setSelectedCustomerName(customer.fullname);
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
// const handleCustomerSelect = (customer: CustomerProps) => {
|
||||
// setFormField({ ...formField, customerid: customer.id });
|
||||
// setSelectedCustomerName(customer.fullname);
|
||||
// setDropdownOpen(false);
|
||||
// };
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@ -230,9 +229,9 @@ const AddDialog = () => {
|
||||
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
.slice(0, 10);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
// useEffect(() => {
|
||||
// fetchRoles();
|
||||
// }, [fetchRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
const validation = validatePassword(formField.password, formField.retype_password);
|
||||
@ -248,20 +247,20 @@ const AddDialog = () => {
|
||||
}
|
||||
}, [showAddDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||
// useEffect(() => {
|
||||
// getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
// const handleClickOutside = (event: any) => {
|
||||
// if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
// setDropdownOpen(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
// document.addEventListener('mousedown', handleClickOutside);
|
||||
// return () => {
|
||||
// document.removeEventListener('mousedown', handleClickOutside);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
|
||||
event.preventDefault();
|
||||
@ -331,7 +330,7 @@ const AddDialog = () => {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grow flex flex-col" ref={dropdownRef}>
|
||||
<Input
|
||||
@ -366,7 +365,7 @@ const AddDialog = () => {
|
||||
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Email */}
|
||||
<div className="flex gap-2.5">
|
||||
@ -387,7 +386,7 @@ const AddDialog = () => {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grow flex flex-col">
|
||||
<Select
|
||||
@ -414,7 +413,7 @@ const AddDialog = () => {
|
||||
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex gap-2.5">
|
||||
|
||||
@ -16,7 +16,7 @@ import { useCallApi } from '@/hooks';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
import { EnforceSwitch } from '@/components/switch';
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const DeleteDialog = () => {
|
||||
const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext();
|
||||
|
||||
@ -39,8 +39,8 @@ import {
|
||||
validateFormCreateUser
|
||||
} from './Types';
|
||||
|
||||
const API_URL_CUSTOMER = apiConfig.service_customer;
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
// const API_URL_CUSTOMER = apiConfig.service_customer;
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const EditDialog = () => {
|
||||
const parentRef = useRef<any | null>(null);
|
||||
@ -109,59 +109,59 @@ const EditDialog = () => {
|
||||
[selectedUser, formField]
|
||||
);
|
||||
|
||||
const doFetchUserRole = useCallback(async (sorting: any) => {
|
||||
try {
|
||||
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
||||
const response = await GetData(`${API_URL}/user_role/list`, {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
|
||||
});
|
||||
// const doFetchUserRole = useCallback(async (sorting: any) => {
|
||||
// try {
|
||||
// sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
|
||||
// const response = await GetData(`${API_URL}/user_role/list`, {
|
||||
// limit: 100,
|
||||
// page: 1,
|
||||
// with_deleted: false,
|
||||
// order_field: sorting[0].id,
|
||||
// order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
|
||||
// });
|
||||
|
||||
setRoles(response?.data.list);
|
||||
} catch (error) {
|
||||
console.error('Error fetching role', error);
|
||||
}
|
||||
}, []);
|
||||
// setRoles(response?.data.list);
|
||||
// } catch (error) {
|
||||
// console.error('Error fetching role', error);
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||
const filter: any =
|
||||
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
|
||||
// const getCustomerList = async (sorting: any, filterValue: string) => {
|
||||
// const filter: any =
|
||||
// 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 = {
|
||||
limit: 100,
|
||||
page: 1,
|
||||
with_deleted: false,
|
||||
order_field: sorting[0].id,
|
||||
order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||
};
|
||||
// const query: any = {
|
||||
// limit: 100,
|
||||
// page: 1,
|
||||
// with_deleted: false,
|
||||
// order_field: sorting[0].id,
|
||||
// order_direction: sorting[0].desc ? 'DESC' : 'ASC'
|
||||
// };
|
||||
|
||||
if (filter && Object.keys(filter).length > 0) {
|
||||
query.filter = JSON.stringify(filter);
|
||||
}
|
||||
try {
|
||||
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||
// if (filter && Object.keys(filter).length > 0) {
|
||||
// query.filter = JSON.stringify(filter);
|
||||
// }
|
||||
// try {
|
||||
// const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
|
||||
|
||||
if (response?.status) {
|
||||
setCustomers(response?.data.list);
|
||||
} else {
|
||||
toast.error(response?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to fetch customer list');
|
||||
} finally {
|
||||
setIsLoadingCustomer(false);
|
||||
}
|
||||
};
|
||||
// if (response?.status) {
|
||||
// setCustomers(response?.data.list);
|
||||
// } else {
|
||||
// toast.error(response?.message);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// toast.error('Failed to fetch customer list');
|
||||
// } finally {
|
||||
// setIsLoadingCustomer(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
const doFetchUserData = useCallback(async (id: string) => {
|
||||
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) {
|
||||
setFormField((prev) => ({
|
||||
@ -169,9 +169,9 @@ const EditDialog = () => {
|
||||
name: response.data.name,
|
||||
username: response.data.username,
|
||||
email: response.data.email,
|
||||
id_role: response.data.idRole,
|
||||
// id_role: response.data.idRole,
|
||||
status: response.data.status,
|
||||
customerid: response.data.customer?.id || ''
|
||||
// customerid: response.data.customer?.id || ''
|
||||
}));
|
||||
} else {
|
||||
setFormField((prev) => ({
|
||||
@ -179,30 +179,30 @@ const EditDialog = () => {
|
||||
name: '',
|
||||
username: '',
|
||||
email: '',
|
||||
id_role: '',
|
||||
// id_role: '',
|
||||
status: '',
|
||||
customerid: ''
|
||||
// customerid: ''
|
||||
}));
|
||||
}
|
||||
setIsFetching(false);
|
||||
}, []);
|
||||
|
||||
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoadingCustomer(true);
|
||||
setSearchTerm(e.target.value);
|
||||
setSelectedCustomerName(e.target.value);
|
||||
setDropdownOpen(true);
|
||||
const timer = setTimeout(() => {
|
||||
getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
// const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// setIsLoadingCustomer(true);
|
||||
// setSearchTerm(e.target.value);
|
||||
// setSelectedCustomerName(e.target.value);
|
||||
// setDropdownOpen(true);
|
||||
// const timer = setTimeout(() => {
|
||||
// getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
|
||||
// }, 500);
|
||||
// return () => clearTimeout(timer);
|
||||
// };
|
||||
|
||||
const handleCustomerSelect = (customer: CustomerProps) => {
|
||||
setFormField({ ...formField, customerid: customer.id });
|
||||
setSelectedCustomerName(customer.fullname);
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
// const handleCustomerSelect = (customer: CustomerProps) => {
|
||||
// setFormField({ ...formField, customerid: customer.id });
|
||||
// setSelectedCustomerName(customer.fullname);
|
||||
// setDropdownOpen(false);
|
||||
// };
|
||||
|
||||
const filteredCustomer = customers
|
||||
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
@ -216,8 +216,8 @@ const EditDialog = () => {
|
||||
}, [showEditDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||
doFetchUserRole([{ id: 'name', desc: false }]);
|
||||
// getCustomerList([{ id: 'created_at', desc: false }], '');
|
||||
// doFetchUserRole([{ id: 'name', desc: false }]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -316,7 +316,7 @@ const EditDialog = () => {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grow flex flex-col" ref={dropdownRef}>
|
||||
<Input
|
||||
@ -351,7 +351,7 @@ const EditDialog = () => {
|
||||
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Email */}
|
||||
<div className="flex gap-2.5">
|
||||
@ -374,7 +374,7 @@ const EditDialog = () => {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grow flex flex-col">
|
||||
<Select
|
||||
@ -401,7 +401,7 @@ const EditDialog = () => {
|
||||
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex gap-2.5">
|
||||
|
||||
@ -21,7 +21,7 @@ export interface CreateUserParams {
|
||||
password: string;
|
||||
retype_password: string;
|
||||
name: string;
|
||||
id_role: string;
|
||||
// id_role: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@ -31,18 +31,18 @@ export const initialStateCreateUser: {
|
||||
password: string;
|
||||
retype_password: string;
|
||||
name: string;
|
||||
id_role: string;
|
||||
// id_role: string;
|
||||
status: string;
|
||||
customerid: string;
|
||||
// customerid: string;
|
||||
} = {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
retype_password: '',
|
||||
name: '',
|
||||
id_role: '',
|
||||
// id_role: '',
|
||||
status: '',
|
||||
customerid: ''
|
||||
// customerid: ''
|
||||
};
|
||||
|
||||
export const validateFormCreateUser = (
|
||||
@ -56,7 +56,7 @@ export const validateFormCreateUser = (
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'username', label: 'Username' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'id_role', label: 'Role' },
|
||||
// { key: 'id_role', label: 'Role' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'password', label: 'Password' },
|
||||
{ key: 'retype_password', label: 'Retype Password' }
|
||||
@ -65,7 +65,7 @@ export const validateFormCreateUser = (
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'username', label: 'Username' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'id_role', label: 'Role' },
|
||||
// { key: 'id_role', label: 'Role' },
|
||||
{ key: 'status', label: 'Status' }
|
||||
];
|
||||
|
||||
|
||||
@ -35,10 +35,10 @@ interface SelectedUser {
|
||||
name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
// role: string;
|
||||
new_password: string;
|
||||
check_new_password: string;
|
||||
customer: string;
|
||||
// customer: string;
|
||||
}
|
||||
|
||||
const initialProps: ContextProps = {
|
||||
@ -57,7 +57,7 @@ const initialProps: ContextProps = {
|
||||
|
||||
const ManageUserContext = createContext<ContextProps>(initialProps);
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
const ManageUserContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
/* state */
|
||||
@ -98,16 +98,16 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
||||
enableSorting: true,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.customer?.username,
|
||||
id: 'Users.customer',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />,
|
||||
enableSorting: true,
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
headerClassName: 'w-[300px]'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorFn: (row) => row.customer?.username,
|
||||
// id: 'Users.customer',
|
||||
// header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />,
|
||||
// enableSorting: true,
|
||||
// enableHiding: false,
|
||||
// meta: {
|
||||
// headerClassName: 'w-[300px]'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
accessorFn: (row) => row.email,
|
||||
id: 'email',
|
||||
@ -128,23 +128,23 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
||||
headerClassName: 'w-[250px]'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.role.name,
|
||||
id: 'Users.role',
|
||||
header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
|
||||
enableSorting: true,
|
||||
enableHiding: false,
|
||||
cell: (data: any) => {
|
||||
const { role } = data.row.original;
|
||||
// console.log('role :', role);
|
||||
let html = <p className="text-danger italic">Unassigned</p>;
|
||||
if (role && role.name) html = role.name;
|
||||
return html;
|
||||
},
|
||||
meta: {
|
||||
headerClassName: 'w-[350px]'
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorFn: (row) => row.role.name,
|
||||
// id: 'Users.role',
|
||||
// header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
|
||||
// enableSorting: true,
|
||||
// enableHiding: false,
|
||||
// cell: (data: any) => {
|
||||
// const { role } = data.row.original;
|
||||
// // console.log('role :', role);
|
||||
// let html = <p className="text-danger italic">Unassigned</p>;
|
||||
// if (role && role.name) html = role.name;
|
||||
// return html;
|
||||
// },
|
||||
// meta: {
|
||||
// headerClassName: 'w-[350px]'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
accessorFn: (row) => row.status,
|
||||
id: 'status',
|
||||
@ -192,8 +192,8 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
||||
setSelectedUser(row.id);
|
||||
}}
|
||||
>
|
||||
<QrCode className="w-4 h-4 mr-2" />
|
||||
QR Code
|
||||
{/* <QrCode className="w-4 h-4 mr-2" />
|
||||
QR Code */}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEditDialog(true, row.id)}>
|
||||
<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
|
||||
const mappedSorting = sorting.map((sort: any) => ({
|
||||
...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';
|
||||
|
||||
filter =
|
||||
filter.length == 0
|
||||
? {}
|
||||
: { 'Users.username': { like: `%${filter[0].value?.toLowerCase()}%` } };
|
||||
: { 'username': { like: `%${filter[0].value?.toLowerCase()}%` } };
|
||||
const response = await GetData(`${API_URL}/user/list`, {
|
||||
limit: limit,
|
||||
page: page + 1,
|
||||
@ -241,7 +241,7 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
||||
order_direction: orderDirection,
|
||||
filter: JSON.stringify(filter)
|
||||
});
|
||||
// console.log('response api:', response);
|
||||
console.log('response api:', response);
|
||||
|
||||
return { data: response?.data.list, totalCount: response?.data.total_count };
|
||||
};
|
||||
@ -267,17 +267,12 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
|
||||
pagination={{ size: 10 }}
|
||||
toolbar={<ListToolBar />}
|
||||
layout={{ card: true }}
|
||||
sorting={[{ id: 'Users.created_at', desc: false }]}
|
||||
sorting={[{ id: 'created_at', desc: false }]}
|
||||
serverSide={true}
|
||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
||||
doGetListData(pageIndex, pageSize, sorting, columnFilters)
|
||||
}
|
||||
>
|
||||
<Dialog open={showQr} onOpenChange={setShowQr}>
|
||||
<DialogContent>
|
||||
<GenerateQr idCustomer={selectedUser} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{children}
|
||||
</DataGridProvider>
|
||||
</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=""
|
||||
/> */}
|
||||
<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">
|
||||
{auth?.user.email}
|
||||
{auth?.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,8 @@ import { ErrorsRouting } from '@/errors';
|
||||
|
||||
import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage';
|
||||
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 ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction';
|
||||
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 HistoryCard from '@/pages/cards/history-card/HistoryCard';
|
||||
|
||||
// DISBURSEMENT
|
||||
import ComingSoonPage from '@/pages/ComingSoonPage';
|
||||
|
||||
const AppRoutingSetup = (): ReactElement => {
|
||||
return (
|
||||
@ -62,8 +64,13 @@ const AppRoutingSetup = (): ReactElement => {
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<Demo2Layout />}>
|
||||
<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/postoadms" element={<PostoAdmsMaster />} />
|
||||
<Route path="/master-data/sucos" element={<SucosMaster />} />
|
||||
@ -87,7 +94,7 @@ const AppRoutingSetup = (): ReactElement => {
|
||||
|
||||
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
|
||||
|
||||
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
|
||||
|
||||
|
||||
<Route path="/groups/group-management" element={<ManageGroups />} />
|
||||
|
||||
@ -98,10 +105,10 @@ const AppRoutingSetup = (): ReactElement => {
|
||||
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
|
||||
<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 />} /> */}
|
||||
|
||||
{/* cards */}
|
||||
<Route path="/cards/manage-card" element={<ManageCards />} />
|
||||
{/* <Route path="/cards/manage-card" element={<ManageCards />} />
|
||||
|
||||
<Route path="/cards/card-history" element={<HistoryCard />} />
|
||||
|
||||
@ -126,18 +133,17 @@ const AppRoutingSetup = (): ReactElement => {
|
||||
<Route path="/menu/welcome" element={<Welcome />} />
|
||||
<Route path="/message/inbox" element={<Inbox />} />
|
||||
<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/manage-position"
|
||||
element={<ManagePositionPage />}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* DISBURSEMENT */}
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/disbursement/transaction-history"
|
||||
element={<HistoryTransactionDisbursement />}
|
||||
/>
|
||||
/> */}
|
||||
{/* DISBURSEMENT */}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["google.maps", "vite/client"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
Reference in New Issue
Block a user