dashboard view done

This commit is contained in:
wayanrivan
2025-04-24 11:12:00 +07:00
parent fd251919b3
commit 552d1de84c
5 changed files with 250 additions and 34 deletions

View File

@ -21,6 +21,8 @@ import { getAuth } from '@/auth';
import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config';
import TransactionValue from './blocks/TransactionValue';
import TransactionPieChart from './blocks/TransactionPieChart';
import MemberActivity from './blocks/MemberActivity';
// sum -> nominal, count-> total
type CountType = 'sum' | 'count';
@ -339,10 +341,11 @@ const DashboardHomePage = () => {
</div>
</div>
<TransactionValue
startdate={fromDate.toISOString()} // Invoke the toISOString method
enddate={toDate.toISOString()} // Invoke the toISOString method
/>
<div className="flex space-x-4 mt-5">
<TransactionValue startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
<TransactionPieChart startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
<MemberActivity startdate={fromDate.toISOString()} enddate={toDate.toISOString()} />
</div>
</Container>
</>

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

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

View File

@ -30,21 +30,22 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
}, [startdate, enddate, GetData]);
let transactionData: any[] = [];
if (responseTransactionValue?.data.length > 0) {
for (let i = 0; i < responseTransactionValue?.data.length; i++) {
if (responseTransactionValue?.data?.length > 0) {
for (let i = 0; i < responseTransactionValue.data.length; i++) {
let type = "";
let unit = "";
if(responseTransactionValue?.data[i].total_amount>=1000 && responseTransactionValue?.data[i].total_amount<1000000){
unit = "K"
}else if(responseTransactionValue?.data[i].total_amount>=1000000 && responseTransactionValue?.data[i].total_amount<1000000000){
unit = "M"
}
else if(responseTransactionValue?.data[i].total_amount>=1000000000){
unit = "B"
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) {
switch (responseTransactionValue.data[i].kind) {
case "P":
type = "Purchase";
break;
@ -67,7 +68,7 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
transactionData.push({
label: type,
value: responseTransactionValue?.data[i].total_amount,
value: amount,
unit: unit
});
}
@ -76,26 +77,32 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
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">
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
</div>
<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>
{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-14 text-right text-sm text-gray-700">{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}</span>
</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>
);
};

View File

@ -45,6 +45,8 @@ const DetailTransaction = () => {
id: selectedTransactionId
});
setTransactionDetails(response?.data);
console.log(response?.data);
console.log(selectedTransactionId);
} catch (error) {
console.error('Error fetching transaction', error);
}
@ -72,6 +74,7 @@ const DetailTransaction = () => {
<DialogHeader>
<DialogTitle>Transaction Details</DialogTitle>
</DialogHeader>
<DialogDescription></DialogDescription>
<DialogBody>
{/* Tabs Navigation */}
<div className="flex border-b border-gray-200">
@ -207,7 +210,7 @@ const DetailTransaction = () => {
</div>
<div>
<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>
<p className="text-sm text-gray-500">Name</p>