dashboard page view done
This commit is contained in:
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/media/app/favicon.ico" />
|
<link rel="icon" href="/media/app/favicon.ico" />
|
||||||
<!-- <link rel="icon" href="/media/app/app-logo.png.png" /> -->
|
<!-- <link rel="icon" href="/media/app/logo-tt.jpeg.png" /> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="description" content="" />
|
<meta name="description" content="" />
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"@firebase/firestore": "^4.7.4",
|
"@firebase/firestore": "^4.7.4",
|
||||||
"@formatjs/intl-pluralrules": "^5.3.4",
|
"@formatjs/intl-pluralrules": "^5.3.4",
|
||||||
"@formatjs/intl-relativetimeformat": "^11.4.4",
|
"@formatjs/intl-relativetimeformat": "^11.4.4",
|
||||||
|
"@googlemaps/markerclusterer": "^2.6.2",
|
||||||
"@mui/base": "5.0.0-beta.40",
|
"@mui/base": "5.0.0-beta.40",
|
||||||
"@mui/icons-material": "^6.4.6",
|
"@mui/icons-material": "^6.4.6",
|
||||||
"@mui/material": "^6.1.6",
|
"@mui/material": "^6.1.6",
|
||||||
@ -59,6 +60,7 @@
|
|||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"lucide-react": "^0.456.0",
|
"lucide-react": "^0.456.0",
|
||||||
"metronic-tailwind-react": "file:",
|
"metronic-tailwind-react": "file:",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
@ -97,7 +99,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/leaflet": "^1.9.14",
|
"@types/google.maps": "^3.65.2",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/leaflet.markercluster": "^1.5.6",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
"@types/papaparse": "^5.3.16",
|
"@types/papaparse": "^5.3.16",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
|
|||||||
BIN
public/media/app/logo-tt.jpeg
Normal file
BIN
public/media/app/logo-tt.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@ -23,12 +23,12 @@ const loginSchema = Yup.object().shape({
|
|||||||
remember: Yup.boolean()
|
remember: Yup.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
// const initialValues = {
|
||||||
username: '',
|
// username: '',
|
||||||
password: '',
|
// password: '',
|
||||||
token: '',
|
// token: '',
|
||||||
remember: false
|
// remember: false
|
||||||
};
|
// };
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -39,80 +39,80 @@ const Login = () => {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const { currentLayout } = useLayout();
|
const { currentLayout } = useLayout();
|
||||||
|
|
||||||
const formik = useFormik({
|
// const formik = useFormik({
|
||||||
initialValues,
|
// initialValues,
|
||||||
validationSchema: loginSchema,
|
// validationSchema: loginSchema,
|
||||||
onSubmit: async (values, { setStatus, setSubmitting }) => {
|
// onSubmit: async (values, { setStatus, setSubmitting }) => {
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
if (!login) throw new Error('JWTProvider is required for this form.');
|
// if (!login) throw new Error('JWTProvider is required for this form.');
|
||||||
await login(values.username, values.password, values.token);
|
// await login(values.username, values.password, values.token);
|
||||||
|
|
||||||
if (values.remember) {
|
// if (values.remember) {
|
||||||
localStorage.setItem('username', values.username);
|
// localStorage.setItem('username', values.username);
|
||||||
} else {
|
// } else {
|
||||||
localStorage.removeItem('username');
|
// localStorage.removeItem('username');
|
||||||
}
|
// }
|
||||||
|
|
||||||
navigate(from, { replace: true });
|
// navigate(from, { replace: true });
|
||||||
} catch (error: any) {
|
// } catch (error: any) {
|
||||||
if (error.response && error.response.data) {
|
// if (error.response && error.response.data) {
|
||||||
setStatus(error.response.data.message);
|
// setStatus(error.response.data.message);
|
||||||
} else {
|
// } else {
|
||||||
setStatus('The login details are incorrect');
|
// setStatus('The login details are incorrect');
|
||||||
}
|
// }
|
||||||
|
|
||||||
setSubmitting(false);
|
// setSubmitting(false);
|
||||||
}
|
// }
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
const togglePassword = (event: MouseEvent<HTMLButtonElement>) => {
|
// const togglePassword = (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault();
|
// event.preventDefault();
|
||||||
setShowPassword(!showPassword);
|
// setShowPassword(!showPassword);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleKeyPress = (e: { key: string }) => {
|
// const handleKeyPress = (e: { key: string }) => {
|
||||||
if (e.key === 'Enter') {
|
// if (e.key === 'Enter') {
|
||||||
formik.handleSubmit();
|
// formik.handleSubmit();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>TPAY | Sign In</title>
|
<title>REVENUE | Sign In</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="card max-w-[390px] w-full">
|
<div className="card max-w-[390px] w-full">
|
||||||
<form
|
<form
|
||||||
className="card-body flex flex-col gap-5 p-10"
|
className="card-body flex flex-col gap-5 p-10"
|
||||||
onSubmit={formik.handleSubmit}
|
// onSubmit={formik.handleSubmit}
|
||||||
noValidate
|
noValidate
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 leading-none ">Sign in</h3>
|
<h3 className="text-lg font-semibold text-gray-900 leading-none ">Sign in</h3>
|
||||||
</div>
|
</div>
|
||||||
{formik.status && <Alert variant="danger">{formik.status}</Alert>}
|
{/* {formik.status && <Alert variant="danger">{formik.status}</Alert>} */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="form-label text-gray-900 ps-2.5">Login</label>
|
<label className="form-label text-gray-900 ps-2.5">Login</label>
|
||||||
<label className="input">
|
<label className="input">
|
||||||
<input
|
<input
|
||||||
placeholder="Enter username"
|
placeholder="Enter username"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
{...formik.getFieldProps('username')}
|
// {...formik.getFieldProps('username')}
|
||||||
className={clsx('form-control', {
|
className={clsx('form-control', {
|
||||||
'is-invalid': formik.touched.username && formik.errors.username
|
// 'is-invalid': formik.touched.username && formik.errors.username
|
||||||
})}
|
})}
|
||||||
onKeyPress={handleKeyPress}
|
// onKeyPress={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{formik.touched.username && formik.errors.username && (
|
{/* {formik.touched.username && formik.errors.username && (
|
||||||
<span role="alert" className="text-danger text-xs mt-1">
|
<span role="alert" className="text-danger text-xs mt-1">
|
||||||
{formik.errors.username}
|
{formik.errors.username}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@ -124,13 +124,15 @@ const Login = () => {
|
|||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
placeholder="Enter Password"
|
placeholder="Enter Password"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
{...formik.getFieldProps('password')}
|
// {...formik.getFieldProps('password')}
|
||||||
className={clsx('form-control', {
|
// className={clsx('form-control', {
|
||||||
'is-invalid': formik.touched.password && formik.errors.password
|
// 'is-invalid': formik.touched.password && formik.errors.password
|
||||||
})}
|
// })}
|
||||||
onKeyPress={handleKeyPress}
|
// onKeyPress={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-icon" onClick={togglePassword} type="button">
|
<button className="btn btn-icon"
|
||||||
|
// onClick={togglePassword}
|
||||||
|
type="button">
|
||||||
<KeenIcon icon="eye" className={clsx('text-gray-500', { hidden: showPassword })} />
|
<KeenIcon icon="eye" className={clsx('text-gray-500', { hidden: showPassword })} />
|
||||||
<KeenIcon
|
<KeenIcon
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
@ -138,71 +140,22 @@ const Login = () => {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
{formik.touched.password && formik.errors.password && (
|
{/* {formik.touched.password && formik.errors.password && (
|
||||||
<span role="alert" className="text-danger text-xs mt-1">
|
<span role="alert" className="text-danger text-xs mt-1">
|
||||||
{formik.errors.password}
|
{formik.errors.password}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="form-label text-gray-900 ps-2.5">Code Verify</label>
|
|
||||||
<label className="input">
|
|
||||||
<input
|
|
||||||
placeholder="Enter code verify"
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
autoComplete="off"
|
|
||||||
maxLength={6}
|
|
||||||
{...formik.getFieldProps('token')}
|
|
||||||
className={clsx('form-control', {
|
|
||||||
'is-invalid': formik.touched.token && formik.errors.token
|
|
||||||
})}
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (!/[0-9]/.test(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{formik.touched.token && formik.errors.token && (
|
|
||||||
<span role="alert" className="text-danger text-xs mt-1">
|
|
||||||
{formik.errors.token}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-1">
|
|
||||||
<label className="checkbox-group">
|
|
||||||
<input
|
|
||||||
className="checkbox checkbox-sm"
|
|
||||||
type="checkbox"
|
|
||||||
{...formik.getFieldProps('remember')}
|
|
||||||
/>
|
|
||||||
<span className="checkbox-label">Remember me</span>
|
|
||||||
</label>
|
|
||||||
{/* <Link
|
|
||||||
to={
|
|
||||||
currentLayout?.name === 'auth-branded'
|
|
||||||
? '/auth/reset-password'
|
|
||||||
: '/auth/classic/reset-password'
|
|
||||||
}
|
|
||||||
className="text-2sm link shrink-0"
|
|
||||||
>
|
|
||||||
Forgot Password?
|
|
||||||
</Link> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary flex justify-center grow"
|
className="btn btn-primary flex justify-center grow"
|
||||||
disabled={loading || formik.isSubmitting}
|
// disabled={loading || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
{loading ? 'Please wait...' : 'Sign In'}
|
{loading ? 'Please wait...' : 'Sign In'}
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
|
<p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
|
||||||
Copyright {moment().year()} © Telkomcel All rights reserved.
|
Copyright {moment().year()} © Timor Telecom All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const LoaderTransparant = () => {
|
|||||||
<img
|
<img
|
||||||
// className="h-[30px] max-w-none"
|
// className="h-[30px] max-w-none"
|
||||||
className="h-14 w-auto drop-shadow-md"
|
className="h-14 w-auto drop-shadow-md"
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const ScreenLoader = () => {
|
|||||||
<img
|
<img
|
||||||
// className="h-[30px] max-w-none"
|
// className="h-[30px] max-w-none"
|
||||||
className="h-14 w-auto drop-shadow-md"
|
className="h-14 w-auto drop-shadow-md"
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
{/* <div className="text-gray-500 font-medium text-sm">Loading...</div> */}
|
||||||
|
|||||||
@ -30,19 +30,19 @@ const Layout = () => {
|
|||||||
<div className="flex flex-col p-8 lg:p-16 gap-4">
|
<div className="flex flex-col p-8 lg:p-16 gap-4">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="h-12 max-w-none"
|
className="h-20 max-w-none"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-2xl font-semibold text-gray-900">TPAY Dashboard Portal</h3>
|
<h3 className="text-2xl font-semibold text-gray-900">Revenue Dashboard Portal</h3>
|
||||||
<div className="text-base font-medium text-gray-600">
|
<div className="text-base font-medium text-gray-600">
|
||||||
A user-friendly interface providing seamless access to TPAY’s
|
{/* A user-friendly interface providing seamless access to TPAY’s */}
|
||||||
<span className="text-gray-900 font-semibold">
|
<span className="text-gray-900 font-semibold">
|
||||||
<br />
|
<br />
|
||||||
transaction management, reporting tools, and configuration settings.
|
{/* transaction management, reporting tools, and configuration settings. */}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const Header = () => {
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex items-center transition-[height] shrink-0 h-[--tw-header-height] bg-[length:600px] bg-no-repeat bg-red-600',
|
'flex items-center transition-[height] shrink-0 h-[--tw-header-height] bg-[length:600px] bg-no-repeat bg-blue-600',
|
||||||
headerSticky &&
|
headerSticky &&
|
||||||
'transition-[height] fixed z-10 top-0 left-0 right-0 shadow-sm backdrop-blur-md bg-white/70 dark:bg-coal-500/70 dark:border-b dark:border-b-coal-100'
|
'transition-[height] fixed z-10 top-0 left-0 right-0 shadow-sm backdrop-blur-md bg-white/70 dark:bg-coal-500/70 dark:border-b dark:border-b-coal-100'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -53,12 +53,12 @@ const HeaderLogo = () => {
|
|||||||
alt="logo"
|
alt="logo"
|
||||||
/> */}
|
/> */}
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="dark:hidden h-14"
|
className="dark:hidden h-14"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={toAbsoluteUrl('/media/app/app-logo.png')}
|
src={toAbsoluteUrl('/media/app/logo-tt.jpeg')}
|
||||||
className="hidden dark:inline-block min-h-[42px]"
|
className="hidden dark:inline-block min-h-[42px]"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
/>
|
/>
|
||||||
@ -66,7 +66,7 @@ const HeaderLogo = () => {
|
|||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className={`text-xl hidden md:block ${isSticky ? 'text-black' : 'text-gray-50'}`}>
|
<h3 className={`text-xl hidden md:block ${isSticky ? 'text-black' : 'text-gray-50'}`}>
|
||||||
TPAY Dashboard Portal
|
Revenue Dashboard Portal
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -34,9 +34,9 @@ const HeaderTopbar = () => {
|
|||||||
>
|
>
|
||||||
{getAuth()?.user.name}
|
{getAuth()?.user.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
|
{/* <span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
|
||||||
as {getAuth()?.role_name}
|
as {getAuth()?.role_name}
|
||||||
</span>
|
</span> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Menu className="w-12 h-12">
|
<Menu className="w-12 h-12">
|
||||||
|
|||||||
@ -8,56 +8,69 @@ import {
|
|||||||
MenuSub,
|
MenuSub,
|
||||||
MenuTitle
|
MenuTitle
|
||||||
} from '@/components/menu';
|
} from '@/components/menu';
|
||||||
import { useMenus } from '@/providers';
|
// import { useMenus } from '@/providers';
|
||||||
// import { useLocation } from 'react-router';
|
// import { useLocation } from 'react-router';
|
||||||
import { useLanguage } from '@/i18n';
|
import { useLanguage } from '@/i18n';
|
||||||
import { doGetNavbarMenu } from '@/actions/NavbarMenuActions';
|
import { MappedMenu } from '@/types/GlobalTypes';
|
||||||
import { RoleList, MappedMenu } from '@/types/GlobalTypes';
|
|
||||||
import { useEffect, useState } from 'react';
|
// ---------------------------------------------------------------------------
|
||||||
|
// Static menu structure (previously fetched from the API via
|
||||||
|
// doGetNavbarMenu). Update `path` values to match your actual route
|
||||||
|
// definitions:
|
||||||
|
// - "Dashboard" points to the real dashboard route.
|
||||||
|
// - Every other item points to "/coming-soon", a shared placeholder page
|
||||||
|
// that just renders "This page will be available soon".
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const COMING_SOON_PATH = '/coming-soon';
|
||||||
|
|
||||||
|
const STATIC_MENU: MappedMenu[] = [
|
||||||
|
{
|
||||||
|
title: 'Main Dashboard',
|
||||||
|
path: '-',
|
||||||
|
children: [{ title: 'Dashboard', path: '/' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '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: COMING_SOON_PATH },
|
||||||
|
{ title: 'Group Role List', path: COMING_SOON_PATH },
|
||||||
|
{ title: 'Role List', path: COMING_SOON_PATH }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manage Profile',
|
||||||
|
path: '-',
|
||||||
|
children: [{ title: 'Manage Cell', path: COMING_SOON_PATH }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const NavbarMenu = () => {
|
const NavbarMenu = () => {
|
||||||
// const { pathname } = useLocation();
|
// const { pathname } = useLocation();
|
||||||
// const { getMenuConfig } = useMenus();
|
// const { getMenuConfig } = useMenus();
|
||||||
// const primaryMenu = getMenuConfig('primary');
|
// const primaryMenu = getMenuConfig('primary');
|
||||||
const { isRTL } = useLanguage();
|
const { isRTL } = useLanguage();
|
||||||
const [menuData, setMenuData] = useState<RoleList[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
let navbarMenu;
|
|
||||||
|
|
||||||
useEffect(() => {
|
const navbarMenu: TMenuConfig = STATIC_MENU;
|
||||||
const fetchNavbarMenu = async () => {
|
|
||||||
try {
|
|
||||||
const result = await doGetNavbarMenu();
|
|
||||||
if (result.status && result.data) {
|
|
||||||
setMenuData(result.data.roles_list);
|
|
||||||
} else {
|
|
||||||
setError(result.message);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to fetch navbar menu');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchNavbarMenu();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const mapMenuData = (data: RoleList[]): MappedMenu[] => {
|
|
||||||
return data.map((item) => {
|
|
||||||
const mappedItem: MappedMenu = {
|
|
||||||
title: item.name,
|
|
||||||
path: item.link !== '-' ? item.link : '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
mappedItem.children = mapMenuData(item.children);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedItem;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
navbarMenu = mapMenuData(menuData);
|
|
||||||
// navbarMenu = primaryMenu?.[0].children;
|
|
||||||
|
|
||||||
const buildMenu = (items: TMenuConfig) => {
|
const buildMenu = (items: TMenuConfig) => {
|
||||||
return items.map((item, index) => {
|
return items.map((item, index) => {
|
||||||
@ -149,11 +162,11 @@ const NavbarMenu = () => {
|
|||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div className="scrollable-x-auto">
|
<div className="scrollable-x-auto">
|
||||||
<Menu highlight={true} className="gap-5 lg:gap-7.5">
|
<Menu highlight={true} className="gap-5 lg:gap-7.5">
|
||||||
{navbarMenu && navbarMenu && buildMenu(navbarMenu)}
|
{navbarMenu && buildMenu(navbarMenu)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { NavbarMenu };
|
export { NavbarMenu };
|
||||||
20
src/pages/ComingSoonPage.tsx
Normal file
20
src/pages/ComingSoonPage.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Container } from '@/components';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
const ComingSoonPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>TPAY | Coming Soon</title>
|
||||||
|
</Helmet>
|
||||||
|
<Container>
|
||||||
|
<div className="flex flex-col items-center justify-center text-center" style={{ minHeight: '60vh' }}>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-2">This page will be available soon</h2>
|
||||||
|
<p className="text-sm text-gray-500">We're still working on this feature. Please check back later.</p>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComingSoonPage;
|
||||||
@ -25,7 +25,7 @@ const AccountUserProfileContent = () => {
|
|||||||
|
|
||||||
{/* Right Column - MFA Setup */}
|
{/* Right Column - MFA Setup */}
|
||||||
<div className="xl:col-span-1">
|
<div className="xl:col-span-1">
|
||||||
<GenerateQr idCustomer={parsedUser.id} />
|
{/* <GenerateQr idCustomer={parsedUser.id} /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccountUserProfileContextProvider>
|
</AccountUserProfileContextProvider>
|
||||||
|
|||||||
@ -13,43 +13,43 @@ interface IBasicSettingsProps {
|
|||||||
const BasicSettings = () => {
|
const BasicSettings = () => {
|
||||||
// const user = localStorage.getItem('user');
|
// const user = localStorage.getItem('user');
|
||||||
// const parsedUser = user ? JSON.parse(user) : null;
|
// const parsedUser = user ? JSON.parse(user) : null;
|
||||||
const parsedUser = getAuth()?.user;
|
// const parsedUser = getAuth()?.user;
|
||||||
// console.log('parsedUser :', parsedUser);
|
// console.log('parsedUser :', parsedUser);
|
||||||
const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
// const [newUsername, setNewUsername] = useState<string>(parsedUser?.username || '');
|
||||||
const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
// const [newEmail, setNewEmail] = useState<string>(parsedUser?.email || '');
|
||||||
const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
// const [newName, setNewName] = useState<string>(parsedUser?.name || '');
|
||||||
|
|
||||||
const { setProfile } = useContext(AccountUserProfileContext);
|
const { setProfile } = useContext(AccountUserProfileContext);
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const isChanged =
|
// const isChanged =
|
||||||
newUsername !== parsedUser?.username ||
|
// newUsername !== parsedUser?.username ||
|
||||||
newEmail !== parsedUser?.email ||
|
// newEmail !== parsedUser?.email ||
|
||||||
newName !== parsedUser?.name;
|
// newName !== parsedUser?.name;
|
||||||
|
|
||||||
const handleChangeProfile = async () => {
|
// const handleChangeProfile = async () => {
|
||||||
setIsSubmitting(true);
|
// setIsSubmitting(true);
|
||||||
try {
|
// try {
|
||||||
await setProfile({ name: newName, username: newUsername, email: newEmail });
|
// // await setProfile({ name: newName, username: newUsername, email: newEmail });
|
||||||
|
|
||||||
const newCache = {
|
// const newCache = {
|
||||||
name: newName,
|
// name: newName,
|
||||||
email: newEmail,
|
// email: newEmail,
|
||||||
username: newUsername
|
// username: newUsername
|
||||||
};
|
// };
|
||||||
|
|
||||||
localStorage.setItem('user', JSON.stringify(newCache));
|
// localStorage.setItem('user', JSON.stringify(newCache));
|
||||||
} catch (error: any) {
|
// } catch (error: any) {
|
||||||
const errorMessage =
|
// const errorMessage =
|
||||||
error?.response?.data?.message ||
|
// error?.response?.data?.message ||
|
||||||
error?.message ||
|
// error?.message ||
|
||||||
'An error occurred while resetting the password.';
|
// 'An error occurred while resetting the password.';
|
||||||
toast.error(errorMessage);
|
// toast.error(errorMessage);
|
||||||
} finally {
|
// } finally {
|
||||||
setIsSubmitting(false);
|
// setIsSubmitting(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card pb-2.5">
|
<div className="card pb-2.5">
|
||||||
@ -65,7 +65,7 @@ const BasicSettings = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder='Name'
|
placeholder='Name'
|
||||||
// value={newName}
|
// value={newName}
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
// onChange={(e) => setNewName(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +76,7 @@ const BasicSettings = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder='Username'
|
placeholder='Username'
|
||||||
// value={newUsername}
|
// value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
// onChange={(e) => setNewUsername(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@ const BasicSettings = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder='Email'
|
placeholder='Email'
|
||||||
// value={newEmail}
|
// value={newEmail}
|
||||||
onChange={(e) => setNewEmail(e.target.value)}
|
// onChange={(e) => setNewEmail(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -99,8 +99,8 @@ const BasicSettings = () => {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleChangeProfile}
|
// onClick={handleChangeProfile}
|
||||||
disabled={isSubmitting || !isChanged}
|
// disabled={isSubmitting || !isChanged}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'loading...' : 'Save Change'}
|
{isSubmitting ? 'loading...' : 'Save Change'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,408 +1,20 @@
|
|||||||
import { Container, KeenIcon, DefaultTooltip } from '@/components';
|
import { Container } from '@/components';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { Card, Chart, YearPicker } from './blocks';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { useFetchCardData, useFetchChartData } from './hooks';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useFetchYear } from './hooks/useFetchYear';
|
|
||||||
import { get5LastYear } from '@/utils/Date';
|
|
||||||
import { staticChartData } from './staticChart';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import BalanceCard from './blocks/BalanceCard';
|
import SitesSatelliteMap from './blocks/SitesateliteMap';
|
||||||
import { getAuth } from '@/auth';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import TransactionValue from './blocks/TransactionValue';
|
|
||||||
import TransactionPieChart from './blocks/TransactionPieChart';
|
|
||||||
import MemberActivity from './blocks/MemberActivity';
|
|
||||||
import BankSaldo from './blocks/BankSaldo';
|
|
||||||
|
|
||||||
// sum -> nominal, count-> total
|
|
||||||
type CountType = 'sum' | 'count';
|
|
||||||
type ChartType = 'line' | 'bar';
|
|
||||||
type ChartLegend = 'true' | 'false';
|
|
||||||
|
|
||||||
const DashboardHomePage = () => {
|
const DashboardHomePage = () => {
|
||||||
const selectYear = get5LastYear();
|
|
||||||
const [initialYear, setInitialYear] = useState<string>('');
|
|
||||||
const [selectedYear, setSelectedYear] = useState<string>('');
|
|
||||||
const [count, setCount] = useState<CountType>('sum');
|
|
||||||
const [chartType, setChartType] = useState<ChartType>('line');
|
|
||||||
const [chartLegend, setChartLegend] = useState<ChartLegend>('true');
|
|
||||||
const [dateRange, setDateRange] = useState<{ from: Date; to: Date }>({
|
|
||||||
from: new Date(),
|
|
||||||
to: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
const getFirstDayOfMonth = () => {
|
|
||||||
const now = new Date();
|
|
||||||
return new Date(now.getFullYear(), now.getMonth(), 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getToday = () => {
|
|
||||||
return new Date();
|
|
||||||
};
|
|
||||||
|
|
||||||
const [fromDate, setFromDate] = useState(getFirstDayOfMonth());
|
|
||||||
const [toDate, setToDate] = useState(getToday());
|
|
||||||
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
|
|
||||||
const API_URL_BANK = apiConfig.service_wallet;
|
|
||||||
|
|
||||||
const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null);
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
const res = await GetData(`${API_URL}/card-statistic`, {});
|
|
||||||
setResponseStatisticCard(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [bankaccount, setbankaccount] = useState<any>(null);
|
|
||||||
|
|
||||||
const fetchDataBankAccount = async () => {
|
|
||||||
const res = await GetData(`${API_URL_BANK}/dashboard/balance/account/${getAuth()?.user?.customer?.id}`, {});
|
|
||||||
setbankaccount(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDataBankAccount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [responseGraphic, setresponseGraphic] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataGraphic = async () => {
|
|
||||||
const res = await GetData(`${API_URL}/cashin-vs-cashout`, {
|
|
||||||
date_from: fromDate.toISOString(),
|
|
||||||
date_to: toDate.toISOString()
|
|
||||||
});
|
|
||||||
setresponseGraphic(res);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataGraphic();
|
|
||||||
}, [fromDate, toDate]);
|
|
||||||
|
|
||||||
let staticChartDataApiFetch = [];
|
|
||||||
|
|
||||||
// Make map for date => { cashin: 0, cashout: 0 }
|
|
||||||
const dataMap: Record<string, { cashin: number; cashout: number }> = {};
|
|
||||||
const current = moment(fromDate);
|
|
||||||
const end = moment(toDate);
|
|
||||||
|
|
||||||
// Inisialization dataMap with all dates
|
|
||||||
while (current.isSameOrBefore(end, 'day')) {
|
|
||||||
const dateStr = current.format('DD-MM-YYYY');
|
|
||||||
dataMap[dateStr] = { cashin: 0, cashout: 0 };
|
|
||||||
current.add(1, 'day');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cashin
|
|
||||||
if (Array.isArray(responseGraphic?.data.total_cashin)) {
|
|
||||||
responseGraphic.data.total_cashin.forEach((item: any) => {
|
|
||||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
|
||||||
// Only update cashin if it's not already set (meaning no value was added before)
|
|
||||||
if (dataMap[date]) {
|
|
||||||
dataMap[date].cashin = Number(item.total_count ?? 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cashout
|
|
||||||
if (Array.isArray(responseGraphic?.data.total_cashout)) {
|
|
||||||
responseGraphic.data.total_cashout.forEach((item: any) => {
|
|
||||||
const date = moment(item.created_date).format('DD-MM-YYYY');
|
|
||||||
// Only update cashout if it's not already set (meaning no value was added before)
|
|
||||||
if (dataMap[date]) {
|
|
||||||
dataMap[date].cashout = Number(item.total_count ?? 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change to array for chart
|
|
||||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
|
||||||
month,
|
|
||||||
cashin: values.cashin,
|
|
||||||
cashout: values.cashout
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Change to array for chart
|
|
||||||
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
|
||||||
month,
|
|
||||||
cashin: values.cashin,
|
|
||||||
cashout: values.cashout
|
|
||||||
}));
|
|
||||||
|
|
||||||
const currentRole = getAuth()?.role_name;
|
|
||||||
const idCustomer = getAuth()?.user.customer?.id;
|
|
||||||
// console.log(currentRole);
|
|
||||||
|
|
||||||
// Menyusun tanggal awal dan akhir berdasarkan selectedYear
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectYear.length > 0 && !selectedYear) {
|
|
||||||
let latestYear = Math.max(...selectYear.map((item) => parseInt(item, 10))).toString();
|
|
||||||
// console.log('latestYear :', latestYear);
|
|
||||||
setSelectedYear(latestYear);
|
|
||||||
setInitialYear(latestYear);
|
|
||||||
}
|
|
||||||
}, [selectYear, selectedYear]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedYear) {
|
|
||||||
setDateRange({
|
|
||||||
from: moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
|
||||||
to: moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedYear]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialYear) {
|
|
||||||
setDateRange({
|
|
||||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
|
||||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [initialYear]);
|
|
||||||
|
|
||||||
const handleYearChange = (year: string) => {
|
|
||||||
setSelectedYear(year);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCountType = (value: CountType) => {
|
|
||||||
setCount(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChartType = (value: ChartType) => {
|
|
||||||
setChartType(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChartLegend = (value: ChartLegend) => {
|
|
||||||
setChartLegend(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetFilter = useCallback(() => {
|
|
||||||
const firstDay = getFirstDayOfMonth();
|
|
||||||
const today = getToday();
|
|
||||||
|
|
||||||
setFromDate(firstDay);
|
|
||||||
setToDate(today);
|
|
||||||
|
|
||||||
// reset filter yang lain kalau perlu
|
|
||||||
setDateRange({ from: firstDay, to: today });
|
|
||||||
setSelectedYear(initialYear);
|
|
||||||
setCount('sum');
|
|
||||||
setChartType('line');
|
|
||||||
setChartLegend('true');
|
|
||||||
}, [initialYear]);
|
|
||||||
|
|
||||||
const { cardData } = useFetchCardData(
|
|
||||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
|
||||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
|
||||||
count
|
|
||||||
);
|
|
||||||
|
|
||||||
const { chartData } = useFetchChartData(
|
|
||||||
moment(dateRange.from).format('YYYY-MM-DD'),
|
|
||||||
moment(dateRange.to).format('YYYY-MM-DD'),
|
|
||||||
count
|
|
||||||
);
|
|
||||||
|
|
||||||
const toolbar = (
|
|
||||||
<div className="flex gap-3 items-center w-1/2">
|
|
||||||
<div className="w-auto min-w-[120px]">
|
|
||||||
<Select value={chartType} onValueChange={handleChartType}>
|
|
||||||
<SelectTrigger size="sm">
|
|
||||||
<SelectValue placeholder="Select Chart Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="w-32">
|
|
||||||
<SelectItem value="line">Line</SelectItem>
|
|
||||||
<SelectItem value="bar">Bar</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="w-auto min-w-[120px]">
|
|
||||||
<Select value={chartLegend} onValueChange={handleChartLegend}>
|
|
||||||
<SelectTrigger size="sm">
|
|
||||||
<SelectValue placeholder="Select Legend Visibility" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="w-full">
|
|
||||||
<SelectItem value="true">Show Legend</SelectItem>
|
|
||||||
<SelectItem value="false">Hide Legend</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const number: number = responseStatisticCard?.data.total_cash_in ?? 0;
|
|
||||||
const formattedNumber: number = parseFloat(number.toFixed(2));
|
|
||||||
|
|
||||||
const numbercashout: number = responseStatisticCard?.data.total_cash_out ?? 0;
|
|
||||||
const formattedNumbercashout: number = parseFloat(numbercashout.toFixed(2));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>TPAY | Dashboard</title>
|
<title>REVENUE | Dashboard</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Container>
|
<Container>
|
||||||
{/* Account Balance Cards */}
|
<div className="mt-5">
|
||||||
{/* {currentRole === 'Escrow' || currentRole === 'Master Agent' ? (
|
<SitesSatelliteMap />
|
||||||
<div className="grid gap-5 lg:gap-7.5 mb-14 mt-7">
|
|
||||||
<BalanceCard id={idCustomer} />
|
|
||||||
</div>
|
|
||||||
) : null} */}
|
|
||||||
|
|
||||||
<div className="flex space-x-4 mt-5">
|
|
||||||
{bankaccount?.data && bankaccount?.data.length > 0
|
|
||||||
&& getAuth()?.statusbalance=='Y'
|
|
||||||
? (
|
|
||||||
bankaccount?.data.map((bankaccountdatas: { id_balance:string,amount: string, credit_limit: string, monthly_limit: string; wallet: string; }, index: number) => (
|
|
||||||
<BankSaldo
|
|
||||||
title={bankaccountdatas.wallet}
|
|
||||||
balance={bankaccountdatas.amount}
|
|
||||||
creditLimit={bankaccountdatas.credit_limit}
|
|
||||||
monthlyLimit={bankaccountdatas.monthly_limit}
|
|
||||||
idbalance={bankaccountdatas.id_balance}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cards */}
|
|
||||||
<div className="flex gap-6 overflow-x-auto pb-2 mt-5">
|
|
||||||
<Card
|
|
||||||
title="Registered Users"
|
|
||||||
total={responseStatisticCard?.data.total_registered ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.registered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.registered_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Unregistered Users"
|
|
||||||
total={responseStatisticCard?.data.total_unregistered ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.unregistered_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.unregistered_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Cash-in"
|
|
||||||
total={formattedNumber.toFixed(2)} // now a number: 972.80
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.cash_in_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.cash_in_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Cash-out"
|
|
||||||
total={formattedNumbercashout.toFixed(2)} // now a number: 972.80
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.cash_out_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.cash_out_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Active Event"
|
|
||||||
total={responseStatisticCard?.data.total_event ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.event_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.event_last_week.surplus ?? false}
|
|
||||||
icon="test"
|
|
||||||
/>
|
|
||||||
<Card
|
|
||||||
title="Total Billing"
|
|
||||||
total={responseStatisticCard?.data.total_billing ?? 0}
|
|
||||||
growth={parseFloat((responseStatisticCard?.data?.billing_last_week.percent ?? 0).toFixed(2)) ?? 0}
|
|
||||||
surplus={responseStatisticCard?.data.billing_last_week.surplus ?? false}
|
|
||||||
icon='test'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 items-center mb-6 mt-6">
|
|
||||||
<div className="flex gap-3 items-center w-full md:w-auto">
|
|
||||||
<label className="input input-sm w-[160px]">
|
|
||||||
From
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="from"
|
|
||||||
value={moment(fromDate).format('YYYY-MM-DD')}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
setFromDate(new Date(e.target.value));
|
|
||||||
} else {
|
|
||||||
// Reset to default value (e.g. first day of current month)
|
|
||||||
setFromDate(getFirstDayOfMonth());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="input input-sm w-[160px]">
|
|
||||||
To
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
name="to"
|
|
||||||
value={moment(toDate).format('YYYY-MM-DD')}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
setToDate(new Date(e.target.value));
|
|
||||||
} else {
|
|
||||||
// Reset to default value (e.g. today)
|
|
||||||
setToDate(getToday());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DefaultTooltip title="Reset Filter" placement="top">
|
|
||||||
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
|
||||||
<KeenIcon icon="arrow-circle-left" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Chart */}
|
|
||||||
<div className="grid gap-5 lg:gap-7.5 mt-5">
|
|
||||||
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<Chart
|
|
||||||
title="Overview"
|
|
||||||
count={12}
|
|
||||||
toolbar={toolbar}
|
|
||||||
chartData={staticChartDataApiFetch}
|
|
||||||
chartType={chartType}
|
|
||||||
chartLegend={chartLegend}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-4 mt-5">
|
|
||||||
<TransactionValue startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
<TransactionPieChart startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
<MemberActivity startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardHomePage;
|
export default DashboardHomePage;
|
||||||
189
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
189
src/pages/dashboards/home/blocks/SitesateliteMap.tsx
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import 'leaflet.markercluster';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||||
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||||
|
import { sitePoints, SitePoint } from '../sitepoints';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Satellite map (Leaflet + Esri World Imagery, no API key required) with a
|
||||||
|
// pin for every Cell/Sector row from the uploaded Excel
|
||||||
|
// (Sites_Coordinates_untuk_CC.xlsm -> "site coordinates 4G").
|
||||||
|
//
|
||||||
|
// SETUP REQUIRED:
|
||||||
|
// npm install leaflet leaflet.markercluster
|
||||||
|
// npm install --save-dev @types/leaflet @types/leaflet.markercluster
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const DILI_CENTER: [number, number] = [-8.5586, 125.5736];
|
||||||
|
|
||||||
|
// Groups points that share the exact same coordinates so a single pin can
|
||||||
|
// list every cell/sector at that physical site.
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signal-tower badge icon: colored circle background + white "broadcast
|
||||||
|
// tower" glyph, sized up slightly for pins that represent more than one
|
||||||
|
// cell/sector at the same coordinates.
|
||||||
|
const pinIcon = (multi: boolean) => {
|
||||||
|
const size = multi ? 30 : 26;
|
||||||
|
const bg = multi ? '#2563eb' : '#ef4444';
|
||||||
|
|
||||||
|
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]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const SitesSatelliteMap = () => {
|
||||||
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const mapRef = useRef<L.Map | null>(null);
|
||||||
|
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||||
|
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedCount, setSelectedCount] = useState(sitePoints.length);
|
||||||
|
|
||||||
|
const grouped = useMemo(() => groupByCoordinate(sitePoints), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapContainerRef.current || mapRef.current) return;
|
||||||
|
|
||||||
|
const map = L.map(mapContainerRef.current, {
|
||||||
|
center: DILI_CENTER,
|
||||||
|
zoom: 12,
|
||||||
|
zoomControl: true
|
||||||
|
});
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
// Esri World Imagery — free satellite tiles, no API key required.
|
||||||
|
L.tileLayer(
|
||||||
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
|
{
|
||||||
|
attribution:
|
||||||
|
'Tiles © Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community',
|
||||||
|
maxZoom: 19
|
||||||
|
}
|
||||||
|
).addTo(map);
|
||||||
|
|
||||||
|
// Optional labels/roads overlay on top of the satellite imagery.
|
||||||
|
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);
|
||||||
|
|
||||||
|
const cluster = L.markerClusterGroup({ maxClusterRadius: 50 });
|
||||||
|
clusterRef.current = cluster;
|
||||||
|
|
||||||
|
grouped.forEach((points, key) => {
|
||||||
|
const [lat, lng] = key.split(',').map(Number);
|
||||||
|
const marker = L.marker([lat, lng], { icon: pinIcon(points.length > 1) });
|
||||||
|
|
||||||
|
const site = points[0].site;
|
||||||
|
const location = points[0].location;
|
||||||
|
const rows = points
|
||||||
|
.map((p) => `<tr><td style="padding-right:8px;">${p.cell}</td><td>${p.sector}</td></tr>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
marker.bindPopup(`
|
||||||
|
<div style="font-family: Arial, sans-serif; font-size: 13px; max-width: 240px;">
|
||||||
|
<div style="font-weight:600; margin-bottom:2px;">${site}</div>
|
||||||
|
<div style="color:#666; margin-bottom:6px;">${location} · ${lat.toFixed(5)}, ${lng.toFixed(5)}</div>
|
||||||
|
<table style="width:100%; border-collapse:collapse;">${rows}</table>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
markerByKeyRef.current.set(key, marker);
|
||||||
|
cluster.addLayer(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addLayer(cluster);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
clusterRef.current = null;
|
||||||
|
markerByKeyRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [grouped]);
|
||||||
|
|
||||||
|
// Simple search: pans/zooms to a matching cell, sector, or site name and
|
||||||
|
// opens its popup.
|
||||||
|
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], 17);
|
||||||
|
const marker = markerByKeyRef.current.get(key);
|
||||||
|
if (marker && clusterRef.current) {
|
||||||
|
clusterRef.current.zoomToShowLayer(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">
|
||||||
|
Menampilkan <span className="font-semibold">{selectedCount}</span> dari {sitePoints.length} titik
|
||||||
|
(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"
|
||||||
|
/>
|
||||||
|
</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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SitesSatelliteMap;
|
||||||
1161
src/pages/dashboards/home/sitepoints.ts
Normal file
1161
src/pages/dashboards/home/sitepoints.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -54,16 +54,18 @@ import WithdrawalEmoney from '@/pages/transaction/withdrawl-emoney/WithdrawalEmo
|
|||||||
import ManageCards from '@/pages/cards/manage-card/ManageCard';
|
import ManageCards from '@/pages/cards/manage-card/ManageCard';
|
||||||
import HistoryCard from '@/pages/cards/history-card/HistoryCard';
|
import HistoryCard from '@/pages/cards/history-card/HistoryCard';
|
||||||
|
|
||||||
// DISBURSEMENT
|
import ComingSoonPage from '@/pages/ComingSoonPage';
|
||||||
|
|
||||||
const AppRoutingSetup = (): ReactElement => {
|
const AppRoutingSetup = (): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<RequireAuth />}>
|
{/* <Route element={<RequireAuth />}> */}
|
||||||
<Route element={<Demo2Layout />}>
|
<Route element={<Demo2Layout />}>
|
||||||
<Route path="/" element={<DashboardHomePage />} />
|
<Route path="/" element={<DashboardHomePage />} />
|
||||||
|
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
|
||||||
|
<Route path="/coming-soon" element={<ComingSoonPage />} />
|
||||||
|
|
||||||
<Route path="/master-data" element={<MasterData />} />
|
{/* <Route path="/master-data" element={<MasterData />} />
|
||||||
<Route path="/master-data/municipios" element={<Municipios />} />
|
<Route path="/master-data/municipios" element={<Municipios />} />
|
||||||
<Route path="/master-data/postoadms" element={<PostoAdmsMaster />} />
|
<Route path="/master-data/postoadms" element={<PostoAdmsMaster />} />
|
||||||
<Route path="/master-data/sucos" element={<SucosMaster />} />
|
<Route path="/master-data/sucos" element={<SucosMaster />} />
|
||||||
@ -87,7 +89,7 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
|
|
||||||
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
|
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
|
||||||
|
|
||||||
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
|
|
||||||
|
|
||||||
<Route path="/groups/group-management" element={<ManageGroups />} />
|
<Route path="/groups/group-management" element={<ManageGroups />} />
|
||||||
|
|
||||||
@ -98,10 +100,10 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
|
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
|
||||||
<Route path="/members/agent-balance" element={<AgentBalance />} />
|
<Route path="/members/agent-balance" element={<AgentBalance />} />
|
||||||
<Route path="/members/search-member" element={<SearchMember />} />
|
<Route path="/members/search-member" element={<SearchMember />} />
|
||||||
<Route path="/members/search-member" element={<SearchMember />} />
|
<Route path="/members/search-member" element={<SearchMember />} /> */}
|
||||||
|
|
||||||
{/* cards */}
|
{/* cards */}
|
||||||
<Route path="/cards/manage-card" element={<ManageCards />} />
|
{/* <Route path="/cards/manage-card" element={<ManageCards />} />
|
||||||
|
|
||||||
<Route path="/cards/card-history" element={<HistoryCard />} />
|
<Route path="/cards/card-history" element={<HistoryCard />} />
|
||||||
|
|
||||||
@ -131,16 +133,16 @@ const AppRoutingSetup = (): ReactElement => {
|
|||||||
<Route
|
<Route
|
||||||
path="/settings/user-management/manage-position"
|
path="/settings/user-management/manage-position"
|
||||||
element={<ManagePositionPage />}
|
element={<ManagePositionPage />}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* DISBURSEMENT */}
|
{/* DISBURSEMENT */}
|
||||||
<Route
|
{/* <Route
|
||||||
path="/disbursement/transaction-history"
|
path="/disbursement/transaction-history"
|
||||||
element={<HistoryTransactionDisbursement />}
|
element={<HistoryTransactionDisbursement />}
|
||||||
/>
|
/> */}
|
||||||
{/* DISBURSEMENT */}
|
{/* DISBURSEMENT */}
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
{/* </Route> */}
|
||||||
<Route path="error/*" element={<ErrorsRouting />} />
|
<Route path="error/*" element={<ErrorsRouting />} />
|
||||||
<Route path="auth/*" element={<AuthPage />} />
|
<Route path="auth/*" element={<AuthPage />} />
|
||||||
<Route path="*" element={<Navigate to="/error/404" />} />
|
<Route path="*" element={<Navigate to="/error/404" />} />
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"types": ["google.maps", "vite/client"],
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
Reference in New Issue
Block a user