Compare commits

..

22 Commits

Author SHA1 Message Date
643dd15252 update sites crud done 2026-07-08 11:26:25 +07:00
9b5941956f update, crud user done 2026-07-08 10:39:03 +07:00
bf8daa74fa update get data site points from api 2026-07-08 10:07:08 +07:00
bb2aa0e3cd update 2026-07-07 12:05:51 +07:00
8a7dfc2c86 update, consume for login done 2026-07-07 11:34:38 +07:00
0ec6ef1841 update revenue dashboard 2026-07-07 09:06:45 +07:00
c9e28043e4 update the import statement for sitePoints to match the correct file name (sitepoints instead of sitePoints) 2026-07-03 11:20:01 +07:00
fb11249d3f update 2026-07-02 16:42:28 +07:00
3f9452cf8e update logo 2026-07-02 16:22:21 +07:00
ea1a678a25 update 2026-07-02 15:20:41 +07:00
8ca596af86 update: src/pages/dashboards/home/DashboardHomePage.tsx 2026-07-02 14:38:31 +07:00
46736523fe dashboard page view done 2026-07-02 14:36:32 +07:00
306080f592 update agent balance column 2026-05-29 10:58:00 +07:00
479d27892c update issue: transaction kind ===k 2026-05-25 08:52:56 +07:00
5c3e03dd3b update 2026-05-18 15:41:47 +07:00
b9edf2ec89 update 2026-05-18 10:02:28 +07:00
c2c8a3fe31 Merge branch 'master' of https://git.telkomcel.tl/TPAY/dashboard 2026-04-10 09:17:06 +07:00
cdf06c867f update 2026-04-10 09:14:18 +07:00
367a8b9bb0 delete yarn-lock and package-lock 2026-03-10 14:09:24 +07:00
6796bacc20 Delete package-lock.json 2026-03-08 15:11:48 +00:00
57c14b8fde Delete yarn.lock 2026-03-08 15:11:38 +00:00
699857e085 update conflict 2026-03-08 14:05:59 +07:00
67 changed files with 2059 additions and 18900 deletions

View File

@ -1,10 +1,12 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
# VITE_APP_API_URL= # VITE_APP_API_URL=
# VITE_APP_API_URL=http://127.0.0.1:4003/apitest # VITE_APP_API_URL=http://127.0.0.1:4003/apitest
VITE_APP_API_URL=https://tpay.shiblysolution.id/api # VITE_APP_API_URL=https://tpay.shiblysolution.id/api
VITE_APP_API_URL=http://127.0.0.1:7001/api
VITE_APP_API_URL_MASTER_DATA=http://127.0.0.1:7002/api
# VITE_APP_API_URL=http://api-v2.tpay.tl/api # VITE_APP_API_URL=http://api-v2.tpay.tl/api
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api # VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting # VITE_APP_API_URL=http://127.0.0.1:4003/apitesting

View File

@ -1,4 +1,4 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
VITE_APP_API_URL=https://api-v2.tpay.tl/api VITE_APP_API_URL=https://api-v2.tpay.tl/api

View File

@ -1,8 +1,8 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
VITE_APP_API_URL=https://tpay.shiblysolution.id/api VITE_APP_API_URL=http://127.0.0.1:7001/api
# VITE_APP_API_URL=http://api-v2.tpay.tl/api # VITE_APP_API_URL=http://api-v2.tpay.tl/api
VITE_BASE_URL=/dashboard/ VITE_BASE_URL=/dashboard/
VITE_ENV=staging VITE_ENV=staging

View File

@ -2,8 +2,8 @@
<html class="h-full" lang="en" dir="ltr"> <html class="h-full" lang="en" dir="ltr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/media/app/favicon.ico" /> <!-- <link rel="icon" href="/media/app/favicon.ico" /> -->
<!-- <link rel="icon" href="/media/app/app-logo.png.png" /> --> <link rel="icon" href="/media/app/logo-tt.jpeg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="description" content="" /> <meta name="description" content="" />

10718
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -8,6 +8,8 @@ const RequireAuth = () => {
const { auth, loading } = useAuthContext(); const { auth, loading } = useAuthContext();
const location = useLocation(); const location = useLocation();
// console.log('RequireAuth render: loading =', loading, ' auth =', auth);
if (loading) { if (loading) {
return <ScreenLoader />; return <ScreenLoader />;
} }

View File

@ -2,14 +2,21 @@ import { type TLanguageCode } from '@/i18n';
export interface AuthModel { export interface AuthModel {
id: string; id: string;
access_token: string; // access_token: string;
refresh_token: string; // refresh_token: string;
role_name: string; role_name: string;
roles_list: []; // roles_list: [];
company: any; // company: any;
user: any; // user: any;
// api_token: string; // // api_token: string;
statusbalance: string; // statusbalance: string;
username?: string;
password?: string;
user?: UserModel;
statusbalance?: string;
access_token?: string;
token_type?: string;
// language?: TLanguageCode;
} }
export interface UserModel { export interface UserModel {
@ -18,18 +25,21 @@ export interface UserModel {
name: string; name: string;
password: string | undefined; password: string | undefined;
email: string; email: string;
id_role: string; customer?: {
id: string;
};
// id_role: string;
role_name: string; role_name: string;
roles_list: []; // roles_list: [];
token: AuthModel; // token: AuthModel;
first_name: string; // first_name: string;
last_name: string; // last_name: string;
fullname?: string; // fullname?: string;
occupation?: string; // occupation?: string;
companyName?: string; // companyName?: string;
phone?: string; // phone?: string;
roles?: number[]; // roles?: number[];
pic?: string; // pic?: string;
language?: TLanguageCode; // language?: TLanguageCode;
} }

View File

@ -83,7 +83,7 @@ const Login = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>TPAY | Sign In</title> <title>REVENUE | Sign In</title>
</Helmet> </Helmet>
<div className="card max-w-[390px] w-full"> <div className="card max-w-[390px] w-full">
<form <form
@ -145,54 +145,6 @@ const Login = () => {
)} )}
</div> </div>
<div className="flex flex-col gap-1">
<label className="form-label text-gray-900 ps-2.5">Code Verify</label>
<label className="input">
<input
placeholder="Enter code verify"
type="text"
inputMode="numeric"
autoComplete="off"
maxLength={6}
{...formik.getFieldProps('token')}
className={clsx('form-control', {
'is-invalid': formik.touched.token && formik.errors.token
})}
onKeyPress={(e) => {
if (!/[0-9]/.test(e.key)) {
e.preventDefault();
}
}}
/>
</label>
{formik.touched.token && formik.errors.token && (
<span role="alert" className="text-danger text-xs mt-1">
{formik.errors.token}
</span>
)}
</div>
<div className="flex items-center justify-between gap-1">
<label className="checkbox-group">
<input
className="checkbox checkbox-sm"
type="checkbox"
{...formik.getFieldProps('remember')}
/>
<span className="checkbox-label">Remember me</span>
</label>
{/* <Link
to={
currentLayout?.name === 'auth-branded'
? '/auth/reset-password'
: '/auth/classic/reset-password'
}
className="text-2sm link shrink-0"
>
Forgot Password?
</Link> */}
</div>
<button <button
type="submit" type="submit"
className="btn btn-primary flex justify-center grow" className="btn btn-primary flex justify-center grow"
@ -202,7 +154,7 @@ const Login = () => {
</button> </button>
<div> <div>
<p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}> <p className="text-2sm text-center" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
Copyright {moment().year()} &copy; Telkomcel All rights reserved. Copyright {moment().year()} &copy; Timor Telecom All rights reserved.
</p> </p>
</div> </div>
</form> </form>

View File

@ -14,12 +14,10 @@ import { type AuthModel, type UserModel } from '@/auth';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_user;
export const LOGIN_URL = `${API_URL}/login`; export const LOGIN_URL = `${API_URL}/login`;
export const FORGOT_PASSWORD_URL = `${API_URL}/reset_password`; export const GET_USER_URL = `${API_URL}/user/profile`;
export const RESET_PASSWORD_URL = `${API_URL}/update_password`;
export const GET_USER_URL = `${API_URL}/user/detail`;
interface AuthContextProps { interface AuthContextProps {
loading: boolean; loading: boolean;
@ -29,9 +27,6 @@ interface AuthContextProps {
currentUser: UserModel | undefined; currentUser: UserModel | undefined;
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>; setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
login: (email: string, password: string, token?: string) => Promise<void>; login: (email: string, password: string, token?: string) => Promise<void>;
requestPasswordResetLink: (email: string) => Promise<void>;
changePassword: (token: string, password: string, password_confirmation: string) => Promise<void>;
getUser: () => Promise<AxiosResponse<any> | {}>;
logout: () => void; logout: () => void;
verify: () => Promise<void>; verify: () => Promise<void>;
} }
@ -55,15 +50,13 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
const login = async (username: string, password: string, token?: string) => { const login = async (username: string, password: string, token?: string) => {
try { try {
const { data: auth } = await axios const { data: auth } = await axios
.post(LOGIN_URL, { username, password, token }) .post(LOGIN_URL, { username, password })
.then((response) => response.data); .then((response) => response.data);
const enhancedAuth: AuthModel = { const enhancedAuth: AuthModel = {
...auth.token, ...auth.token,
id: auth.user.id, id: auth.userid,
role_name: auth.role_name,
user: auth.user, user: auth.user,
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
}; };
saveAuth(enhancedAuth); saveAuth(enhancedAuth);
@ -81,20 +74,9 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
} }
}; };
const requestPasswordResetLink = async (email: string) => {
await axios.put(FORGOT_PASSWORD_URL + '/' + email);
};
const changePassword = async (token: string, password: string, retype_password: string) => {
await axios.put(`${RESET_PASSWORD_URL}/${token}`, {
password,
retype_password
});
};
const getUser = async () => { const getUser = async () => {
let _axios = await axios let _axios = await axios
.get(`${GET_USER_URL}/${authHelper.getAuth()?.id}`) .get(`${GET_USER_URL}/${authHelper.getAuth()?.user?.id}`)
.then((response) => response.data.data); .then((response) => response.data.data);
return { data: _axios }; return { data: _axios };
@ -105,18 +87,14 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
try { try {
const { data: user } = await getUser(); const { data: user } = await getUser();
// Perbarui auth yang sekarang dengan statusbalance
saveAuth({ saveAuth({
...auth, ...auth,
statusbalance: user.role.status_balance
}); });
const createCacheUser = { const createCacheUser = {
name: user.name, name: user.name,
email: user.email, email: user.email,
username: user.username, username: user.username,
role_name: auth.role_name,
statusbalance: user.role.status_balance
}; };
localStorage.setItem('user', JSON.stringify(createCacheUser)); localStorage.setItem('user', JSON.stringify(createCacheUser));
@ -149,9 +127,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
currentUser, currentUser,
setCurrentUser, setCurrentUser,
login, login,
requestPasswordResetLink,
changePassword,
getUser,
logout, logout,
verify verify
}} }}

View File

@ -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> */}

View File

@ -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> */}

View File

@ -1,36 +1,41 @@
interface apiConfigProps { interface apiConfigProps {
service_dashboard: string; service_user: string;
service_customer: string;
service_master_data: string; service_master_data: string;
// service_master_data2: string;
service_transaction: string;
service_wallet: string;
transaction: string;
nationality: string;
service_disbursement: string;
service_notification: string;
service_feedback: string;
service_card: string;
api_dashboard:string
// service_dashboard: string;
// service_customer: string;
// // service_master_data2: string;
// service_transaction: string;
// service_wallet: string;
// transaction: string;
// nationality: string;
// service_disbursement: string;
// service_notification: string;
// service_feedback: string;
// service_card: string;
// api_dashboard:string;
} }
const API_URL = import.meta.env.VITE_APP_API_URL; const API_URL = import.meta.env.VITE_APP_API_URL;
const API_URL_MASTER_DATA = import.meta.env.VITE_APP_API_URL_MASTER_DATA;
const apiConfig: apiConfigProps = { const apiConfig: apiConfigProps = {
service_feedback: `${API_URL}/k`, service_user: `${API_URL}`,
service_master_data: `${API_URL_MASTER_DATA}`,
// service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`, // service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`,
service_dashboard: `${API_URL}/d`, // service_feedback: `${API_URL}/k`,
service_customer: `${API_URL}/c`, // service_customer: `${API_URL}/c`,
// service_master_data2: `${API_URL}/m`, // // service_master_data2: `${API_URL}/m`,
service_master_data: `${API_URL}/t`, // // service_master_data: `${API_URL}/t`,
service_transaction: `${API_URL}/tt`, // service_transaction: `${API_URL}/tt`,
service_wallet: `${API_URL}/w`, // service_wallet: `${API_URL}/w`,
transaction: `${API_URL}/x`, // transaction: `${API_URL}/x`,
service_disbursement: `${API_URL}/s`, // service_disbursement: `${API_URL}/s`,
service_notification: `${API_URL}/n`, // service_notification: `${API_URL}/n`,
api_dashboard: `${API_URL}/r`, // api_dashboard: `${API_URL}/r`,
service_card: `${API_URL}/a`, // service_card: `${API_URL}/a`,
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/` // nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
}; };
export { apiConfig }; export { apiConfig };

View File

@ -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 TPAYs {/* A user-friendly interface providing seamless access to TPAYs */}
<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>

View File

@ -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'
)} )}

View File

@ -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>

View File

@ -10,7 +10,6 @@ const HeaderTopbar = () => {
const itemChatRef = useRef<any>(null); const itemChatRef = useRef<any>(null);
const itemUserRef = useRef<any>(null); const itemUserRef = useRef<any>(null);
const itemNotificationsRef = useRef<any>(null); const itemNotificationsRef = useRef<any>(null);
const { isRTL } = useLanguage(); const { isRTL } = useLanguage();
const [isSticky, setIsSticky] = useState(false); const [isSticky, setIsSticky] = useState(false);
@ -32,10 +31,10 @@ const HeaderTopbar = () => {
<span <span
className={`font-semibold text-lg leading-tight ${isSticky ? 'text-black' : 'text-white'}`} className={`font-semibold text-lg leading-tight ${isSticky ? 'text-black' : 'text-white'}`}
> >
{getAuth()?.user.name} {getAuth()?.user?.name}
</span> </span>
<span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}> <span className={`text-xs ${isSticky ? 'text-gray-700' : 'text-white'}`}>
as {getAuth()?.role_name} {getAuth()?.user?.email}
</span> </span>
</div> </div>

View File

@ -8,56 +8,74 @@ import {
MenuSub, MenuSub,
MenuTitle MenuTitle
} from '@/components/menu'; } from '@/components/menu';
import { useMenus } from '@/providers'; // import { useMenus } from '@/providers';
// import { useLocation } from 'react-router'; // import { useLocation } from 'react-router';
import { useLanguage } from '@/i18n'; import { useLanguage } from '@/i18n';
import { doGetNavbarMenu } from '@/actions/NavbarMenuActions'; import { MappedMenu } from '@/types/GlobalTypes';
import { RoleList, MappedMenu } from '@/types/GlobalTypes';
import { useEffect, useState } from 'react'; // ---------------------------------------------------------------------------
// Static menu structure (previously fetched from the API via
// doGetNavbarMenu). Update `path` values to match your actual route
// definitions:
// - "Dashboard" points to the real dashboard route.
// - Every other item points to "/coming-soon", a shared placeholder page
// that just renders "This page will be available soon".
// ---------------------------------------------------------------------------
const COMING_SOON_PATH = '/coming-soon';
const STATIC_MENU: MappedMenu[] = [
{
title: 'Main Dashboard',
path: '-',
children: [{ title: 'Dashboard', path: '/' }]
},
{
title: 'Manage Sites',
path: '-',
children: [{ title: 'Manage Sites', path: '/sites/manage-sites' }]
},
{
title: 'Revenue',
path: '-',
children: [
{ title: 'SMS', path: COMING_SOON_PATH },
{ title: 'Voice', path: COMING_SOON_PATH },
{ title: 'Digital Content', path: COMING_SOON_PATH },
{ title: 'Refill Packet', path: COMING_SOON_PATH },
{ title: 'Refill Voucher', path: COMING_SOON_PATH }
]
},
{
title: 'Dealer & Outlet',
path: '-',
children: [
{ title: 'Dealer', path: COMING_SOON_PATH },
{ title: 'Outlet', path: COMING_SOON_PATH }
]
},
{
title: 'User Management',
path: '-',
children: [
{ title: 'User List', path: '/settings/user-management/manage-user' },
{ title: 'Group Role List', path: COMING_SOON_PATH },
{ title: 'Role List', path: COMING_SOON_PATH }
]
},
{
title: 'Manage Profile',
path: '-',
children: [{ title: 'Manage Cell', path: COMING_SOON_PATH }]
}
];
const NavbarMenu = () => { const NavbarMenu = () => {
// const { pathname } = useLocation(); // const { pathname } = useLocation();
// const { getMenuConfig } = useMenus(); // const { getMenuConfig } = useMenus();
// const primaryMenu = getMenuConfig('primary'); // const primaryMenu = getMenuConfig('primary');
const { isRTL } = useLanguage(); const { isRTL } = useLanguage();
const [menuData, setMenuData] = useState<RoleList[]>([]);
const [error, setError] = useState<string | null>(null);
let navbarMenu;
useEffect(() => { const navbarMenu: TMenuConfig = STATIC_MENU;
const fetchNavbarMenu = async () => {
try {
const result = await doGetNavbarMenu();
if (result.status && result.data) {
setMenuData(result.data.roles_list);
} else {
setError(result.message);
}
} catch (err) {
setError('Failed to fetch navbar menu');
}
};
fetchNavbarMenu();
}, []);
const mapMenuData = (data: RoleList[]): MappedMenu[] => {
return data.map((item) => {
const mappedItem: MappedMenu = {
title: item.name,
path: item.link !== '-' ? item.link : '/'
};
if (item.children && item.children.length > 0) {
mappedItem.children = mapMenuData(item.children);
}
return mappedItem;
});
};
navbarMenu = mapMenuData(menuData);
// navbarMenu = primaryMenu?.[0].children;
const buildMenu = (items: TMenuConfig) => { const buildMenu = (items: TMenuConfig) => {
return items.map((item, index) => { return items.map((item, index) => {
@ -149,7 +167,7 @@ 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>

View File

@ -59,7 +59,3 @@ export function uniqueID(): string {
const baseUrl = import.meta.env?.VITE_BASE_URL || '/'; const baseUrl = import.meta.env?.VITE_BASE_URL || '/';
export const TEMPLATE_CSV_URL = `${baseUrl}template_notification.csv`; export const TEMPLATE_CSV_URL = `${baseUrl}template_notification.csv`;
<<<<<<< HEAD
=======
>>>>>>> 10532a7cb1d720488c181b1fcac3b8046c888e77

View 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;

View File

@ -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>

View File

@ -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>

View File

@ -1,405 +1,17 @@
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> </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>
{/* 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>
</> </>
); );

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View 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} &middot; ${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 &copy; Esri &mdash; 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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -1,4 +0,0 @@
export * from './Card';
export * from './DateRangePicker';
export * from './Chart';
export * from './YearPicker';

View File

@ -1,5 +1,2 @@
export * from './DashboardHomeContext'; export * from './DashboardHomeContext';
export * from './useDashboardHomeContext'; export * from './useDashboardHomeContext';
export * from './useFetchCardData';
export * from './useFetchChartData';
// export * from './useFetchYear';

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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']
}
};

View File

@ -24,6 +24,7 @@ import { toast } from 'sonner';
import { Container, DataGridLoader, DataGridProvider, LoaderTransparant } from '@/components'; import { Container, DataGridLoader, DataGridProvider, LoaderTransparant } from '@/components';
import { ListToolBar } from './ListToolbar'; import { ListToolBar } from './ListToolbar';
import { toAbsoluteUrl } from '@/utils'; import { toAbsoluteUrl } from '@/utils';
const BASE_URL = apiConfig.service_customer; const BASE_URL = apiConfig.service_customer;
let initBalance = { let initBalance = {
@ -52,7 +53,6 @@ const AgentBalance = () => {
const [barcode, setBarcode] = useState(''); const [barcode, setBarcode] = useState('');
const [requested_date, setRequested_date] = useState(''); const [requested_date, setRequested_date] = useState('');
const [token_legacy, setToken_legacy] = useState(''); const [token_legacy, setToken_legacy] = useState('');
// const [selectedMember, setSelectedMember] = useState(initBalance);
useEffect(() => { useEffect(() => {
// fetchAgentBalance(); // fetchAgentBalance();
@ -63,7 +63,7 @@ const AgentBalance = () => {
let balances = await axios.get(`${BASE_URL}/customer/agent-balance`, { let balances = await axios.get(`${BASE_URL}/customer/agent-balance`, {
params: { params: {
limit: limit, limit: limit,
page: page + 1, // +1 because your API uses 1-based pages page: page + 1,
with_deleted: false, with_deleted: false,
order_field: 'created_at', order_field: 'created_at',
order_direction: 'DESC', order_direction: 'DESC',
@ -80,6 +80,74 @@ const AgentBalance = () => {
return { data: resBalance, totalCount: balances?.data.data.total_count }; return { data: resBalance, totalCount: balances?.data.data.total_count };
}; };
// ─── Export ───────────────────────────────────────────────────────────────────────────────
const fetchExport = async (filter: any) => {
try {
toast.info('Preparing export…');
// API expects filter as array of { id, value } — same shape as columnFilters
const filterArray = Object.entries(filter)
.filter(([, value]) => value !== '' && value != null)
.map(([id, value]) => ({ id, value }));
const res = await axios.get(`${BASE_URL}/customer/agent-balance/export-excel`, {
params: {
with_deleted: false,
order_field: 'created_at',
order_direction: 'DESC',
filter: JSON.stringify(filterArray),
specialFilter: true
},
responseType: 'blob'
});
const url = URL.createObjectURL(new Blob([res.data]));
const link = document.createElement('a');
link.href = url;
link.download = `agent_balance_export_${new Date().toISOString().slice(0, 10)}.xlsx`;
link.click();
URL.revokeObjectURL(url);
toast.success('Export complete');
} catch (error: any) {
const msg = error?.response?.data?.error || error.message;
toast.error(`Export failed: ${msg}`);
}
};
// ─────────────────────────────────────────────────────────────────────────────
const fetchExportPdf = async (filter: any) => {
try {
toast.info('Preparing export…');
// API expects filter as array of { id, value } — same shape as columnFilters
const filterArray = Object.entries(filter)
.filter(([, value]) => value !== '' && value != null)
.map(([id, value]) => ({ id, value }));
const res = await axios.get(`${BASE_URL}/customer/agent-balance/export-pdf`, {
params: {
with_deleted: false,
order_field: 'created_at',
order_direction: 'DESC',
filter: JSON.stringify(filterArray),
specialFilter: true
},
responseType: 'blob'
});
const url = URL.createObjectURL(new Blob([res.data]));
const link = document.createElement('a');
link.href = url;
link.download = `agent_balance_export_${new Date().toISOString().slice(0, 10)}.pdf`;
link.click();
URL.revokeObjectURL(url);
toast.success('Export complete');
} catch (error: any) {
const msg = error?.response?.data?.error || error.message;
toast.error(`Export failed: ${msg}`);
}
};
const openDialog = () => setIsDialogOpen(true); const openDialog = () => setIsDialogOpen(true);
const closeDialog = () => { const closeDialog = () => {
setIsDialogOpen(false); setIsDialogOpen(false);
@ -108,7 +176,6 @@ const AgentBalance = () => {
await fetchAgentBalance(currentPage, pageSize, '', ''); await fetchAgentBalance(currentPage, pageSize, '', '');
closeDialog(); closeDialog();
setIsReloading(false); setIsReloading(false);
// fetchAgentBalance();
}; };
function formatDate(value: string) { function formatDate(value: string) {
@ -139,24 +206,14 @@ const AgentBalance = () => {
<title>TPAY | Agent Balance</title> <title>TPAY | Agent Balance</title>
</Helmet> </Helmet>
<Container> <Container>
{/* <ConfirmDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
title="Confirm Action"
content={''}
onYes={handleYes}
onNo={() => setDialogOpen(false)}
/> */}
<h1 className="text-xl font-medium leading-none text-gray-900 mb-5">Agent Balances</h1> <h1 className="text-xl font-medium leading-none text-gray-900 mb-5">Agent Balances</h1>
<Breadcrumbs> <Breadcrumbs>
<Link underline="none" color="inherit" href="/"> <Link underline="none" color="inherit" href="/">
<span className="text-sm hover:underline">Dashboard</span> <span className="text-sm hover:underline">Dashboard</span>
</Link> </Link>
<Link underline="none" color="inherit"> <Link underline="none" color="inherit">
<span className="text-sm">Members</span> <span className="text-sm">Members</span>
</Link> </Link>
<Link underline="none" color="inherit"> <Link underline="none" color="inherit">
<span className="text-sm">Agent Balances</span> <span className="text-sm">Agent Balances</span>
</Link> </Link>
@ -166,7 +223,6 @@ const AgentBalance = () => {
{isReloading && <DataGridLoader />} {isReloading && <DataGridLoader />}
{!isReloading && ( {!isReloading && (
<DataGridProvider <DataGridProvider
// data={dataBalance}
pagination={{ size: pageSize }} pagination={{ size: pageSize }}
columns={getColumns(handleUpdate)} columns={getColumns(handleUpdate)}
layout={{ card: true }} layout={{ card: true }}
@ -179,6 +235,8 @@ const AgentBalance = () => {
createGroup={createGroup} createGroup={createGroup}
onReload={handleReload} onReload={handleReload}
isReloading={isReloading} isReloading={isReloading}
onExport={fetchExport}
onExportPdf={fetchExportPdf}
/> />
} }
onRowSelectionChange={(selected, table: any) => { onRowSelectionChange={(selected, table: any) => {

View File

@ -13,7 +13,8 @@ export type AgentBalance = {
balance_emoney: string, balance_emoney: string,
balance_merchant: string, balance_merchant: string,
balance_point: string, balance_point: string,
balance_deposit: string balance_deposit: string,
balance_edtl: string,
}; };
export const getColumns = (handleUpdate: (data: any) => void): ColumnDef<AgentBalance>[] => [ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef<AgentBalance>[] => [
@ -129,6 +130,20 @@ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef<AgentBa
); );
} }
}, },
{
accessorKey: 'balance_edtl',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
EDTL
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{ {
accessorKey: 'balance_point', accessorKey: 'balance_point',
header: ({ column }) => { header: ({ column }) => {

View File

@ -6,14 +6,14 @@ interface ListToolBarProps {
createGroup: () => void; createGroup: () => void;
onReload: () => void; onReload: () => void;
isReloading: boolean; isReloading: boolean;
onExport: (filter: Record<string, string>) => void;
onExportPdf: (filter: Record<string, string>) => void;
} }
const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) => { const ListToolBar = ({ createGroup, onReload, isReloading, onExport, onExportPdf }: ListToolBarProps) => {
const { table, reload } = useDataGrid(); const { table, reload } = useDataGrid();
const [usernameFilter, setUsernameFilter] = useState(''); const [usernameFilter, setUsernameFilter] = useState('');
const [msisdn, setMsisdn] = useState(''); const [msisdn, setMsisdn] = useState('');
const [typeSearchValue, setSearchTypeValue] = useState<string>();
const [searchValue, setSearchValue] = useState<string>();
const [fullname, setFullname] = useState(''); const [fullname, setFullname] = useState('');
const [merchantName, setMerchantName] = useState(''); const [merchantName, setMerchantName] = useState('');
@ -33,14 +33,31 @@ const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) =
setMerchantName(e.target.value); setMerchantName(e.target.value);
}; };
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { /** Build the same filter object the table uses for server-side fetching */
e.preventDefault() const buildFilter = (): Record<string, string> => {
table.resetPageIndex() const filter: Record<string, string> = {};
if (msisdn) filter['msisdn'] = msisdn;
if (fullname) filter['fullname'] = fullname;
if (usernameFilter) filter['username'] = usernameFilter;
if (merchantName) filter['agent_name'] = merchantName;
return filter;
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
table.resetPageIndex();
table.getColumn('username')?.setFilterValue(usernameFilter); table.getColumn('username')?.setFilterValue(usernameFilter);
table.getColumn('msisdn')?.setFilterValue(msisdn); table.getColumn('msisdn')?.setFilterValue(msisdn);
table.getColumn('fullname')?.setFilterValue(fullname); table.getColumn('fullname')?.setFilterValue(fullname);
table.getColumn('agent_name')?.setFilterValue(merchantName); table.getColumn('agent_name')?.setFilterValue(merchantName);
// if (!merchantName && !fullname && !msisdn && !usernameFilter) reload() };
const handleExport = () => {
onExport(buildFilter());
};
const handleExportPdf = () => {
onExportPdf(buildFilter());
}; };
return ( return (
@ -48,25 +65,33 @@ const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) =
<div className="flex flex-wrap gap-2 lg:gap-5 w-full"> <div className="flex flex-wrap gap-2 lg:gap-5 w-full">
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<form onSubmit={(e:any) => handleSearch(e)}> <form onSubmit={handleSearch}>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<label className="input input-sm w-1/3"> <label className="input input-sm w-1/3">
<input type="text" placeholder="Search phone" value={msisdn} onChange={filtermsisdn}/> <input type="text" placeholder="Search phone" value={msisdn} onChange={filtermsisdn} />
</label> </label>
<label className="input input-sm w-1/3"> <label className="input input-sm w-1/3">
<input type="text" placeholder="Search fullname" value={fullname} onChange={filterfullname}/> <input type="text" placeholder="Search fullname" value={fullname} onChange={filterfullname} />
</label> </label>
<label className="input input-sm w-1/3"> <label className="input input-sm w-1/3">
<input type="text" placeholder="Search username" value={usernameFilter} onChange={handleUsernameChange}/> <input type="text" placeholder="Search username" value={usernameFilter} onChange={handleUsernameChange} />
</label> </label>
<label className="input input-sm w-1/3"> <label className="input input-sm w-1/3">
<input type="text" placeholder="Search merchant name" value={merchantName} onChange={filtermerchantname}/> <input type="text" placeholder="Search merchant name" value={merchantName} onChange={filtermerchantname} />
</label> </label>
<Button variant="outline" className="h-7.5"><KeenIcon icon="magnifier" /></Button> <Button variant="outline" className="h-7.5" type="submit">
<KeenIcon icon="magnifier" />
</Button>
<Button className="h-7.5" type="button" onClick={handleExport}>
Export Excel
</Button>
<Button className="h-7.5" type="button" onClick={handleExportPdf}>
Export PDF
</Button>
</div> </div>
</form> </form>
<div className="flex gap-3 items-center"> <div className="flex gap-2 items-center">
<DefaultTooltip title={isReloading ? 'Refreshing...' : 'Refresh'} placement={'top'}> <DefaultTooltip title={isReloading ? 'Refreshing...' : 'Refresh'} placement={'top'}>
<Button variant="outline" className="h-7.5" onClick={onReload} disabled={isReloading}> <Button variant="outline" className="h-7.5" onClick={onReload} disabled={isReloading}>
{isReloading ? ( {isReloading ? (
@ -79,6 +104,7 @@ const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) =
</Button> </Button>
</DefaultTooltip> </DefaultTooltip>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export default function ManageUserPage() {
return ( return (
<> <>
<Helmet> <Helmet>
<title>TPAY | Manage User</title> <title>REVENUE | Manage User</title>
</Helmet> </Helmet>
<ManageUserContextProvider> <ManageUserContextProvider>
<Container> <Container>
@ -20,9 +20,9 @@ export default function ManageUserPage() {
<span className="text-sm hover:underline">Dashboard</span> <span className="text-sm hover:underline">Dashboard</span>
</Link> </Link>
<Link underline="none" color="inherit"> {/* <Link underline="none" color="inherit">
<span className="text-sm">Settings</span> <span className="text-sm">Settings</span>
</Link> </Link> */}
<Link underline="none" color="inherit"> <Link underline="none" color="inherit">
<span className="text-sm">User Management</span> <span className="text-sm">User Management</span>

View File

@ -41,8 +41,7 @@ import {
validateFormCreateUser validateFormCreateUser
} from './Types'; } from './Types';
const API_URL_CUSTOMER = apiConfig.service_customer; const API_URL_USER = apiConfig.service_user;
const API_URL = apiConfig.service_dashboard;
type PasswordType = 'password' | 'retype_password'; type PasswordType = 'password' | 'retype_password';
@ -116,7 +115,7 @@ const AddDialog = () => {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const response = await PostData(`${API_URL}/user/create`, formField); const response = await PostData(`${API_URL_USER}/user/create`, formField);
if (response?.status) { if (response?.status) {
handleAddDialog(false); handleAddDialog(false);
@ -143,74 +142,74 @@ const AddDialog = () => {
[formField] [formField]
); );
const fetchRoles = useCallback(async () => { // const fetchRoles = useCallback(async () => {
const params = { // const params = {
limit: 100, // limit: 100,
page: 1, // page: 1,
with_deleted: false, // with_deleted: false,
order_field: 'name', // order_field: 'name',
order_direction: 'ASC', // order_direction: 'ASC',
filter: JSON.stringify({ // filter: JSON.stringify({
status: 'Y' // status: 'Y'
}) // })
}; // };
const response = await GetData(`${API_URL}/user_role/list`, params); // const response = await GetData(`${API_URL}/user_role/list`, params);
if (response?.status) { // if (response?.status) {
const roleList = response.data?.list || []; // const roleList = response.data?.list || [];
setRoles(roleList); // setRoles(roleList);
} else { // } else {
setRoles(() => []); // setRoles(() => []);
} // }
}, []); // }, []);
const getCustomerList = async (sorting: any, filterValue: string) => { // const getCustomerList = async (sorting: any, filterValue: string) => {
const filter: any = // const filter: any =
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } }; // filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting; // sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
const query: any = { // const query: any = {
limit: 100, // limit: 100,
page: 1, // page: 1,
with_deleted: false, // with_deleted: false,
order_field: sorting[0].id, // order_field: sorting[0].id,
order_direction: sorting[0].desc ? 'DESC' : 'ASC' // order_direction: sorting[0].desc ? 'DESC' : 'ASC'
}; // };
if (filter && Object.keys(filter).length > 0) { // if (filter && Object.keys(filter).length > 0) {
query.filter = JSON.stringify(filter); // query.filter = JSON.stringify(filter);
} // }
try { // try {
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); // const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
if (response?.status) { // if (response?.status) {
setCustomers(response?.data.list); // setCustomers(response?.data.list);
} else { // } else {
toast.error(response?.message); // toast.error(response?.message);
} // }
} catch (error) { // } catch (error) {
toast.error('Failed to fetch customer list'); // toast.error('Failed to fetch customer list');
} finally { // } finally {
setIsLoading(false); // setIsLoading(false);
} // }
}; // };
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => { // const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true); // setIsLoading(true);
setSearchTerm(e.target.value); // setSearchTerm(e.target.value);
setSelectedCustomerName(e.target.value); // setSelectedCustomerName(e.target.value);
setDropdownOpen(true); // setDropdownOpen(true);
const timer = setTimeout(() => { // const timer = setTimeout(() => {
getCustomerList([{ id: 'fullname', desc: false }], e.target.value); // getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
}, 500); // }, 500);
return () => clearTimeout(timer); // return () => clearTimeout(timer);
}; // };
const handleCustomerSelect = (customer: CustomerProps) => { // const handleCustomerSelect = (customer: CustomerProps) => {
setFormField({ ...formField, customerid: customer.id }); // setFormField({ ...formField, customerid: customer.id });
setSelectedCustomerName(customer.fullname); // setSelectedCustomerName(customer.fullname);
setDropdownOpen(false); // setDropdownOpen(false);
}; // };
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -230,9 +229,9 @@ const AddDialog = () => {
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase())) .filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
.slice(0, 10); .slice(0, 10);
useEffect(() => { // useEffect(() => {
fetchRoles(); // fetchRoles();
}, [fetchRoles]); // }, [fetchRoles]);
useEffect(() => { useEffect(() => {
const validation = validatePassword(formField.password, formField.retype_password); const validation = validatePassword(formField.password, formField.retype_password);
@ -248,20 +247,20 @@ const AddDialog = () => {
} }
}, [showAddDialog]); }, [showAddDialog]);
useEffect(() => { // useEffect(() => {
getCustomerList([{ id: 'created_at', desc: false }], ''); // getCustomerList([{ id: 'created_at', desc: false }], '');
const handleClickOutside = (event: any) => { // const handleClickOutside = (event: any) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { // if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setDropdownOpen(false); // setDropdownOpen(false);
} // }
}; // };
document.addEventListener('mousedown', handleClickOutside); // document.addEventListener('mousedown', handleClickOutside);
return () => { // return () => {
document.removeEventListener('mousedown', handleClickOutside); // document.removeEventListener('mousedown', handleClickOutside);
}; // };
}, []); // }, []);
const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => { const togglePassword = useCallback((event: MouseEvent<HTMLButtonElement>, key: string) => {
event.preventDefault(); event.preventDefault();
@ -331,7 +330,7 @@ const AddDialog = () => {
</div> </div>
{/* Customer */} {/* Customer */}
<div className="flex gap-2.5"> {/* <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Customer</label> <label className="form-label flex items-center gap-1 max-w-56">Customer</label>
<div className="grow flex flex-col" ref={dropdownRef}> <div className="grow flex flex-col" ref={dropdownRef}>
<Input <Input
@ -366,7 +365,7 @@ const AddDialog = () => {
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span> <span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
)} )}
</div> </div>
</div> </div> */}
{/* Email */} {/* Email */}
<div className="flex gap-2.5"> <div className="flex gap-2.5">
@ -387,7 +386,7 @@ const AddDialog = () => {
</div> </div>
{/* Role */} {/* Role */}
<div className="flex gap-2.5"> {/* <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Role</label> <label className="form-label flex items-center gap-1 max-w-56">Role</label>
<div className="grow flex flex-col"> <div className="grow flex flex-col">
<Select <Select
@ -414,7 +413,7 @@ const AddDialog = () => {
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span> <span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
)} )}
</div> </div>
</div> </div> */}
{/* Status */} {/* Status */}
<div className="flex gap-2.5"> <div className="flex gap-2.5">

View File

@ -16,7 +16,7 @@ import { useCallApi } from '@/hooks';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
import { EnforceSwitch } from '@/components/switch'; import { EnforceSwitch } from '@/components/switch';
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_user;
const DeleteDialog = () => { const DeleteDialog = () => {
const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext(); const { showDeleteDialog, handleDeleteDialog, selectedUser } = useUserContext();

View File

@ -39,8 +39,8 @@ import {
validateFormCreateUser validateFormCreateUser
} from './Types'; } from './Types';
const API_URL_CUSTOMER = apiConfig.service_customer; // const API_URL_CUSTOMER = apiConfig.service_customer;
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_user;
const EditDialog = () => { const EditDialog = () => {
const parentRef = useRef<any | null>(null); const parentRef = useRef<any | null>(null);
@ -109,59 +109,59 @@ const EditDialog = () => {
[selectedUser, formField] [selectedUser, formField]
); );
const doFetchUserRole = useCallback(async (sorting: any) => { // const doFetchUserRole = useCallback(async (sorting: any) => {
try { // try {
sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting; // sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting;
const response = await GetData(`${API_URL}/user_role/list`, { // const response = await GetData(`${API_URL}/user_role/list`, {
limit: 100, // limit: 100,
page: 1, // page: 1,
with_deleted: false, // with_deleted: false,
order_field: sorting[0].id, // order_field: sorting[0].id,
order_direction: sorting[0].desc == false ? 'ASC' : 'DESC' // order_direction: sorting[0].desc == false ? 'ASC' : 'DESC'
}); // });
setRoles(response?.data.list); // setRoles(response?.data.list);
} catch (error) { // } catch (error) {
console.error('Error fetching role', error); // console.error('Error fetching role', error);
} // }
}, []); // }, []);
const getCustomerList = async (sorting: any, filterValue: string) => { // const getCustomerList = async (sorting: any, filterValue: string) => {
const filter: any = // const filter: any =
filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } }; // filterValue?.trim().length === 0 ? {} : { fullname: { like: `%${filterValue}%` } };
sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting; // sorting = sorting.length == 0 ? [{ id: 'created_at', desc: false }] : sorting;
const query: any = { // const query: any = {
limit: 100, // limit: 100,
page: 1, // page: 1,
with_deleted: false, // with_deleted: false,
order_field: sorting[0].id, // order_field: sorting[0].id,
order_direction: sorting[0].desc ? 'DESC' : 'ASC' // order_direction: sorting[0].desc ? 'DESC' : 'ASC'
}; // };
if (filter && Object.keys(filter).length > 0) { // if (filter && Object.keys(filter).length > 0) {
query.filter = JSON.stringify(filter); // query.filter = JSON.stringify(filter);
} // }
try { // try {
const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); // const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query);
if (response?.status) { // if (response?.status) {
setCustomers(response?.data.list); // setCustomers(response?.data.list);
} else { // } else {
toast.error(response?.message); // toast.error(response?.message);
} // }
} catch (error) { // } catch (error) {
toast.error('Failed to fetch customer list'); // toast.error('Failed to fetch customer list');
} finally { // } finally {
setIsLoadingCustomer(false); // setIsLoadingCustomer(false);
} // }
}; // };
const doFetchUserData = useCallback(async (id: string) => { const doFetchUserData = useCallback(async (id: string) => {
setIsFetching(true); setIsFetching(true);
const response = await GetData(`${API_URL}/user/detail/${id}`, { id }); const response = await GetData(`${API_URL}/user/profile/${id}`, { id });
if (response?.status) { if (response?.status) {
setFormField((prev) => ({ setFormField((prev) => ({
@ -169,9 +169,9 @@ const EditDialog = () => {
name: response.data.name, name: response.data.name,
username: response.data.username, username: response.data.username,
email: response.data.email, email: response.data.email,
id_role: response.data.idRole, // id_role: response.data.idRole,
status: response.data.status, status: response.data.status,
customerid: response.data.customer?.id || '' // customerid: response.data.customer?.id || ''
})); }));
} else { } else {
setFormField((prev) => ({ setFormField((prev) => ({
@ -179,30 +179,30 @@ const EditDialog = () => {
name: '', name: '',
username: '', username: '',
email: '', email: '',
id_role: '', // id_role: '',
status: '', status: '',
customerid: '' // customerid: ''
})); }));
} }
setIsFetching(false); setIsFetching(false);
}, []); }, []);
const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => { // const handleCustomerSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsLoadingCustomer(true); // setIsLoadingCustomer(true);
setSearchTerm(e.target.value); // setSearchTerm(e.target.value);
setSelectedCustomerName(e.target.value); // setSelectedCustomerName(e.target.value);
setDropdownOpen(true); // setDropdownOpen(true);
const timer = setTimeout(() => { // const timer = setTimeout(() => {
getCustomerList([{ id: 'fullname', desc: false }], e.target.value); // getCustomerList([{ id: 'fullname', desc: false }], e.target.value);
}, 500); // }, 500);
return () => clearTimeout(timer); // return () => clearTimeout(timer);
}; // };
const handleCustomerSelect = (customer: CustomerProps) => { // const handleCustomerSelect = (customer: CustomerProps) => {
setFormField({ ...formField, customerid: customer.id }); // setFormField({ ...formField, customerid: customer.id });
setSelectedCustomerName(customer.fullname); // setSelectedCustomerName(customer.fullname);
setDropdownOpen(false); // setDropdownOpen(false);
}; // };
const filteredCustomer = customers const filteredCustomer = customers
.filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase())) .filter((item) => item.fullname.toLowerCase().includes(searchTerm.toLowerCase()))
@ -216,8 +216,8 @@ const EditDialog = () => {
}, [showEditDialog]); }, [showEditDialog]);
useEffect(() => { useEffect(() => {
getCustomerList([{ id: 'created_at', desc: false }], ''); // getCustomerList([{ id: 'created_at', desc: false }], '');
doFetchUserRole([{ id: 'name', desc: false }]); // doFetchUserRole([{ id: 'name', desc: false }]);
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -316,7 +316,7 @@ const EditDialog = () => {
</div> </div>
{/* Customer */} {/* Customer */}
<div className="flex gap-2.5"> {/* <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Customer</label> <label className="form-label flex items-center gap-1 max-w-56">Customer</label>
<div className="grow flex flex-col" ref={dropdownRef}> <div className="grow flex flex-col" ref={dropdownRef}>
<Input <Input
@ -351,7 +351,7 @@ const EditDialog = () => {
<span className="text-red-500 text-xs mt-1">{errors.customerid}</span> <span className="text-red-500 text-xs mt-1">{errors.customerid}</span>
)} )}
</div> </div>
</div> </div> */}
{/* Email */} {/* Email */}
<div className="flex gap-2.5"> <div className="flex gap-2.5">
@ -374,7 +374,7 @@ const EditDialog = () => {
</div> </div>
{/* Role */} {/* Role */}
<div className="flex gap-2.5"> {/* <div className="flex gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">Role</label> <label className="form-label flex items-center gap-1 max-w-56">Role</label>
<div className="grow flex flex-col"> <div className="grow flex flex-col">
<Select <Select
@ -401,7 +401,7 @@ const EditDialog = () => {
<span className="text-red-500 text-xs mt-1">{errors.id_role}</span> <span className="text-red-500 text-xs mt-1">{errors.id_role}</span>
)} )}
</div> </div>
</div> </div> */}
{/* Status */} {/* Status */}
<div className="flex gap-2.5"> <div className="flex gap-2.5">

View File

@ -21,7 +21,7 @@ export interface CreateUserParams {
password: string; password: string;
retype_password: string; retype_password: string;
name: string; name: string;
id_role: string; // id_role: string;
status: string; status: string;
} }
@ -31,18 +31,18 @@ export const initialStateCreateUser: {
password: string; password: string;
retype_password: string; retype_password: string;
name: string; name: string;
id_role: string; // id_role: string;
status: string; status: string;
customerid: string; // customerid: string;
} = { } = {
email: '', email: '',
username: '', username: '',
password: '', password: '',
retype_password: '', retype_password: '',
name: '', name: '',
id_role: '', // id_role: '',
status: '', status: '',
customerid: '' // customerid: ''
}; };
export const validateFormCreateUser = ( export const validateFormCreateUser = (
@ -56,7 +56,7 @@ export const validateFormCreateUser = (
{ key: 'name', label: 'Name' }, { key: 'name', label: 'Name' },
{ key: 'username', label: 'Username' }, { key: 'username', label: 'Username' },
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'id_role', label: 'Role' }, // { key: 'id_role', label: 'Role' },
{ key: 'status', label: 'Status' }, { key: 'status', label: 'Status' },
{ key: 'password', label: 'Password' }, { key: 'password', label: 'Password' },
{ key: 'retype_password', label: 'Retype Password' } { key: 'retype_password', label: 'Retype Password' }
@ -65,7 +65,7 @@ export const validateFormCreateUser = (
{ key: 'name', label: 'Name' }, { key: 'name', label: 'Name' },
{ key: 'username', label: 'Username' }, { key: 'username', label: 'Username' },
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'id_role', label: 'Role' }, // { key: 'id_role', label: 'Role' },
{ key: 'status', label: 'Status' } { key: 'status', label: 'Status' }
]; ];

View File

@ -35,10 +35,10 @@ interface SelectedUser {
name: string; name: string;
email: string; email: string;
username: string; username: string;
role: string; // role: string;
new_password: string; new_password: string;
check_new_password: string; check_new_password: string;
customer: string; // customer: string;
} }
const initialProps: ContextProps = { const initialProps: ContextProps = {
@ -57,7 +57,7 @@ const initialProps: ContextProps = {
const ManageUserContext = createContext<ContextProps>(initialProps); const ManageUserContext = createContext<ContextProps>(initialProps);
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_user;
const ManageUserContextProvider = ({ children }: { children: React.ReactNode }) => { const ManageUserContextProvider = ({ children }: { children: React.ReactNode }) => {
/* state */ /* state */
@ -98,16 +98,16 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
enableSorting: true, enableSorting: true,
enableHiding: false enableHiding: false
}, },
{ // {
accessorFn: (row) => row.customer?.username, // accessorFn: (row) => row.customer?.username,
id: 'Users.customer', // id: 'Users.customer',
header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />, // header: ({ column }) => <DataGridColumnHeader title="Customer" column={column} />,
enableSorting: true, // enableSorting: true,
enableHiding: false, // enableHiding: false,
meta: { // meta: {
headerClassName: 'w-[300px]' // headerClassName: 'w-[300px]'
} // }
}, // },
{ {
accessorFn: (row) => row.email, accessorFn: (row) => row.email,
id: 'email', id: 'email',
@ -128,23 +128,23 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
} }
}, },
{ // {
accessorFn: (row) => row.role.name, // accessorFn: (row) => row.role.name,
id: 'Users.role', // id: 'Users.role',
header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />, // header: ({ column }) => <DataGridColumnHeader title="Role Name" column={column} />,
enableSorting: true, // enableSorting: true,
enableHiding: false, // enableHiding: false,
cell: (data: any) => { // cell: (data: any) => {
const { role } = data.row.original; // const { role } = data.row.original;
// console.log('role :', role); // // console.log('role :', role);
let html = <p className="text-danger italic">Unassigned</p>; // let html = <p className="text-danger italic">Unassigned</p>;
if (role && role.name) html = role.name; // if (role && role.name) html = role.name;
return html; // return html;
}, // },
meta: { // meta: {
headerClassName: 'w-[350px]' // headerClassName: 'w-[350px]'
} // }
}, // },
{ {
accessorFn: (row) => row.status, accessorFn: (row) => row.status,
id: 'status', id: 'status',
@ -192,8 +192,8 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
setSelectedUser(row.id); setSelectedUser(row.id);
}} }}
> >
<QrCode className="w-4 h-4 mr-2" /> {/* <QrCode className="w-4 h-4 mr-2" />
QR Code QR Code */}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditDialog(true, row.id)}> <DropdownMenuItem onClick={() => handleEditDialog(true, row.id)}>
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
@ -223,16 +223,16 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
// Tambahkan prefix ke field dari tabel Users // Tambahkan prefix ke field dari tabel Users
const mappedSorting = sorting.map((sort: any) => ({ const mappedSorting = sorting.map((sort: any) => ({
...sort, ...sort,
id: USER_TABLE_COLUMNS.includes(sort.id) ? `Users.${sort.id}` : sort.id id: USER_TABLE_COLUMNS.includes(sort.id) ? `${sort.id}` : sort.id
})); }));
const orderField = mappedSorting[0]?.id ?? 'Users.username'; const orderField = mappedSorting[0]?.id ?? 'username';
const orderDirection = mappedSorting[0]?.desc === false ? 'DESC' : 'ASC'; const orderDirection = mappedSorting[0]?.desc === false ? 'DESC' : 'ASC';
filter = filter =
filter.length == 0 filter.length == 0
? {} ? {}
: { 'Users.username': { like: `%${filter[0].value?.toLowerCase()}%` } }; : { 'username': { like: `%${filter[0].value?.toLowerCase()}%` } };
const response = await GetData(`${API_URL}/user/list`, { const response = await GetData(`${API_URL}/user/list`, {
limit: limit, limit: limit,
page: page + 1, page: page + 1,
@ -241,7 +241,7 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
order_direction: orderDirection, order_direction: orderDirection,
filter: JSON.stringify(filter) filter: JSON.stringify(filter)
}); });
// console.log('response api:', response); console.log('response api:', response);
return { data: response?.data.list, totalCount: response?.data.total_count }; return { data: response?.data.list, totalCount: response?.data.total_count };
}; };
@ -267,17 +267,12 @@ const ManageUserContextProvider = ({ children }: { children: React.ReactNode })
pagination={{ size: 10 }} pagination={{ size: 10 }}
toolbar={<ListToolBar />} toolbar={<ListToolBar />}
layout={{ card: true }} layout={{ card: true }}
sorting={[{ id: 'Users.created_at', desc: false }]} sorting={[{ id: 'created_at', desc: false }]}
serverSide={true} serverSide={true}
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
doGetListData(pageIndex, pageSize, sorting, columnFilters) doGetListData(pageIndex, pageSize, sorting, columnFilters)
} }
> >
<Dialog open={showQr} onOpenChange={setShowQr}>
<DialogContent>
<GenerateQr idCustomer={selectedUser} />
</DialogContent>
</Dialog>
{children} {children}
</DataGridProvider> </DataGridProvider>
</ManageUserContext.Provider> </ManageUserContext.Provider>

View 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>
</>
);
}

View 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 };

View 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 };

View 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 };

View 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 };

View 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;
};

View File

@ -0,0 +1,2 @@
export * from './ListToolBar';
export * from './EditDialog';

View 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 };

View File

@ -0,0 +1,2 @@
export * from './ManageSitesContext';
export * from './useManageSitesContext';

View 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 };

View File

@ -0,0 +1 @@
export * from './ManageSitesPage';

View File

@ -374,7 +374,7 @@ const DetailTransaction = () => {
)} )}
<div className="py-4 overflow-y-auto max-h-[400px]"> <div className="py-4 overflow-y-auto max-h-[400px]">
{activeTab === 'detail' && transactionDetails?.kind === 'P' && ( {activeTab === 'detail' && (transactionDetails?.kind === 'P' || transactionDetails?.kind === 'K') && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold flex items-center"> <h3 className="font-semibold flex items-center">
Transaction Information Transaction Information

View File

@ -414,7 +414,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
limit, limit,
page: page + 1, page: page + 1,
with_deleted: false, with_deleted: false,
order_field: 'Transactions.created_at', order_field: 'Transactions.transaction_date',
order_direction: 'DESC', order_direction: 'DESC',
filter: JSON.stringify(formattedFilter) filter: JSON.stringify(formattedFilter)
}); });

View File

@ -45,9 +45,9 @@ const DropdownUser = ({ menuItemRef }: IDropdownUserProps) => {
alt="" alt=""
/> */} /> */}
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-[14px] text-gray-800 font-semibold">{auth?.user.username}</p> <p className="text-[14px] text-gray-800 font-semibold">{auth?.user?.username}</p>
<p className="text-xs text-gray-600 hover:text-primary font-medium leading-none"> <p className="text-xs text-gray-600 hover:text-primary font-medium leading-none">
{auth?.user.email} {auth?.user?.email}
</p> </p>
</div> </div>
</div> </div>

View File

@ -8,6 +8,8 @@ import { ErrorsRouting } from '@/errors';
import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage'; import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage';
import ManageUserPage from '@/pages/settings/user/manage-user/ManageUserPage'; import ManageUserPage from '@/pages/settings/user/manage-user/ManageUserPage';
import ManageSitesPage from '@/pages/sites/manage-sites/ManageSitesPage';
import Transaction from '@/pages/transaction/history-transaction/Transaction'; import Transaction from '@/pages/transaction/history-transaction/Transaction';
import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction'; import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction';
import TransactionTopup from '@/pages/transaction/topup/TransactionTopup'; import TransactionTopup from '@/pages/transaction/topup/TransactionTopup';
@ -54,7 +56,7 @@ import WithdrawalEmoney from '@/pages/transaction/withdrawl-emoney/WithdrawalEmo
import ManageCards from '@/pages/cards/manage-card/ManageCard'; import ManageCards from '@/pages/cards/manage-card/ManageCard';
import HistoryCard from '@/pages/cards/history-card/HistoryCard'; import HistoryCard from '@/pages/cards/history-card/HistoryCard';
// DISBURSEMENT import ComingSoonPage from '@/pages/ComingSoonPage';
const AppRoutingSetup = (): ReactElement => { const AppRoutingSetup = (): ReactElement => {
return ( return (
@ -62,8 +64,13 @@ const AppRoutingSetup = (): ReactElement => {
<Route element={<RequireAuth />}> <Route element={<RequireAuth />}>
<Route element={<Demo2Layout />}> <Route element={<Demo2Layout />}>
<Route path="/" element={<DashboardHomePage />} /> <Route path="/" element={<DashboardHomePage />} />
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
<Route path="/coming-soon" element={<ComingSoonPage />} />
<Route path="/settings/user-management/manage-user" element={<ManageUserPage />} />
<Route path="/master-data" element={<MasterData />} /> <Route path="/sites/manage-sites" element={<ManageSitesPage />} />
{/* <Route path="/master-data" element={<MasterData />} />
<Route path="/master-data/municipios" element={<Municipios />} /> <Route path="/master-data/municipios" element={<Municipios />} />
<Route path="/master-data/postoadms" element={<PostoAdmsMaster />} /> <Route path="/master-data/postoadms" element={<PostoAdmsMaster />} />
<Route path="/master-data/sucos" element={<SucosMaster />} /> <Route path="/master-data/sucos" element={<SucosMaster />} />
@ -87,7 +94,7 @@ const AppRoutingSetup = (): ReactElement => {
<Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} /> <Route path="/master-data/wallet-rule" element={<WalletRuleMaster />} />
<Route path="/account/home/user-profile" element={<AccountUserProfilePage />} />
<Route path="/groups/group-management" element={<ManageGroups />} /> <Route path="/groups/group-management" element={<ManageGroups />} />
@ -98,10 +105,10 @@ const AppRoutingSetup = (): ReactElement => {
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} /> <Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
<Route path="/members/agent-balance" element={<AgentBalance />} /> <Route path="/members/agent-balance" element={<AgentBalance />} />
<Route path="/members/search-member" element={<SearchMember />} /> <Route path="/members/search-member" element={<SearchMember />} />
<Route path="/members/search-member" element={<SearchMember />} /> <Route path="/members/search-member" element={<SearchMember />} /> */}
{/* cards */} {/* cards */}
<Route path="/cards/manage-card" element={<ManageCards />} /> {/* <Route path="/cards/manage-card" element={<ManageCards />} />
<Route path="/cards/card-history" element={<HistoryCard />} /> <Route path="/cards/card-history" element={<HistoryCard />} />
@ -126,18 +133,17 @@ const AppRoutingSetup = (): ReactElement => {
<Route path="/menu/welcome" element={<Welcome />} /> <Route path="/menu/welcome" element={<Welcome />} />
<Route path="/message/inbox" element={<Inbox />} /> <Route path="/message/inbox" element={<Inbox />} />
<Route path="/webservice/webservice-management" element={<ManageWebServices />} /> <Route path="/webservice/webservice-management" element={<ManageWebServices />} />
<Route path="/settings/user-management/manage-user" element={<ManageUserPage />} />
<Route path="/settings/user-management/log-activity" element={<LogActivityPage />} /> <Route path="/settings/user-management/log-activity" element={<LogActivityPage />} />
<Route <Route
path="/settings/user-management/manage-position" path="/settings/user-management/manage-position"
element={<ManagePositionPage />} element={<ManagePositionPage />}
/> /> */}
{/* DISBURSEMENT */} {/* DISBURSEMENT */}
<Route {/* <Route
path="/disbursement/transaction-history" path="/disbursement/transaction-history"
element={<HistoryTransactionDisbursement />} element={<HistoryTransactionDisbursement />}
/> /> */}
{/* DISBURSEMENT */} {/* DISBURSEMENT */}
</Route> </Route>
</Route> </Route>

View File

@ -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",

5724
yarn.lock

File diff suppressed because it is too large Load Diff