update get data site points from api
This commit is contained in:
@ -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
|
||||||
|
|
||||||
@ -6,6 +6,7 @@ GENERATE_SOURCEMAP=false
|
|||||||
# 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=http://127.0.0.1:7001/api
|
||||||
|
VITE_APP_API_URL_MASTER_DATA=http://127.0.0.1:7002/api
|
||||||
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
||||||
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
||||||
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting
|
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
VITE_APP_NAME=tpay-dashboard-tl
|
VITE_APP_NAME=revenue-tt-dashboard
|
||||||
VITE_APP_VERSION=1=9.1.1
|
VITE_APP_VERSION=1=9.1.1
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
VITE_APP_API_URL=https://api-v2.tpay.tl/api
|
VITE_APP_API_URL=https://api-v2.tpay.tl/api
|
||||||
|
|||||||
@ -1,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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -14,7 +14,7 @@ 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 GET_USER_URL = `${API_URL}/user/profile`;
|
export const GET_USER_URL = `${API_URL}/user/profile`;
|
||||||
@ -57,7 +57,7 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
|||||||
...auth.token,
|
...auth.token,
|
||||||
id: auth.userid,
|
id: auth.userid,
|
||||||
user: auth.user,
|
user: auth.user,
|
||||||
};
|
};
|
||||||
|
|
||||||
saveAuth(enhancedAuth);
|
saveAuth(enhancedAuth);
|
||||||
setCurrentUser(auth.user);
|
setCurrentUser(auth.user);
|
||||||
@ -87,7 +87,6 @@ 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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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}`,
|
// 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 };
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { CreditCard, Wallet } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
type WalletResponse = {
|
|
||||||
amount: number;
|
|
||||||
id_wallet: string;
|
|
||||||
wallet: string;
|
|
||||||
credit_limit: number;
|
|
||||||
monthly_limit: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BalanceCardProps {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL_WALLET = apiConfig.service_wallet;
|
|
||||||
|
|
||||||
const BalanceCard = ({ id }: BalanceCardProps) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [wallets, setWallets] = useState<WalletResponse[]>([]);
|
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD'
|
|
||||||
}).format(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumberWithDots = (value: number) => {
|
|
||||||
return value.toLocaleString('id-ID');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColorClass = (amount: number, walletType: string) => {
|
|
||||||
if (walletType.toLowerCase().includes('point account')) return 'bg-amber-300';
|
|
||||||
// if (amount > 3125) return 'bg-amber-300';
|
|
||||||
return 'bg-red-500';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchAccountBalances() {
|
|
||||||
try {
|
|
||||||
const response = await GetData(`${API_URL_WALLET}/dashboard/balance/account/${id}`, {
|
|
||||||
id: id
|
|
||||||
});
|
|
||||||
if (response?.status === true) {
|
|
||||||
setWallets(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching account balances', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchAccountBalances();
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid lg:grid-cols-4 md:grid-cols-2 gap-5 items-stretch">
|
|
||||||
{wallets.map((wallet) => {
|
|
||||||
const colorClass = getColorClass(wallet.amount, wallet.wallet);
|
|
||||||
const isNegative = wallet.amount < 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={wallet.id_wallet}
|
|
||||||
className="rounded-lg overflow-hidden shadow-lg border border-gray-200 hover:shadow-xl transition-shadow duration-300"
|
|
||||||
>
|
|
||||||
<div className={`${colorClass} h-2`} />
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h3 className="font-bold text-lg text-gray-800">{wallet.wallet}</h3>
|
|
||||||
{wallet.wallet.includes('Credit') ? (
|
|
||||||
<CreditCard className="text-gray-600" size={20} />
|
|
||||||
) : (
|
|
||||||
<Wallet className="text-gray-600" size={20} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className={`text-xl font-bold ${isNegative ? 'text-red-600' : 'text-gray-800'}`}>
|
|
||||||
{formatCurrency(wallet.amount)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200 flex items-center justify-between">
|
|
||||||
<div className="text-xs text-gray-500">Credit Limit:</div>
|
|
||||||
<div className="text-sm">{formatNumberWithDots(wallet.credit_limit)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex items-center justify-between">
|
|
||||||
<div className="text-xs text-gray-500">Monthly Limit:</div>
|
|
||||||
<div className="text-sm">{formatNumberWithDots(wallet.monthly_limit)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BalanceCard;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Wallet } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import ShowDetailWalletDialog from './ShowDetailWalletDialog';
|
|
||||||
|
|
||||||
interface AccountCardProps {
|
|
||||||
title: string;
|
|
||||||
balance: string;
|
|
||||||
creditLimit: string;
|
|
||||||
monthlyLimit: string;
|
|
||||||
idbalance: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BankSaldo: React.FC<AccountCardProps> = ({
|
|
||||||
title,
|
|
||||||
balance,
|
|
||||||
creditLimit,
|
|
||||||
monthlyLimit,
|
|
||||||
idbalance,
|
|
||||||
}) => {
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-full max-w-xs bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
|
|
||||||
<div className="h-1 bg-red-500" />
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h2 className="text-lg font-semibold">{title}</h2>
|
|
||||||
<Wallet className="h-5 w-5 text-gray-500" />
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-gray-800">
|
|
||||||
{new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD',
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(parseFloat(balance))}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Credit Limit:</span>
|
|
||||||
<span>{creditLimit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>Monthly Limit:</span>
|
|
||||||
<span>{monthlyLimit}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="h-7.5"
|
|
||||||
onClick={() => setShowDialog(true)}
|
|
||||||
>
|
|
||||||
Detail Wallet
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showDialog && (
|
|
||||||
<ShowDetailWalletDialog
|
|
||||||
open={showDialog}
|
|
||||||
onClose={() => setShowDialog(false)}
|
|
||||||
idbalance={idbalance}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default BankSaldo;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { useLanguage } from '@/i18n';
|
|
||||||
import { fCurrency } from '@/utils/FormatNumber';
|
|
||||||
import { toAbsoluteUrl } from '@/utils/Assets';
|
|
||||||
|
|
||||||
interface CardDataProduct {
|
|
||||||
title: string;
|
|
||||||
total: string;
|
|
||||||
growth: number;
|
|
||||||
surplus: boolean;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Card = ({ title, total, growth, surplus, icon }: CardDataProduct) => {
|
|
||||||
const { isRTL } = useLanguage();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card h-full w-full bg-[length:85%] [background-position:9rem_-4rem] rtl:[background-position:-4rem_-4rem] bg-no-repeat channel-stats-bg">
|
|
||||||
<div className="card-body flex flex-col gap-4 p-5 lg:p-b-7.5 lg:pt-4">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<span className="text-sm font-normal text-gray-600 mb-5">{title}</span>
|
|
||||||
<span className="text-3xl font-semibold text-gray-900 mb-0">{total}</span>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold mt-2 flex items-center gap-1 ${growth === 0 ? 'text-primary' : surplus ? 'text-green-500' : 'text-red-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{growth === 0 ? '−' : surplus ? '▲' : '▼'} {growth} % From Last Week
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Card };
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
import ApexChart from 'react-apexcharts';
|
|
||||||
import { ApexOptions } from 'apexcharts';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { fCurrency } from '@/utils/FormatNumber';
|
|
||||||
|
|
||||||
interface Series {
|
|
||||||
name: string;
|
|
||||||
data: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chart = ({
|
|
||||||
title,
|
|
||||||
toolbar,
|
|
||||||
chartType,
|
|
||||||
chartLegend,
|
|
||||||
subtitle,
|
|
||||||
number,
|
|
||||||
count,
|
|
||||||
chartData = []
|
|
||||||
}: any) => {
|
|
||||||
const [series, setSeries] = useState<Series[]>([]);
|
|
||||||
const [categories, setCategories] = useState<string[]>([]);
|
|
||||||
|
|
||||||
let legendOpt = null;
|
|
||||||
if (chartLegend == 'true') {
|
|
||||||
legendOpt = true;
|
|
||||||
} else {
|
|
||||||
legendOpt = false;
|
|
||||||
}
|
|
||||||
const options: ApexOptions = {
|
|
||||||
chart: {
|
|
||||||
type: 'area',
|
|
||||||
toolbar: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: '30%'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: true,
|
|
||||||
offsetY: -10,
|
|
||||||
offsetX: chartType === 'bar' ? 1.5 : 0,
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
markers: {
|
|
||||||
size: 0,
|
|
||||||
shape: 'circle'
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: categories,
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
colors: 'var(--tw-gray-500)',
|
|
||||||
fontSize: '12px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
style: {
|
|
||||||
colors: 'var(--tw-gray-500)',
|
|
||||||
fontSize: '12px'
|
|
||||||
},
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
borderColor: 'var(--tw-gray-200)',
|
|
||||||
strokeDashArray: 5,
|
|
||||||
padding: {
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 20,
|
|
||||||
left: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
shared: true,
|
|
||||||
intersect: false,
|
|
||||||
y: {
|
|
||||||
formatter: (value: any) => {
|
|
||||||
if (count == 'sum') {
|
|
||||||
return fCurrency(value);
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
curve: 'smooth',
|
|
||||||
lineCap: 'butt',
|
|
||||||
colors: undefined,
|
|
||||||
width: 3,
|
|
||||||
dashArray: 0
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: legendOpt,
|
|
||||||
position: 'right',
|
|
||||||
floating: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
if (chartData && chartData.length !== 0) {
|
|
||||||
const categories = chartData.map((item: any) => item.month);
|
|
||||||
const statusNames = Object.keys(chartData[0]).filter((key) => key !== 'month');
|
|
||||||
const series = statusNames.map((status) => {
|
|
||||||
const cleanName = status
|
|
||||||
.replace('_count', '')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/(^|\s)\S/g, (match) => match.toUpperCase());
|
|
||||||
return {
|
|
||||||
name: cleanName,
|
|
||||||
data: chartData.map((item: any) => item[status])
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setCategories(categories);
|
|
||||||
setSeries(series);
|
|
||||||
}
|
|
||||||
}, [chartData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card h-full">
|
|
||||||
<div className="card-header border-0 ps-5 pb-0">
|
|
||||||
<h3 className="card-title">{title}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body flex flex-col gap-4 p-2 lg:p-b7.5 lg:pt-4">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="flex flex-col w-full">
|
|
||||||
<div className="ps-3">{toolbar && <div>{toolbar}</div>}</div>
|
|
||||||
<span className="text-sm font-normal text-gray-700 mb-1">{subtitle}</span>
|
|
||||||
<span className="text-3xl font-semibold text-gray-900 mb-0">{number}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ApexChart
|
|
||||||
id="earnings_chart" //
|
|
||||||
options={options}
|
|
||||||
series={series}
|
|
||||||
type={chartType}
|
|
||||||
legend={chartLegend}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Chart };
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { KeenIcon } from '@/components/keenicons';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
interface DateRangePickerProps {
|
|
||||||
date: DateRange | undefined;
|
|
||||||
setDate: (date: DateRange | undefined) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DateRangePicker = ({ date, setDate }: DateRangePickerProps) => {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<button
|
|
||||||
id="date"
|
|
||||||
className={cn(
|
|
||||||
'btn btn-sm btn-light data-[state=open]:bg-light-active',
|
|
||||||
!date && 'text-gray-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<KeenIcon icon="calendar" className="me-0.5" />
|
|
||||||
{date?.from ? (
|
|
||||||
date.to ? (
|
|
||||||
<>
|
|
||||||
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
format(date.from, 'LLL dd, y')
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>Pick a date range</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="end">
|
|
||||||
<Calendar
|
|
||||||
initialFocus
|
|
||||||
mode="range"
|
|
||||||
defaultMonth={date?.from}
|
|
||||||
selected={date}
|
|
||||||
onSelect={setDate}
|
|
||||||
numberOfMonths={2}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DateRangePicker };
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { UsersIcon, UserCheckIcon } from "lucide-react";
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const polarToCartesian = (cx: number, cy: number, radius: number, angleInDegrees: number) => {
|
|
||||||
const angleInRadians = (angleInDegrees * Math.PI) / 180.0;
|
|
||||||
return {
|
|
||||||
x: cx + radius * Math.cos(angleInRadians),
|
|
||||||
y: cy + radius * Math.sin(angleInRadians),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const describeArc = (
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
radius: number,
|
|
||||||
startAngle: number,
|
|
||||||
endAngle: number
|
|
||||||
) => {
|
|
||||||
const start = polarToCartesian(x, y, radius, endAngle);
|
|
||||||
const end = polarToCartesian(x, y, radius, startAngle);
|
|
||||||
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
|
|
||||||
|
|
||||||
return [
|
|
||||||
"M", start.x, start.y,
|
|
||||||
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y,
|
|
||||||
].join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemberActivity = ({ startdate, enddate }: Props) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/active-user`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
const percentage = parseFloat((responseTransactionValue?.data?.total_customer_active_percentage ?? 0).toFixed(2));
|
|
||||||
const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0;
|
|
||||||
const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0;
|
|
||||||
const reguler = responseTransactionValue?.data?.reguler ?? 0;
|
|
||||||
const premium = responseTransactionValue?.data?.premium ?? 0;
|
|
||||||
const agent = responseTransactionValue?.data?.agent ?? 0;
|
|
||||||
|
|
||||||
// Calculate pointer angle
|
|
||||||
const angle = (percentage / 100) * 180;
|
|
||||||
const pointerX = 50 + 25 * Math.cos((angle - 180) * Math.PI / 180);
|
|
||||||
const pointerY = 50 + 25 * Math.sin((angle - 180) * Math.PI / 180);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6 bg-white rounded-lg shadow-md w-full max-w-md">
|
|
||||||
<div className="pb-3 mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700">Member Activity</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-6">
|
|
||||||
{/* Sidebar Info */}
|
|
||||||
<ul className="space-y-4 w-1/2 text-gray-700 text-sm">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UsersIcon className="w-4 h-4" />
|
|
||||||
<span>Total Customer: {totalCustomer}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" />
|
|
||||||
<span>Active Customer: {activeCustomer}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='blue' />
|
|
||||||
<span>Reguler: {reguler}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='green' />
|
|
||||||
<span>Premium: {premium}</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<UserCheckIcon className="w-4 h-4" color='orange' />
|
|
||||||
<span>Agent: {agent}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{/* Gauge Chart */}
|
|
||||||
<div className="w-1/2">
|
|
||||||
<div className="border border-gray-200 rounded-md p-4 text-center">
|
|
||||||
<h3 className="text-sm text-gray-600 font-medium mb-2">Active User</h3>
|
|
||||||
<div className="relative h-24 w-full">
|
|
||||||
<svg viewBox="0 0 100 50" className="w-full h-full">
|
|
||||||
{/* Background arc */}
|
|
||||||
<path
|
|
||||||
d="M 10 50 A 40 40 0 0 1 90 50"
|
|
||||||
fill="none"
|
|
||||||
stroke="#e5e7eb"
|
|
||||||
strokeWidth="10"
|
|
||||||
/>
|
|
||||||
{/* Active arc */}
|
|
||||||
{percentage > 0 && (
|
|
||||||
<path
|
|
||||||
d={describeArc(50, 50, 40, 180, 180 + (percentage * 180 / 100))}
|
|
||||||
fill="none"
|
|
||||||
stroke="#34d399"
|
|
||||||
strokeWidth="10"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* Pointer */}
|
|
||||||
<line
|
|
||||||
x1="50"
|
|
||||||
y1="50"
|
|
||||||
x2={pointerX}
|
|
||||||
y2={pointerY}
|
|
||||||
stroke="#111827"
|
|
||||||
strokeWidth="4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="flex justify-between text-xs text-gray-500 px-1 mt-1">
|
|
||||||
<span>{percentage}%</span>
|
|
||||||
<span>100%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemberActivity;
|
|
||||||
@ -1,489 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogBody,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { DataGridColumnHeader, DataGridProvider } from '@/components';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { DefaultTooltip, KeenIcon } from '@/components';
|
|
||||||
import { toAbsoluteUrl } from '@/utils';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
import ExcelJS from 'exceljs';
|
|
||||||
import { exportToExcel } from '@/pages/wallet/wallet-statement/details_wallet_statement/ExportData';
|
|
||||||
|
|
||||||
interface ShowDetailWalletDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
idbalance: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransferType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date: Date) => date.toLocaleDateString('sv-SE');
|
|
||||||
const getDefaultDateRange = () => {
|
|
||||||
const today = new Date();
|
|
||||||
const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
return {
|
|
||||||
from: formatDate(sevenDaysAgo),
|
|
||||||
to: formatDate(today)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShowDetailWalletDialog: React.FC<ShowDetailWalletDialogProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
idbalance
|
|
||||||
}) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState(getDefaultDateRange());
|
|
||||||
const [selectedTransferType, setSelectedTransferType] = useState<string | null>(null);
|
|
||||||
const [transferType, setTransferType] = useState<TransferType[]>([]);
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [category, setCategory] = useState<string[]>([]);
|
|
||||||
const [transaction, setTransaction] = useState<any[]>([]);
|
|
||||||
const [filters, setFilters] = useState<any>({});
|
|
||||||
|
|
||||||
const fetchTransferType = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
|
|
||||||
limit: 100,
|
|
||||||
page: 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'ASC'
|
|
||||||
});
|
|
||||||
setTransferType(response?.data?.list || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transfer types', error);
|
|
||||||
}
|
|
||||||
}, [GetData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTransferType();
|
|
||||||
}, [fetchTransferType]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const categoryMap: Record<string, string> = {
|
|
||||||
T: 'TRANSFER',
|
|
||||||
P: 'PURCHASE',
|
|
||||||
W: 'WITHDRAW',
|
|
||||||
U: 'TOP UP',
|
|
||||||
R: 'RETURN',
|
|
||||||
N: 'TOP UP PARTNER',
|
|
||||||
E: 'REWARD',
|
|
||||||
L: 'PURCHASE LOJA',
|
|
||||||
B: 'TOP UP P24',
|
|
||||||
A: 'TRANSFER AGENT',
|
|
||||||
M: 'WITHDRAWAL AGENT',
|
|
||||||
O: 'TOP UP AGENT',
|
|
||||||
S: 'TRANSFER P24',
|
|
||||||
I: 'WIJTDRAW MERCHANT',
|
|
||||||
D: 'DONATION',
|
|
||||||
F: 'FEE',
|
|
||||||
V: 'REVERSAL',
|
|
||||||
C: 'CASHBACK CASH',
|
|
||||||
H: 'CASHBACK POINT'
|
|
||||||
};
|
|
||||||
|
|
||||||
const uniqueCategories = Array.from(
|
|
||||||
new Set(
|
|
||||||
transaction
|
|
||||||
.map((tx) => categoryMap[tx.category] || '_') // map code to label or default to '_'
|
|
||||||
.filter((cat) => !!cat && cat !== '_') // remove undefined/null or underscore
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
setCategory(uniqueCategories);
|
|
||||||
}, [transaction]);
|
|
||||||
|
|
||||||
const handleTransferTypeChange = (value: string) => {
|
|
||||||
setSelectedTransferType(value === 'all' ? null : value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCategoryChange = (value: string) => {
|
|
||||||
setSelectedCategory(value === '' ? null : value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAllFilters = () => {
|
|
||||||
setDateRange(getDefaultDateRange());
|
|
||||||
setSelectedTransferType(null);
|
|
||||||
setSelectedCategory(null);
|
|
||||||
setSearchValue('');
|
|
||||||
setFilters({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApplyFilters = () => {
|
|
||||||
const appliedFilters: any = {};
|
|
||||||
|
|
||||||
if (dateRange.from && dateRange.to) {
|
|
||||||
const fromDate = new Date(`${dateRange.from}T00:00:00Z`);
|
|
||||||
const toDate = new Date(`${dateRange.to}T23:59:59Z`);
|
|
||||||
appliedFilters.date = {
|
|
||||||
from: fromDate.toISOString(),
|
|
||||||
to: toDate.toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTransferType) {
|
|
||||||
appliedFilters.transaction_type_id = selectedTransferType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCategory) {
|
|
||||||
appliedFilters.category = selectedCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchValue) {
|
|
||||||
appliedFilters.transaction_code = searchValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilters(appliedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
accessorKey: 'transaction_code',
|
|
||||||
header: ({ column }: any) => (
|
|
||||||
<DataGridColumnHeader title="Transaction Code" column={column} />
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[120px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'msisdn_reff',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="MSISDN Reffer" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[120px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transaction_type.name',
|
|
||||||
header: ({ column }: any) => (
|
|
||||||
<DataGridColumnHeader title="Transaction Type" column={column} />
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[180px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'type',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Type" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
meta: { headerClassName: 'min-w-[80px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'pre_amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Pre Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'post_amount',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Post Amount" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue()?.toFixed(2),
|
|
||||||
meta: { headerClassName: 'min-w-[100px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row: any) => {
|
|
||||||
switch (row.category) {
|
|
||||||
case 'T':
|
|
||||||
return 'TRANSFER';
|
|
||||||
case 'P':
|
|
||||||
return 'PURCHASE';
|
|
||||||
case 'W':
|
|
||||||
return 'WITHDRAW';
|
|
||||||
case 'U':
|
|
||||||
return 'TOP UP';
|
|
||||||
case 'R':
|
|
||||||
return 'RETURN';
|
|
||||||
case 'N':
|
|
||||||
return 'TOP UP PARTNER';
|
|
||||||
case 'E':
|
|
||||||
return 'REWARD';
|
|
||||||
case 'L':
|
|
||||||
return 'PURCHASE LOJA';
|
|
||||||
case 'B':
|
|
||||||
return 'TOP UP P24';
|
|
||||||
case 'A':
|
|
||||||
return 'TRANSFER AGENT';
|
|
||||||
case 'M':
|
|
||||||
return 'WITHDRAWAL AGENT';
|
|
||||||
case 'O':
|
|
||||||
return 'TOP UP AGENT';
|
|
||||||
case 'S':
|
|
||||||
return 'TRANSFER P24';
|
|
||||||
case 'I':
|
|
||||||
return 'WIJTDRAW MERCHANT';
|
|
||||||
case 'D':
|
|
||||||
return 'DONATION';
|
|
||||||
case 'F':
|
|
||||||
return 'FEE';
|
|
||||||
case 'V':
|
|
||||||
return 'REVERSAL';
|
|
||||||
case 'C':
|
|
||||||
return 'CASHBACK CASH';
|
|
||||||
case 'H':
|
|
||||||
return 'CASHBACK POINT';
|
|
||||||
default:
|
|
||||||
return '_';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessorKey: 'category',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Category" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
meta: {
|
|
||||||
headerClassName: 'w-[250px]'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'date',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Date" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => {
|
|
||||||
const val = info.getValue();
|
|
||||||
if (!val) return '-';
|
|
||||||
return new Date(val).toLocaleString('id-ID', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
meta: { headerClassName: 'min-w-[150px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'purpose',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Purpose" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue() || '-',
|
|
||||||
meta: { headerClassName: 'min-w-[200px]' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'notes',
|
|
||||||
header: ({ column }: any) => <DataGridColumnHeader title="Notes" column={column} />,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: (info: any) => info.getValue() || '-',
|
|
||||||
meta: { headerClassName: 'min-w-[200px]' }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const getTransactionLists = useCallback(
|
|
||||||
async (page: number, limit: number, sorting: any, _columnFilters: any) => {
|
|
||||||
// console.log(idbalance);
|
|
||||||
try {
|
|
||||||
const response = await GetData(
|
|
||||||
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`,
|
|
||||||
{
|
|
||||||
limit,
|
|
||||||
page: page + 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'DESC',
|
|
||||||
filter: JSON.stringify(filters)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// console.log(response);
|
|
||||||
|
|
||||||
if (!response || !response.data) {
|
|
||||||
console.warn('No data received:', response);
|
|
||||||
return { data: [], totalCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
setTransaction(response.data.list);
|
|
||||||
return { data: response.data.list, totalCount: response.data.total_count };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction', error);
|
|
||||||
return { data: [], totalCount: 0 };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[GetData, idbalance, filters]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getAllTransactionData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await GetData(
|
|
||||||
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`,
|
|
||||||
{
|
|
||||||
limit: 10000,
|
|
||||||
page: 1,
|
|
||||||
with_deleted: false,
|
|
||||||
order_field: 'created_at',
|
|
||||||
order_direction: 'DESC',
|
|
||||||
filter: JSON.stringify(filters)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response?.data?.list || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching all transaction data', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tambahkan fungsi handle export excel
|
|
||||||
const handleExportExcel = async () => {
|
|
||||||
try {
|
|
||||||
const allData = await getAllTransactionData();
|
|
||||||
|
|
||||||
// Buat objek wallet dummy untuk kompatibilitas dengan fungsi exportToExcel
|
|
||||||
const walletData = {
|
|
||||||
ID: idbalance,
|
|
||||||
name: 'Wallet Details',
|
|
||||||
msisdn: '-'
|
|
||||||
};
|
|
||||||
|
|
||||||
await exportToExcel(allData, walletData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting to Excel:', error);
|
|
||||||
toast.error('Failed to export Excel');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
|
||||||
<DialogContent className="w-screen max-w-screen max-h-screen p-4 overflow-hidden">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Details Wallet Statement</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogDescription />
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="flex flex-wrap gap-2 lg:gap-5 w-full mb-4">
|
|
||||||
<div className="flex flex-wrap gap-3 w-full">
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
From
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={dateRange.from}
|
|
||||||
onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
To
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={dateRange.to}
|
|
||||||
onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="w-full sm:w-[160px]">
|
|
||||||
<Select
|
|
||||||
value={selectedTransferType || 'all'}
|
|
||||||
onValueChange={handleTransferTypeChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-[32px]">
|
|
||||||
<SelectValue placeholder="Transaction Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Select Transaction</SelectItem>
|
|
||||||
{transferType.map((transfer) => (
|
|
||||||
<SelectItem key={transfer.id} value={transfer.id}>
|
|
||||||
{transfer.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full sm:w-[160px]">
|
|
||||||
<Select value={selectedCategory ?? ''} onValueChange={handleCategoryChange}>
|
|
||||||
<SelectTrigger className="h-[32px]">
|
|
||||||
<SelectValue placeholder="Select Category" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{category.length === 0 ? (
|
|
||||||
<div className="text-gray-500 px-4 py-2">No data</div>
|
|
||||||
) : (
|
|
||||||
category.map((cat) => (
|
|
||||||
<SelectItem key={cat} value={cat}>
|
|
||||||
{cat}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="input input-sm w-full sm:w-[160px]">
|
|
||||||
<KeenIcon icon="magnifier" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Transaction Code"
|
|
||||||
className="overflow-hidden text-ellipsis w-full"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<DefaultTooltip title="Reset Filter" placement="top">
|
|
||||||
<Button variant="outline" className="h-8" onClick={handleClearAllFilters}>
|
|
||||||
<KeenIcon icon="arrow-circle-left" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
|
|
||||||
<Button variant="default" className="h-8" onClick={handleApplyFilters}>
|
|
||||||
Filter Data
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DefaultTooltip title={'Export to Excel'} placement={'top'}>
|
|
||||||
<Button
|
|
||||||
variant={'outline'}
|
|
||||||
className="h-7.5 min-w-[58px]"
|
|
||||||
onClick={handleExportExcel}
|
|
||||||
>
|
|
||||||
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="Excel" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<DataGridProvider
|
|
||||||
key={JSON.stringify(filters)}
|
|
||||||
columns={columns}
|
|
||||||
pagination={{ size: 5 }}
|
|
||||||
layout={{ card: true }}
|
|
||||||
sorting={[{ id: 'id', desc: false }]}
|
|
||||||
serverSide={true}
|
|
||||||
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
|
|
||||||
getTransactionLists(pageIndex, pageSize, sorting, columnFilters)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShowDetailWalletDialog;
|
|
||||||
@ -1,7 +1,37 @@
|
|||||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { sitePoints, SitePoint } from '../sitePoints';
|
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 ---
|
// --- TAMBAHAN DATA MOCK POLYGON UNTUK AREA KLASTER ---
|
||||||
// Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster
|
// Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster
|
||||||
@ -194,18 +224,66 @@ const buildSignalPopupHtml = (points: SitePoint[], lat: number, lng: number) =>
|
|||||||
const SitesSatelliteMap = () => {
|
const SitesSatelliteMap = () => {
|
||||||
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
const mapContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapRef = useRef<L.Map | null>(null);
|
const mapRef = useRef<L.Map | null>(null);
|
||||||
|
|
||||||
const markerLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
const markerLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
||||||
const polygonLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
const polygonLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
|
||||||
|
|
||||||
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
|
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedCount, setSelectedCount] = useState(sitePoints.length);
|
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), []);
|
const grouped = useMemo(() => groupByCoordinate(sitePoints), [sitePoints]);
|
||||||
|
|
||||||
|
// --- 1. Fetch data site points dari API saat komponen mount ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapContainerRef.current || mapRef.current) return;
|
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, {
|
const map = L.map(mapContainerRef.current, {
|
||||||
center: DILI_CENTER,
|
center: DILI_CENTER,
|
||||||
@ -219,9 +297,9 @@ const SitesSatelliteMap = () => {
|
|||||||
maxZoom: 19
|
maxZoom: 19
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
|
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
// Keduanya langsung dimasukkan secara permanen ke peta utama
|
// Keduanya langsung dimasukkan secara permanen ke peta utama
|
||||||
@ -269,7 +347,7 @@ const SitesSatelliteMap = () => {
|
|||||||
// Delegasi inisialisasi Event Listener interaktif Tab saat popup dibuka
|
// Delegasi inisialisasi Event Listener interaktif Tab saat popup dibuka
|
||||||
const onPopupOpen = (e: L.PopupEvent) => {
|
const onPopupOpen = (e: L.PopupEvent) => {
|
||||||
const el = e.popup.getElement();
|
const el = e.popup.getElement();
|
||||||
|
|
||||||
// Kasus A: Jika yang dibuka adalah Dialog Klaster Area
|
// Kasus A: Jika yang dibuka adalah Dialog Klaster Area
|
||||||
const clusterContent = el?.querySelector<HTMLElement>('.cluster-popup');
|
const clusterContent = el?.querySelector<HTMLElement>('.cluster-popup');
|
||||||
if (clusterContent) {
|
if (clusterContent) {
|
||||||
@ -277,15 +355,19 @@ const SitesSatelliteMap = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
map.on('popupopen', onPopupOpen);
|
map.on('popupopen', onPopupOpen);
|
||||||
|
}, [grouped, sitePoints]);
|
||||||
|
|
||||||
|
// --- 3. Cleanup peta saat komponen benar-benar unmount ---
|
||||||
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
map.off('zoomend', handleZoomVisibility);
|
if (mapRef.current) {
|
||||||
map.off('popupopen', onPopupOpen);
|
mapRef.current.off();
|
||||||
map.remove();
|
mapRef.current.remove();
|
||||||
mapRef.current = null;
|
mapRef.current = null;
|
||||||
|
}
|
||||||
markerByKeyRef.current.clear();
|
markerByKeyRef.current.clear();
|
||||||
};
|
};
|
||||||
}, [grouped]);
|
}, []);
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
@ -312,7 +394,9 @@ const SitesSatelliteMap = () => {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
Showing <span className="font-semibold">{selectedCount}</span> from {sitePoints.length} points (Cell/Sector)
|
{isLoading
|
||||||
|
? 'Memuat data...'
|
||||||
|
: `Showing ${selectedCount} from ${sitePoints.length} points (Cell/Sector)`}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -320,11 +404,23 @@ const SitesSatelliteMap = () => {
|
|||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
placeholder="Cari Cell, Sector, atau nama Site..."
|
placeholder="Cari Cell, Sector, atau nama Site..."
|
||||||
className="input input-sm w-full sm:w-72"
|
className="input input-sm w-full sm:w-72"
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 className="relative w-full rounded-lg overflow-hidden border border-gray-200" style={{ height: '70vh' }}>
|
||||||
<div ref={mapContainerRef} className="w-full h-full" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell,
|
|
||||||
ResponsiveContainer,
|
|
||||||
} from 'recharts';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransactionPieChart = ({ startdate, enddate }: Props) => {
|
|
||||||
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataTransactionValue = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/transaction-chart`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction value:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataTransactionValue();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{ name: 'Transfer', value: parseFloat((responseTransactionValue?.data?.T ?? 0).toFixed(2)), color: '#3490dc' },
|
|
||||||
{ name: 'Return', value: parseFloat((responseTransactionValue?.data?.R ?? 0).toFixed(2)), color: '#a0aec0' },
|
|
||||||
{ name: 'Topup', value: parseFloat((responseTransactionValue?.data?.U ?? 0).toFixed(2)), color: '#9f7aea' },
|
|
||||||
{ name: 'Purchase', value: parseFloat((responseTransactionValue?.data?.P ?? 0).toFixed(2)), color: '#baf7c5' },
|
|
||||||
{ name: 'Withdraw', value: parseFloat((responseTransactionValue?.data?.W ?? 0).toFixed(2)), color: '#f56565' },
|
|
||||||
{ name: 'Top Up Partner', value: parseFloat((responseTransactionValue?.data?.N ?? 0).toFixed(2)), color: '#f7fa52' },
|
|
||||||
{ name: 'Reward', value: parseFloat((responseTransactionValue?.data?.E ?? 0).toFixed(2)), color: '#f50a19' },
|
|
||||||
{ name: 'Purchase Loja', value: parseFloat((responseTransactionValue?.data?.L ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Top Up P24', value: parseFloat((responseTransactionValue?.data?.B ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Transfer Agent', value: parseFloat((responseTransactionValue?.data?.A ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Withdrawal Agent', value: parseFloat((responseTransactionValue?.data?.M ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
{ name: 'Top Up Agent', value: parseFloat((responseTransactionValue?.data?.O ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Transfer P24', value: parseFloat((responseTransactionValue?.data?.S ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Withdraw Merchant', value: parseFloat((responseTransactionValue?.data?.I ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Donation', value: parseFloat((responseTransactionValue?.data?.D ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
{ name: 'Fee', value: parseFloat((responseTransactionValue?.data?.F ?? 0).toFixed(2)), color: '#F0A04B' },
|
|
||||||
{ name: 'Reversal', value: parseFloat((responseTransactionValue?.data?.V ?? 0).toFixed(2)), color: '#FADA7A' },
|
|
||||||
{ name: 'Cashback Cash', value: parseFloat((responseTransactionValue?.data?.C ?? 0).toFixed(2)), color: '#B1C29E' },
|
|
||||||
{ name: 'Cashback Point', value: parseFloat((responseTransactionValue?.data?.H ?? 0).toFixed(2)), color: '#FCE7C8' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 w-1/3">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700">Transaction Chart</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-6">
|
|
||||||
<div className="w-40 h-40">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={data}
|
|
||||||
dataKey="value"
|
|
||||||
innerRadius={40}
|
|
||||||
outerRadius={60}
|
|
||||||
paddingAngle={3}
|
|
||||||
>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm space-y-2">
|
|
||||||
<div className="font-semibold text-gray-500">Active Transaction</div>
|
|
||||||
{data.map((entry, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between w-48">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: entry.color }}></div>
|
|
||||||
<span className="text-gray-700">{entry.name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-700">{entry.value}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransactionPieChart;
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
import { useCallApi } from '@/hooks';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.api_dashboard;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
startdate: string;
|
|
||||||
enddate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TransactionValue = ({ startdate, enddate }: Props) => {
|
|
||||||
const { GetData } = useCallApi();
|
|
||||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDataTransactionValue = async () => {
|
|
||||||
try {
|
|
||||||
const res = await GetData(`${API_URL}/transaction-value`, {
|
|
||||||
date_from: startdate,
|
|
||||||
date_to: enddate,
|
|
||||||
});
|
|
||||||
setResponseTransactionValue(res);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching transaction value:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDataTransactionValue();
|
|
||||||
}, [startdate, enddate, GetData]);
|
|
||||||
|
|
||||||
let transactionData: any[] = [];
|
|
||||||
if (responseTransactionValue?.data?.length > 0) {
|
|
||||||
for (let i = 0; i < responseTransactionValue.data.length; i++) {
|
|
||||||
let type = "";
|
|
||||||
let unit = "";
|
|
||||||
|
|
||||||
const amount = responseTransactionValue.data[i].total_amount;
|
|
||||||
|
|
||||||
if (amount >= 1000 && amount < 1000000) {
|
|
||||||
unit = "K";
|
|
||||||
} else if (amount >= 1000000 && amount < 1000000000) {
|
|
||||||
unit = "M";
|
|
||||||
} else if (amount >= 1000000000) {
|
|
||||||
unit = "B";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (responseTransactionValue.data[i].kind) {
|
|
||||||
case "P":
|
|
||||||
type = "Purchase";
|
|
||||||
break;
|
|
||||||
case "T":
|
|
||||||
type = "Transfer";
|
|
||||||
break;
|
|
||||||
case "U":
|
|
||||||
type = "Top Up";
|
|
||||||
break;
|
|
||||||
case "W":
|
|
||||||
type = "Withdraw";
|
|
||||||
break;
|
|
||||||
case "R":
|
|
||||||
type = "Return";
|
|
||||||
break;
|
|
||||||
case "N":
|
|
||||||
type = "Top Up Partner";
|
|
||||||
break;
|
|
||||||
case "E":
|
|
||||||
type = "Reward";
|
|
||||||
break;
|
|
||||||
case "L":
|
|
||||||
type = "Purchase Loja"
|
|
||||||
break;
|
|
||||||
case "B":
|
|
||||||
type = "Top Up P24";
|
|
||||||
break;
|
|
||||||
case "A":
|
|
||||||
type = " Transfer Agent";
|
|
||||||
break;
|
|
||||||
case "M":
|
|
||||||
type = "Withdrawal Agent";
|
|
||||||
break;
|
|
||||||
case 'O':
|
|
||||||
type = 'Top Up Agent';
|
|
||||||
break;
|
|
||||||
case 'S':
|
|
||||||
type = 'Transfer P24';
|
|
||||||
break;
|
|
||||||
case 'I':
|
|
||||||
type = 'Withdraw Merchant';
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
type = 'Donation';
|
|
||||||
break;
|
|
||||||
case 'F':
|
|
||||||
type = 'Fee';
|
|
||||||
break;
|
|
||||||
case 'V':
|
|
||||||
type = 'Reversal';
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
type = 'Cashback Cash';
|
|
||||||
break;
|
|
||||||
case 'H':
|
|
||||||
type = 'Cashback Point';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
type = "Unknown";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
transactionData.push({
|
|
||||||
label: type,
|
|
||||||
value: amount,
|
|
||||||
unit: unit
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl shadow p-6 w-1/3">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
|
|
||||||
</div>
|
|
||||||
{transactionData.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{transactionData.map((item, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between">
|
|
||||||
<span className="w-24 text-sm text-gray-700">{item.label}</span>
|
|
||||||
<div className="flex-1 mx-2">
|
|
||||||
<div className="w-full h-3 bg-gray-200 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-3 bg-teal-500 rounded-full"
|
|
||||||
style={{ width: `${(item.value / maxValue) * 100}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="w-24 text-right text-sm text-gray-700 break-words">
|
|
||||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center text-sm text-gray-500 mt-4">
|
|
||||||
No Data Available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransactionValue;
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { get5LastYear } from '@/utils/Date';
|
|
||||||
|
|
||||||
interface YearPickerProps {
|
|
||||||
selectedYear: string;
|
|
||||||
setSelectedYear: (year: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Komponen YearPicker
|
|
||||||
const YearPicker = ({ selectedYear, setSelectedYear }: YearPickerProps) => {
|
|
||||||
const selectYear = get5LastYear();
|
|
||||||
|
|
||||||
const handleYearChange = (year: string) => {
|
|
||||||
setSelectedYear(year);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Select value={selectedYear} onValueChange={handleYearChange}>
|
|
||||||
<SelectTrigger size="sm" className="w-28">
|
|
||||||
<SelectValue placeholder="Pilih Tahun" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{selectYear.length > 0 ? (
|
|
||||||
selectYear.map((year, index) => (
|
|
||||||
<SelectItem key={index} value={year}>
|
|
||||||
{year}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem value="no-data" disabled>
|
|
||||||
No data available
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { YearPicker };
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from './Card';
|
|
||||||
export * from './DateRangePicker';
|
|
||||||
export * from './Chart';
|
|
||||||
export * from './YearPicker';
|
|
||||||
@ -1,5 +1,2 @@
|
|||||||
export * from './DashboardHomeContext';
|
export * from './DashboardHomeContext';
|
||||||
export * from './useDashboardHomeContext';
|
export * from './useDashboardHomeContext';
|
||||||
export * from './useFetchCardData';
|
|
||||||
export * from './useFetchChartData';
|
|
||||||
// export * from './useFetchYear';
|
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { formatDate } from 'date-fns';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface UseFetchCardDataResult {
|
|
||||||
cardData: any;
|
|
||||||
isCardLoading: boolean;
|
|
||||||
cardError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchCardData = (
|
|
||||||
start_date: string,
|
|
||||||
end_date: string,
|
|
||||||
count: string
|
|
||||||
): UseFetchCardDataResult => {
|
|
||||||
const [cardData, setCardData] = useState<[]>([]);
|
|
||||||
const [isCardLoading, setIsCardLoading] = useState<boolean>(false);
|
|
||||||
const [cardError, setCardError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsCardLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/card`, {
|
|
||||||
params: {
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
aggregate: count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setCardData(response.data.data);
|
|
||||||
} catch (err) {
|
|
||||||
setCardError(err);
|
|
||||||
} finally {
|
|
||||||
setIsCardLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
|
||||||
|
|
||||||
return { cardData, isCardLoading, cardError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchCardData };
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { DateRange } from 'react-day-picker';
|
|
||||||
import { formatDate } from 'date-fns';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface UseFetchChartDataResult {
|
|
||||||
chartData: any[];
|
|
||||||
isChartLoading: boolean;
|
|
||||||
chartError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchChartData = (
|
|
||||||
start_date: string,
|
|
||||||
end_date: string,
|
|
||||||
count: string
|
|
||||||
): UseFetchChartDataResult => {
|
|
||||||
const [chartData, setChartData] = useState<any[]>([]);
|
|
||||||
const [isChartLoading, setIsChartLoading] = useState<boolean>(false);
|
|
||||||
const [chartError, setChartError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsChartLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/chart`, {
|
|
||||||
params: {
|
|
||||||
start_date: start_date,
|
|
||||||
end_date: end_date,
|
|
||||||
aggregate: count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setChartData(response.data.data);
|
|
||||||
} catch (err) {
|
|
||||||
setChartError(err);
|
|
||||||
} finally {
|
|
||||||
setIsChartLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
|
||||||
|
|
||||||
return { chartData, isChartLoading, chartError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchChartData };
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { apiConfig } from '@/config/api.config';
|
|
||||||
|
|
||||||
const API_URL = apiConfig.service_dashboard;
|
|
||||||
|
|
||||||
interface YearData {
|
|
||||||
year: string;
|
|
||||||
}
|
|
||||||
interface UseFetchselectYearResult {
|
|
||||||
selectYear: YearData[];
|
|
||||||
isselectYearLoading: boolean;
|
|
||||||
selectYearError: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFetchYear = (
|
|
||||||
path: string = '',
|
|
||||||
start_date: string = '',
|
|
||||||
end_date: string = '',
|
|
||||||
year: string = ''
|
|
||||||
): UseFetchselectYearResult => {
|
|
||||||
const [selectYear, setSelectYear] = useState<YearData[]>([]);
|
|
||||||
const [isselectYearLoading, setIsselectYearLoading] = useState<boolean>(false);
|
|
||||||
const [selectYearError, setSelectYearError] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
setIsselectYearLoading(true);
|
|
||||||
const response = await axios.get(`${API_URL}/dashboard/year`);
|
|
||||||
setSelectYear(response.data.data.map((item: { year: string }) => ({ year: item.year })));
|
|
||||||
} catch (err) {
|
|
||||||
setSelectYearError(err);
|
|
||||||
} finally {
|
|
||||||
setIsselectYearLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [path, start_date, end_date, year]);
|
|
||||||
|
|
||||||
return { selectYear, isselectYearLoading, selectYearError };
|
|
||||||
};
|
|
||||||
|
|
||||||
export { useFetchYear };
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user