dashboard view done
This commit is contained in:
@ -21,6 +21,8 @@ import { getAuth } from '@/auth';
|
|||||||
import { useCallApi } from '@/hooks';
|
import { useCallApi } from '@/hooks';
|
||||||
import { apiConfig } from '@/config/api.config';
|
import { apiConfig } from '@/config/api.config';
|
||||||
import TransactionValue from './blocks/TransactionValue';
|
import TransactionValue from './blocks/TransactionValue';
|
||||||
|
import TransactionPieChart from './blocks/TransactionPieChart';
|
||||||
|
import MemberActivity from './blocks/MemberActivity';
|
||||||
|
|
||||||
// sum -> nominal, count-> total
|
// sum -> nominal, count-> total
|
||||||
type CountType = 'sum' | 'count';
|
type CountType = 'sum' | 'count';
|
||||||
@ -339,10 +341,11 @@ const DashboardHomePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransactionValue
|
<div className="flex space-x-4 mt-5">
|
||||||
startdate={fromDate.toISOString()} // Invoke the toISOString method
|
<TransactionValue startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||||
enddate={toDate.toISOString()} // Invoke the toISOString method
|
<TransactionPieChart startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||||
/>
|
<MemberActivity startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
|||||||
114
src/pages/dashboards/home/blocks/MemberActivity.tsx
Normal file
114
src/pages/dashboards/home/blocks/MemberActivity.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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 MemberActivity = ({ startdate, enddate }: Props) => {
|
||||||
|
const { GetData } = useCallApi();
|
||||||
|
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDataTransactionValue = async () => {
|
||||||
|
try {
|
||||||
|
const res = await GetData(`${API_URL}/active-user`, {
|
||||||
|
date_from: startdate,
|
||||||
|
date_to: enddate,
|
||||||
|
});
|
||||||
|
setResponseTransactionValue(res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching transaction value:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDataTransactionValue();
|
||||||
|
}, [startdate, enddate, GetData]);
|
||||||
|
|
||||||
|
const percentage = responseTransactionValue?.data?.total_customer_active_percentage ?? 0;
|
||||||
|
const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0;
|
||||||
|
const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0;
|
||||||
|
|
||||||
|
// Hitung sudut pointer
|
||||||
|
const angle = 180 - (percentage / 100) * 180; // 180° (kiri bawah) ke 0° (kanan bawah)
|
||||||
|
const radians = (angle * Math.PI) / 180; // Mengubah derajat ke radian
|
||||||
|
const radius = 40; // Radius dari setengah lingkaran
|
||||||
|
const center = 50; // Titik pusat lingkaran
|
||||||
|
const pointerLength = 25; // Panjang pointer
|
||||||
|
|
||||||
|
const x = center + pointerLength * Math.cos(radians);
|
||||||
|
const y = center + pointerLength * Math.sin(radians);
|
||||||
|
|
||||||
|
// Hitung titik akhir untuk arc aktif
|
||||||
|
const arcAngle = (Math.PI * percentage) / 100;
|
||||||
|
const arcX = 50 + radius * Math.cos(Math.PI - arcAngle);
|
||||||
|
const arcY = 50 - radius * Math.sin(arcAngle);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 bg-white rounded-lg shadow-md w-full max-w-xl">
|
||||||
|
<div className="flex justify-between items-center pb-3 mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-700">Member Activity</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<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>
|
||||||
|
</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 className="w-full h-full" viewBox="0 0 100 50">
|
||||||
|
{/* Background arc */}
|
||||||
|
<path
|
||||||
|
d="M 10 50 A 40 40 0 0 1 90 50"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
strokeWidth="10"
|
||||||
|
/>
|
||||||
|
{/* Active arc */}
|
||||||
|
<path
|
||||||
|
d={`M 10 50 A 40 40 0 ${percentage > 50 ? 1 : 0} 1 ${arcX} ${arcY}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="#34d399"
|
||||||
|
strokeWidth="10"
|
||||||
|
/>
|
||||||
|
{/* Pointer */}
|
||||||
|
<line
|
||||||
|
x1="50"
|
||||||
|
y1="50"
|
||||||
|
x2={x}
|
||||||
|
y2={y}
|
||||||
|
stroke="#111827"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 px-1">
|
||||||
|
<span>{percentage}%</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberActivity;
|
||||||
89
src/pages/dashboards/home/blocks/TransactionPieChart.tsx
Normal file
89
src/pages/dashboards/home/blocks/TransactionPieChart.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6 w-full max-w-md">
|
||||||
|
<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;
|
||||||
@ -30,21 +30,22 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
|
|||||||
}, [startdate, enddate, GetData]);
|
}, [startdate, enddate, GetData]);
|
||||||
|
|
||||||
let transactionData: any[] = [];
|
let transactionData: any[] = [];
|
||||||
if (responseTransactionValue?.data.length > 0) {
|
if (responseTransactionValue?.data?.length > 0) {
|
||||||
for (let i = 0; i < responseTransactionValue?.data.length; i++) {
|
for (let i = 0; i < responseTransactionValue.data.length; i++) {
|
||||||
let type = "";
|
let type = "";
|
||||||
let unit = "";
|
let unit = "";
|
||||||
|
|
||||||
if(responseTransactionValue?.data[i].total_amount>=1000 && responseTransactionValue?.data[i].total_amount<1000000){
|
const amount = responseTransactionValue.data[i].total_amount;
|
||||||
unit = "K"
|
|
||||||
}else if(responseTransactionValue?.data[i].total_amount>=1000000 && responseTransactionValue?.data[i].total_amount<1000000000){
|
if (amount >= 1000 && amount < 1000000) {
|
||||||
unit = "M"
|
unit = "K";
|
||||||
}
|
} else if (amount >= 1000000 && amount < 1000000000) {
|
||||||
else if(responseTransactionValue?.data[i].total_amount>=1000000000){
|
unit = "M";
|
||||||
unit = "B"
|
} else if (amount >= 1000000000) {
|
||||||
|
unit = "B";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (responseTransactionValue?.data[i].kind) {
|
switch (responseTransactionValue.data[i].kind) {
|
||||||
case "P":
|
case "P":
|
||||||
type = "Purchase";
|
type = "Purchase";
|
||||||
break;
|
break;
|
||||||
@ -67,7 +68,7 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
|
|||||||
|
|
||||||
transactionData.push({
|
transactionData.push({
|
||||||
label: type,
|
label: type,
|
||||||
value: responseTransactionValue?.data[i].total_amount,
|
value: amount,
|
||||||
unit: unit
|
unit: unit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -76,26 +77,32 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
|
|||||||
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
|
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow p-6 w-full max-w-md mt-5">
|
<div className="bg-white rounded-xl shadow p-6 w-full max-w-md">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
|
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
{transactionData.length > 0 ? (
|
||||||
{transactionData.map((item, index) => (
|
<div className="space-y-4">
|
||||||
<div key={index} className="flex items-center justify-between">
|
{transactionData.map((item, index) => (
|
||||||
<span className="w-24 text-sm text-gray-700">{item.label}</span>
|
<div key={index} className="flex items-center justify-between">
|
||||||
<div className="flex-1 mx-2">
|
<span className="w-24 text-sm text-gray-700">{item.label}</span>
|
||||||
<div className="w-full h-3 bg-gray-200 rounded-full">
|
<div className="flex-1 mx-2">
|
||||||
<div
|
<div className="w-full h-3 bg-gray-200 rounded-full">
|
||||||
className="h-3 bg-teal-500 rounded-full"
|
<div
|
||||||
style={{ width: `${(item.value / maxValue) * 100}%` }}
|
className="h-3 bg-teal-500 rounded-full"
|
||||||
></div>
|
style={{ width: `${(item.value / maxValue) * 100}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="w-14 text-right text-sm text-gray-700">{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="w-14 text-right text-sm text-gray-700">{item.value}{item.unit}</span>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
</div>
|
<div className="text-center text-sm text-gray-500 mt-4">
|
||||||
|
No Data Available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,6 +45,8 @@ const DetailTransaction = () => {
|
|||||||
id: selectedTransactionId
|
id: selectedTransactionId
|
||||||
});
|
});
|
||||||
setTransactionDetails(response?.data);
|
setTransactionDetails(response?.data);
|
||||||
|
console.log(response?.data);
|
||||||
|
console.log(selectedTransactionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching transaction', error);
|
console.error('Error fetching transaction', error);
|
||||||
}
|
}
|
||||||
@ -72,6 +74,7 @@ const DetailTransaction = () => {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Transaction Details</DialogTitle>
|
<DialogTitle>Transaction Details</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
{/* Tabs Navigation */}
|
{/* Tabs Navigation */}
|
||||||
<div className="flex border-b border-gray-200">
|
<div className="flex border-b border-gray-200">
|
||||||
@ -207,7 +210,7 @@ const DetailTransaction = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Description</p>
|
<p className="text-sm text-gray-500">Description</p>
|
||||||
<p className="font-medium">{transactionDetails?.description}</p>
|
<p className="font-medium">{transactionDetails?.description || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Name</p>
|
<p className="text-sm text-gray-500">Name</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user