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
|
||||
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=https://tpay.shiblysolution.id/api
|
||||
VITE_APP_API_URL=http://127.0.0.1:7001/api
|
||||
VITE_APP_API_URL_MASTER_DATA=http://127.0.0.1:7002/api
|
||||
# VITE_APP_API_URL=http://api-v2.tpay.tl/api
|
||||
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
|
||||
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
VITE_APP_NAME=tpay-dashboard-tl
|
||||
VITE_APP_NAME=revenue-tt-dashboard
|
||||
VITE_APP_VERSION=1=9.1.1
|
||||
GENERATE_SOURCEMAP=false
|
||||
VITE_APP_API_URL=https://api-v2.tpay.tl/api
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
VITE_APP_NAME=tpay-dashboard-tl
|
||||
VITE_APP_NAME=revenue-tt-dashboard
|
||||
VITE_APP_VERSION=1=9.1.1
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ const Login = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>TPAY | Sign In</title>
|
||||
<title>REVENUE | Sign In</title>
|
||||
</Helmet>
|
||||
<div className="card max-w-[390px] w-full">
|
||||
<form
|
||||
|
||||
@ -14,7 +14,7 @@ import { type AuthModel, type UserModel } from '@/auth';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
export const LOGIN_URL = `${API_URL}/login`;
|
||||
export const GET_USER_URL = `${API_URL}/user/profile`;
|
||||
@ -57,7 +57,7 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
...auth.token,
|
||||
id: auth.userid,
|
||||
user: auth.user,
|
||||
};
|
||||
};
|
||||
|
||||
saveAuth(enhancedAuth);
|
||||
setCurrentUser(auth.user);
|
||||
@ -87,7 +87,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
try {
|
||||
const { data: user } = await getUser();
|
||||
|
||||
// Perbarui auth yang sekarang dengan statusbalance
|
||||
saveAuth({
|
||||
...auth,
|
||||
});
|
||||
|
||||
@ -1,36 +1,41 @@
|
||||
interface apiConfigProps {
|
||||
service_dashboard: string;
|
||||
service_customer: string;
|
||||
service_user: string;
|
||||
service_master_data: string;
|
||||
// service_master_data2: string;
|
||||
service_transaction: string;
|
||||
service_wallet: string;
|
||||
transaction: string;
|
||||
nationality: string;
|
||||
service_disbursement: string;
|
||||
service_notification: string;
|
||||
service_feedback: string;
|
||||
service_card: string;
|
||||
api_dashboard:string
|
||||
|
||||
// service_dashboard: string;
|
||||
// service_customer: string;
|
||||
// // service_master_data2: string;
|
||||
// service_transaction: string;
|
||||
// service_wallet: string;
|
||||
// transaction: string;
|
||||
// nationality: string;
|
||||
// service_disbursement: string;
|
||||
// service_notification: string;
|
||||
// service_feedback: string;
|
||||
// service_card: string;
|
||||
// api_dashboard:string;
|
||||
}
|
||||
|
||||
const API_URL = import.meta.env.VITE_APP_API_URL;
|
||||
const API_URL_MASTER_DATA = import.meta.env.VITE_APP_API_URL_MASTER_DATA;
|
||||
|
||||
const apiConfig: apiConfigProps = {
|
||||
service_feedback: `${API_URL}/k`,
|
||||
service_user: `${API_URL}`,
|
||||
service_master_data: `${API_URL_MASTER_DATA}`,
|
||||
|
||||
// service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`,
|
||||
service_dashboard: `${API_URL}`,
|
||||
service_customer: `${API_URL}/c`,
|
||||
// service_master_data2: `${API_URL}/m`,
|
||||
service_master_data: `${API_URL}/t`,
|
||||
service_transaction: `${API_URL}/tt`,
|
||||
service_wallet: `${API_URL}/w`,
|
||||
transaction: `${API_URL}/x`,
|
||||
service_disbursement: `${API_URL}/s`,
|
||||
service_notification: `${API_URL}/n`,
|
||||
api_dashboard: `${API_URL}/r`,
|
||||
service_card: `${API_URL}/a`,
|
||||
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||
// service_feedback: `${API_URL}/k`,
|
||||
// service_customer: `${API_URL}/c`,
|
||||
// // service_master_data2: `${API_URL}/m`,
|
||||
// // service_master_data: `${API_URL}/t`,
|
||||
// service_transaction: `${API_URL}/tt`,
|
||||
// service_wallet: `${API_URL}/w`,
|
||||
// transaction: `${API_URL}/x`,
|
||||
// service_disbursement: `${API_URL}/s`,
|
||||
// service_notification: `${API_URL}/n`,
|
||||
// api_dashboard: `${API_URL}/r`,
|
||||
// service_card: `${API_URL}/a`,
|
||||
// nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||
};
|
||||
|
||||
export { apiConfig };
|
||||
|
||||
@ -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 L from 'leaflet';
|
||||
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 ---
|
||||
// Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster
|
||||
@ -194,18 +224,66 @@ const buildSignalPopupHtml = (points: SitePoint[], lat: number, lng: number) =>
|
||||
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 [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(() => {
|
||||
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, {
|
||||
center: DILI_CENTER,
|
||||
@ -219,9 +297,9 @@ const SitesSatelliteMap = () => {
|
||||
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
|
||||
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
|
||||
@ -269,7 +347,7 @@ const SitesSatelliteMap = () => {
|
||||
// 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) {
|
||||
@ -277,15 +355,19 @@ const SitesSatelliteMap = () => {
|
||||
}
|
||||
};
|
||||
map.on('popupopen', onPopupOpen);
|
||||
}, [grouped, sitePoints]);
|
||||
|
||||
// --- 3. Cleanup peta saat komponen benar-benar unmount ---
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
map.off('zoomend', handleZoomVisibility);
|
||||
map.off('popupopen', onPopupOpen);
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
if (mapRef.current) {
|
||||
mapRef.current.off();
|
||||
mapRef.current.remove();
|
||||
mapRef.current = null;
|
||||
}
|
||||
markerByKeyRef.current.clear();
|
||||
};
|
||||
}, [grouped]);
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
@ -312,7 +394,9 @@ const SitesSatelliteMap = () => {
|
||||
<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">
|
||||
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>
|
||||
<input
|
||||
type="text"
|
||||
@ -320,11 +404,23 @@ const SitesSatelliteMap = () => {
|
||||
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>
|
||||
);
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
|
||||
const API_URL = apiConfig.api_dashboard;
|
||||
|
||||
interface Props {
|
||||
startdate: string;
|
||||
enddate: string;
|
||||
}
|
||||
|
||||
const TransactionPieChart = ({ startdate, enddate }: Props) => {
|
||||
|
||||
const { GetData } = useCallApi();
|
||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDataTransactionValue = async () => {
|
||||
try {
|
||||
const res = await GetData(`${API_URL}/transaction-chart`, {
|
||||
date_from: startdate,
|
||||
date_to: enddate,
|
||||
});
|
||||
setResponseTransactionValue(res);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transaction value:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDataTransactionValue();
|
||||
}, [startdate, enddate, GetData]);
|
||||
|
||||
const data = [
|
||||
{ name: 'Transfer', value: parseFloat((responseTransactionValue?.data?.T ?? 0).toFixed(2)), color: '#3490dc' },
|
||||
{ name: 'Return', value: parseFloat((responseTransactionValue?.data?.R ?? 0).toFixed(2)), color: '#a0aec0' },
|
||||
{ name: 'Topup', value: parseFloat((responseTransactionValue?.data?.U ?? 0).toFixed(2)), color: '#9f7aea' },
|
||||
{ name: 'Purchase', value: parseFloat((responseTransactionValue?.data?.P ?? 0).toFixed(2)), color: '#baf7c5' },
|
||||
{ name: 'Withdraw', value: parseFloat((responseTransactionValue?.data?.W ?? 0).toFixed(2)), color: '#f56565' },
|
||||
{ name: 'Top Up Partner', value: parseFloat((responseTransactionValue?.data?.N ?? 0).toFixed(2)), color: '#f7fa52' },
|
||||
{ name: 'Reward', value: parseFloat((responseTransactionValue?.data?.E ?? 0).toFixed(2)), color: '#f50a19' },
|
||||
{ name: 'Purchase Loja', value: parseFloat((responseTransactionValue?.data?.L ?? 0).toFixed(2)), color: '#F0A04B' },
|
||||
{ name: 'Top Up P24', value: parseFloat((responseTransactionValue?.data?.B ?? 0).toFixed(2)), color: '#FADA7A' },
|
||||
{ name: 'Transfer Agent', value: parseFloat((responseTransactionValue?.data?.A ?? 0).toFixed(2)), color: '#B1C29E' },
|
||||
{ name: 'Withdrawal Agent', value: parseFloat((responseTransactionValue?.data?.M ?? 0).toFixed(2)), color: '#FCE7C8' },
|
||||
{ name: 'Top Up Agent', value: parseFloat((responseTransactionValue?.data?.O ?? 0).toFixed(2)), color: '#F0A04B' },
|
||||
{ name: 'Transfer P24', value: parseFloat((responseTransactionValue?.data?.S ?? 0).toFixed(2)), color: '#FADA7A' },
|
||||
{ name: 'Withdraw Merchant', value: parseFloat((responseTransactionValue?.data?.I ?? 0).toFixed(2)), color: '#B1C29E' },
|
||||
{ name: 'Donation', value: parseFloat((responseTransactionValue?.data?.D ?? 0).toFixed(2)), color: '#FCE7C8' },
|
||||
{ name: 'Fee', value: parseFloat((responseTransactionValue?.data?.F ?? 0).toFixed(2)), color: '#F0A04B' },
|
||||
{ name: 'Reversal', value: parseFloat((responseTransactionValue?.data?.V ?? 0).toFixed(2)), color: '#FADA7A' },
|
||||
{ name: 'Cashback Cash', value: parseFloat((responseTransactionValue?.data?.C ?? 0).toFixed(2)), color: '#B1C29E' },
|
||||
{ name: 'Cashback Point', value: parseFloat((responseTransactionValue?.data?.H ?? 0).toFixed(2)), color: '#FCE7C8' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 w-1/3">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-700">Transaction Chart</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="w-40 h-40">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
dataKey="value"
|
||||
innerRadius={40}
|
||||
outerRadius={60}
|
||||
paddingAngle={3}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="text-sm space-y-2">
|
||||
<div className="font-semibold text-gray-500">Active Transaction</div>
|
||||
{data.map((entry, index) => (
|
||||
<div key={index} className="flex items-center justify-between w-48">
|
||||
<div className="flex items-center">
|
||||
<div className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: entry.color }}></div>
|
||||
<span className="text-gray-700">{entry.name}</span>
|
||||
</div>
|
||||
<span className="text-gray-700">{entry.value}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionPieChart;
|
||||
@ -1,153 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { useCallApi } from '@/hooks';
|
||||
|
||||
const API_URL = apiConfig.api_dashboard;
|
||||
|
||||
interface Props {
|
||||
startdate: string;
|
||||
enddate: string;
|
||||
}
|
||||
|
||||
const TransactionValue = ({ startdate, enddate }: Props) => {
|
||||
const { GetData } = useCallApi();
|
||||
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDataTransactionValue = async () => {
|
||||
try {
|
||||
const res = await GetData(`${API_URL}/transaction-value`, {
|
||||
date_from: startdate,
|
||||
date_to: enddate,
|
||||
});
|
||||
setResponseTransactionValue(res);
|
||||
} catch (error) {
|
||||
console.error('Error fetching transaction value:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDataTransactionValue();
|
||||
}, [startdate, enddate, GetData]);
|
||||
|
||||
let transactionData: any[] = [];
|
||||
if (responseTransactionValue?.data?.length > 0) {
|
||||
for (let i = 0; i < responseTransactionValue.data.length; i++) {
|
||||
let type = "";
|
||||
let unit = "";
|
||||
|
||||
const amount = responseTransactionValue.data[i].total_amount;
|
||||
|
||||
if (amount >= 1000 && amount < 1000000) {
|
||||
unit = "K";
|
||||
} else if (amount >= 1000000 && amount < 1000000000) {
|
||||
unit = "M";
|
||||
} else if (amount >= 1000000000) {
|
||||
unit = "B";
|
||||
}
|
||||
|
||||
switch (responseTransactionValue.data[i].kind) {
|
||||
case "P":
|
||||
type = "Purchase";
|
||||
break;
|
||||
case "T":
|
||||
type = "Transfer";
|
||||
break;
|
||||
case "U":
|
||||
type = "Top Up";
|
||||
break;
|
||||
case "W":
|
||||
type = "Withdraw";
|
||||
break;
|
||||
case "R":
|
||||
type = "Return";
|
||||
break;
|
||||
case "N":
|
||||
type = "Top Up Partner";
|
||||
break;
|
||||
case "E":
|
||||
type = "Reward";
|
||||
break;
|
||||
case "L":
|
||||
type = "Purchase Loja"
|
||||
break;
|
||||
case "B":
|
||||
type = "Top Up P24";
|
||||
break;
|
||||
case "A":
|
||||
type = " Transfer Agent";
|
||||
break;
|
||||
case "M":
|
||||
type = "Withdrawal Agent";
|
||||
break;
|
||||
case 'O':
|
||||
type = 'Top Up Agent';
|
||||
break;
|
||||
case 'S':
|
||||
type = 'Transfer P24';
|
||||
break;
|
||||
case 'I':
|
||||
type = 'Withdraw Merchant';
|
||||
break;
|
||||
case 'D':
|
||||
type = 'Donation';
|
||||
break;
|
||||
case 'F':
|
||||
type = 'Fee';
|
||||
break;
|
||||
case 'V':
|
||||
type = 'Reversal';
|
||||
break;
|
||||
case 'C':
|
||||
type = 'Cashback Cash';
|
||||
break;
|
||||
case 'H':
|
||||
type = 'Cashback Point';
|
||||
break;
|
||||
default:
|
||||
type = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
transactionData.push({
|
||||
label: type,
|
||||
value: amount,
|
||||
unit: unit
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow p-6 w-1/3">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
|
||||
</div>
|
||||
{transactionData.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{transactionData.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<span className="w-24 text-sm text-gray-700">{item.label}</span>
|
||||
<div className="flex-1 mx-2">
|
||||
<div className="w-full h-3 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-3 bg-teal-500 rounded-full"
|
||||
style={{ width: `${(item.value / maxValue) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="w-24 text-right text-sm text-gray-700 break-words">
|
||||
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-sm text-gray-500 mt-4">
|
||||
No Data Available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionValue;
|
||||
@ -1,48 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import { get5LastYear } from '@/utils/Date';
|
||||
|
||||
interface YearPickerProps {
|
||||
selectedYear: string;
|
||||
setSelectedYear: (year: string) => void;
|
||||
}
|
||||
|
||||
// Komponen YearPicker
|
||||
const YearPicker = ({ selectedYear, setSelectedYear }: YearPickerProps) => {
|
||||
const selectYear = get5LastYear();
|
||||
|
||||
const handleYearChange = (year: string) => {
|
||||
setSelectedYear(year);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<Select value={selectedYear} onValueChange={handleYearChange}>
|
||||
<SelectTrigger size="sm" className="w-28">
|
||||
<SelectValue placeholder="Pilih Tahun" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectYear.length > 0 ? (
|
||||
selectYear.map((year, index) => (
|
||||
<SelectItem key={index} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="no-data" disabled>
|
||||
No data available
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { YearPicker };
|
||||
@ -1,4 +0,0 @@
|
||||
export * from './Card';
|
||||
export * from './DateRangePicker';
|
||||
export * from './Chart';
|
||||
export * from './YearPicker';
|
||||
@ -1,5 +1,2 @@
|
||||
export * from './DashboardHomeContext';
|
||||
export * from './useDashboardHomeContext';
|
||||
export * from './useFetchCardData';
|
||||
export * from './useFetchChartData';
|
||||
// export * from './useFetchYear';
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
|
||||
interface UseFetchCardDataResult {
|
||||
cardData: any;
|
||||
isCardLoading: boolean;
|
||||
cardError: any;
|
||||
}
|
||||
|
||||
const useFetchCardData = (
|
||||
start_date: string,
|
||||
end_date: string,
|
||||
count: string
|
||||
): UseFetchCardDataResult => {
|
||||
const [cardData, setCardData] = useState<[]>([]);
|
||||
const [isCardLoading, setIsCardLoading] = useState<boolean>(false);
|
||||
const [cardError, setCardError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsCardLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/card`, {
|
||||
params: {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
aggregate: count
|
||||
}
|
||||
});
|
||||
setCardData(response.data.data);
|
||||
} catch (err) {
|
||||
setCardError(err);
|
||||
} finally {
|
||||
setIsCardLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
||||
|
||||
return { cardData, isCardLoading, cardError };
|
||||
};
|
||||
|
||||
export { useFetchCardData };
|
||||
@ -1,49 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
import { formatDate } from 'date-fns';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
|
||||
interface UseFetchChartDataResult {
|
||||
chartData: any[];
|
||||
isChartLoading: boolean;
|
||||
chartError: any;
|
||||
}
|
||||
|
||||
const useFetchChartData = (
|
||||
start_date: string,
|
||||
end_date: string,
|
||||
count: string
|
||||
): UseFetchChartDataResult => {
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
const [isChartLoading, setIsChartLoading] = useState<boolean>(false);
|
||||
const [chartError, setChartError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsChartLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/chart`, {
|
||||
params: {
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
aggregate: count
|
||||
}
|
||||
});
|
||||
setChartData(response.data.data);
|
||||
} catch (err) {
|
||||
setChartError(err);
|
||||
} finally {
|
||||
setIsChartLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [start_date, end_date, count]); // Dependensi pada from dan to
|
||||
|
||||
return { chartData, isChartLoading, chartError };
|
||||
};
|
||||
|
||||
export { useFetchChartData };
|
||||
@ -1,45 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
|
||||
const API_URL = apiConfig.service_dashboard;
|
||||
|
||||
interface YearData {
|
||||
year: string;
|
||||
}
|
||||
interface UseFetchselectYearResult {
|
||||
selectYear: YearData[];
|
||||
isselectYearLoading: boolean;
|
||||
selectYearError: any;
|
||||
}
|
||||
|
||||
const useFetchYear = (
|
||||
path: string = '',
|
||||
start_date: string = '',
|
||||
end_date: string = '',
|
||||
year: string = ''
|
||||
): UseFetchselectYearResult => {
|
||||
const [selectYear, setSelectYear] = useState<YearData[]>([]);
|
||||
const [isselectYearLoading, setIsselectYearLoading] = useState<boolean>(false);
|
||||
const [selectYearError, setSelectYearError] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsselectYearLoading(true);
|
||||
const response = await axios.get(`${API_URL}/dashboard/year`);
|
||||
setSelectYear(response.data.data.map((item: { year: string }) => ({ year: item.year })));
|
||||
} catch (err) {
|
||||
setSelectYearError(err);
|
||||
} finally {
|
||||
setIsselectYearLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [path, start_date, end_date, year]);
|
||||
|
||||
return { selectYear, isselectYearLoading, selectYearError };
|
||||
};
|
||||
|
||||
export { useFetchYear };
|
||||
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