update dashboard view
This commit is contained in:
@ -9,6 +9,7 @@ interface apiConfigProps {
|
|||||||
service_disbursement: string;
|
service_disbursement: string;
|
||||||
service_notification: string;
|
service_notification: string;
|
||||||
service_feedback: string;
|
service_feedback: string;
|
||||||
|
api_dashboard:string
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_APP_API_URL;
|
const API_URL = import.meta.env.VITE_APP_API_URL;
|
||||||
@ -24,6 +25,7 @@ const apiConfig: apiConfigProps = {
|
|||||||
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`,
|
||||||
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import BalanceCard from './blocks/BalanceCard';
|
|||||||
import { getAuth } from '@/auth';
|
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';
|
||||||
|
|
||||||
// sum -> nominal, count-> total
|
// sum -> nominal, count-> total
|
||||||
type CountType = 'sum' | 'count';
|
type CountType = 'sum' | 'count';
|
||||||
@ -37,6 +38,98 @@ const DashboardHomePage = () => {
|
|||||||
from: new Date(),
|
from: new Date(),
|
||||||
to: new Date()
|
to: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFirstDayOfMonth = () => {
|
||||||
|
const now = new Date();
|
||||||
|
return new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToday = () => {
|
||||||
|
return new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [fromDate, setFromDate] = useState(getFirstDayOfMonth());
|
||||||
|
const [toDate, setToDate] = useState(getToday());
|
||||||
|
|
||||||
|
|
||||||
|
const API_URL = apiConfig.api_dashboard;
|
||||||
|
const { GetData } = useCallApi();
|
||||||
|
|
||||||
|
const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
const res = await GetData(`${API_URL}/card-statistic`, {});
|
||||||
|
setResponseStatisticCard(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [responseGraphic, setresponseGraphic] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDataGraphic = async () => {
|
||||||
|
const res = await GetData(`${API_URL}/cashin-vs-cashout`, {
|
||||||
|
date_from: fromDate.toISOString(),
|
||||||
|
date_to: toDate.toISOString()
|
||||||
|
});
|
||||||
|
setresponseGraphic(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDataGraphic();
|
||||||
|
}, [fromDate, toDate]);
|
||||||
|
|
||||||
|
let staticChartDataApiFetch = [];
|
||||||
|
|
||||||
|
// Make map for date => { cashin: 0, cashout: 0 }
|
||||||
|
const dataMap: Record<string, { cashin: number; cashout: number }> = {};
|
||||||
|
const current = moment(fromDate);
|
||||||
|
const end = moment(toDate);
|
||||||
|
|
||||||
|
// Inisialization dataMap with all dates
|
||||||
|
while (current.isSameOrBefore(end, 'day')) {
|
||||||
|
const dateStr = current.format('DD-MM-YYYY');
|
||||||
|
dataMap[dateStr] = { cashin: 0, cashout: 0 };
|
||||||
|
current.add(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cashin
|
||||||
|
if (Array.isArray(responseGraphic?.data.total_cashin)) {
|
||||||
|
responseGraphic.data.total_cashin.forEach((item: any) => {
|
||||||
|
const date = moment(item.created_date).format('DD-MM-YYYY');
|
||||||
|
// Only update cashin if it's not already set (meaning no value was added before)
|
||||||
|
if (dataMap[date]) {
|
||||||
|
dataMap[date].cashin = Number(item.total_count ?? 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cashout
|
||||||
|
if (Array.isArray(responseGraphic?.data.total_cashout)) {
|
||||||
|
responseGraphic.data.total_cashout.forEach((item: any) => {
|
||||||
|
const date = moment(item.created_date).format('DD-MM-YYYY');
|
||||||
|
// Only update cashout if it's not already set (meaning no value was added before)
|
||||||
|
if (dataMap[date]) {
|
||||||
|
dataMap[date].cashout = Number(item.total_count ?? 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to array for chart
|
||||||
|
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
||||||
|
month,
|
||||||
|
cashin: values.cashin,
|
||||||
|
cashout: values.cashout
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Change to array for chart
|
||||||
|
staticChartDataApiFetch = Object.entries(dataMap).map(([month, values]) => ({
|
||||||
|
month,
|
||||||
|
cashin: values.cashin,
|
||||||
|
cashout: values.cashout
|
||||||
|
}));
|
||||||
|
|
||||||
const currentRole = getAuth()?.role_name;
|
const currentRole = getAuth()?.role_name;
|
||||||
const idCustomer = getAuth()?.user.customer?.id;
|
const idCustomer = getAuth()?.user.customer?.id;
|
||||||
// console.log(currentRole);
|
// console.log(currentRole);
|
||||||
@ -85,21 +178,15 @@ const DashboardHomePage = () => {
|
|||||||
setChartLegend(value);
|
setChartLegend(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleFilter = useCallback(
|
|
||||||
// (date: DateRange | undefined) => {
|
|
||||||
// setDateRange({
|
|
||||||
// from: date?.from ?? moment(`${selectedYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
|
||||||
// to: date?.to ?? moment(`${selectedYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// [selectedYear]
|
|
||||||
// );
|
|
||||||
|
|
||||||
const resetFilter = useCallback(() => {
|
const resetFilter = useCallback(() => {
|
||||||
setDateRange({
|
const firstDay = getFirstDayOfMonth();
|
||||||
from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(),
|
const today = getToday();
|
||||||
to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate()
|
|
||||||
});
|
setFromDate(firstDay);
|
||||||
|
setToDate(today);
|
||||||
|
|
||||||
|
// reset filter yang lain kalau perlu
|
||||||
|
setDateRange({ from: firstDay, to: today });
|
||||||
setSelectedYear(initialYear);
|
setSelectedYear(initialYear);
|
||||||
setCount('sum');
|
setCount('sum');
|
||||||
setChartType('line');
|
setChartType('line');
|
||||||
@ -158,24 +245,76 @@ const DashboardHomePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex gap-3 items-center mb-6">
|
{/* Cards */}
|
||||||
<YearPicker selectedYear={selectedYear} setSelectedYear={handleYearChange} />
|
<div className="flex gap-6 overflow-x-auto pb-2">
|
||||||
<div className="w-auto min-w-[120px]">
|
<Card
|
||||||
<Select value={count} onValueChange={handleCountType}>
|
title="Registered Users"
|
||||||
<SelectTrigger size="sm">
|
total={responseStatisticCard?.data.total_registered ?? 0}
|
||||||
<SelectValue placeholder="Select Count Type" />
|
growth={responseStatisticCard?.data.registered_last_week.percent ?? 0}
|
||||||
</SelectTrigger>
|
surplus={responseStatisticCard?.data.registered_last_week.surplus ?? false}
|
||||||
<SelectContent className="w-32">
|
icon="test"
|
||||||
<SelectItem value="sum">Nominal</SelectItem>
|
/>
|
||||||
<SelectItem value="count">Status</SelectItem>
|
<Card
|
||||||
</SelectContent>
|
title="Unregistered Users"
|
||||||
</Select>
|
total={responseStatisticCard?.data.total_unregistered ?? 0}
|
||||||
|
growth={responseStatisticCard?.data.unregistered_last_week.percent ?? 0}
|
||||||
|
surplus={responseStatisticCard?.data.unregistered_last_week.surplus ?? false}
|
||||||
|
icon="test"
|
||||||
|
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Total Cash-in"
|
||||||
|
total={responseStatisticCard?.data.total_cash_in ?? 0}
|
||||||
|
growth={responseStatisticCard?.data.cash_in_last_week.percent ?? 0}
|
||||||
|
surplus={responseStatisticCard?.data.cash_in_last_week.surplus ?? false}
|
||||||
|
icon="test"
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Total Cash-out"
|
||||||
|
total={responseStatisticCard?.data.total_cash_out ?? 0}
|
||||||
|
growth={responseStatisticCard?.data.cash_out_last_week.percent ?? 0}
|
||||||
|
surplus={responseStatisticCard?.data.cash_out_last_week.surplus ?? false}
|
||||||
|
icon="test"
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Active Event"
|
||||||
|
total={responseStatisticCard?.data.total_event ?? 0}
|
||||||
|
growth={responseStatisticCard?.data.event_last_week.percent ?? 0}
|
||||||
|
surplus={responseStatisticCard?.data.event_last_week.surplus ?? false}
|
||||||
|
icon="test"
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Total Billing"
|
||||||
|
total={responseStatisticCard?.data.total_billing ?? 0}
|
||||||
|
growth={responseStatisticCard?.data.billing_last_week.percent ?? 0}
|
||||||
|
surplus={responseStatisticCard?.data.billing_last_week.surplus ?? false}
|
||||||
|
icon='test'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 items-center mb-6 mt-6">
|
||||||
|
<div className="flex gap-3 items-center w-full md:w-auto">
|
||||||
|
<label className="input input-sm w-[160px]">
|
||||||
|
From
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="from"
|
||||||
|
value={moment(fromDate).format('YYYY-MM-DD')}
|
||||||
|
onChange={(e) => setFromDate(new Date(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="input input-sm w-[160px]">
|
||||||
|
To
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="to"
|
||||||
|
value={moment(toDate).format('YYYY-MM-DD')}
|
||||||
|
onChange={(e) => setToDate(new Date(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{/* <DefaultTooltip title="Filter" placement="top">
|
|
||||||
<Button variant="outline" className="h-7.5" onClick={() => handleFilter(dateRange)}>
|
|
||||||
<KeenIcon icon="filter" />
|
|
||||||
</Button>
|
|
||||||
</DefaultTooltip> */}
|
|
||||||
<DefaultTooltip title="Reset Filter" placement="top">
|
<DefaultTooltip title="Reset Filter" placement="top">
|
||||||
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
<Button variant="outline" className="h-7.5" onClick={resetFilter}>
|
||||||
<KeenIcon icon="arrow-circle-left" />
|
<KeenIcon icon="arrow-circle-left" />
|
||||||
@ -183,37 +322,28 @@ const DashboardHomePage = () => {
|
|||||||
</DefaultTooltip>
|
</DefaultTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cards */}
|
|
||||||
<div className="grid gap-5 lg:gap-7.5 mb-5">
|
|
||||||
<div className="grid lg:grid-cols-4 gap-y-5 lg:gap-5 items-stretch">
|
|
||||||
{cardData.map((data: any) => (
|
|
||||||
<div className="lg:col-span-1" key={data.type}>
|
|
||||||
<Card
|
|
||||||
title={data.type.toUpperCase()}
|
|
||||||
total={data.total}
|
|
||||||
type={data.type}
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart */}
|
||||||
<div className="grid gap-5 lg:gap-7.5">
|
<div className="grid gap-5 lg:gap-7.5 mt-5">
|
||||||
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
<div className="grid lg:grid-cols-1 gap-y-5 lg:gap-5 items-stretch">
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<Chart
|
<Chart
|
||||||
title="Overview"
|
title="Overview"
|
||||||
count={12}
|
count={12}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
chartData={staticChartData.series.map((data: any) => data.data)}
|
chartData={staticChartDataApiFetch}
|
||||||
chartType={chartType}
|
chartType={chartType}
|
||||||
chartLegend={chartLegend}
|
chartLegend={chartLegend}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TransactionValue
|
||||||
|
startdate={fromDate.toISOString()} // Invoke the toISOString method
|
||||||
|
enddate={toDate.toISOString()} // Invoke the toISOString method
|
||||||
|
/>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,46 +4,31 @@ import { toAbsoluteUrl } from '@/utils/Assets';
|
|||||||
|
|
||||||
interface CardDataProduct {
|
interface CardDataProduct {
|
||||||
title: string;
|
title: string;
|
||||||
total: string;
|
total: number;
|
||||||
type: Array<{ label: string; value: string; total: string }>;
|
growth: number;
|
||||||
count: 'sum' | 'count';
|
surplus: boolean;
|
||||||
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Card = ({ title, total, type, count }: CardDataProduct) => {
|
const Card = ({ title, total, growth, surplus,icon }: CardDataProduct) => {
|
||||||
const { isRTL } = useLanguage();
|
const { isRTL } = useLanguage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card h-full bg-[length:85%] bg-[length:85%] [background-position:9rem_-4rem] rtl:[background-position:-4rem_-4rem] bg-no-repeat channel-stats-bg">
|
<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="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 justify-between">
|
||||||
<div className="flex flex-col w-8/12" style={{ background: '' }}>
|
<div className="flex flex-col w-full">
|
||||||
<span className="text-sm font-normal text-gray-600 mb-5">{title}</span>
|
<span className="text-sm font-normal text-gray-600 mb-5">{title}</span>
|
||||||
<span className="text-3xl font-semibold text-gray-900 mb-0">
|
<span className="text-3xl font-semibold text-gray-900 mb-0">{total}</span>
|
||||||
{count === 'sum' ? fCurrency(total) : total}
|
<span
|
||||||
|
className={`text-sm font-semibold mt-2 flex items-center gap-1 ${
|
||||||
|
surplus ? 'text-green-500' : 'text-red-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{surplus ? '▲' : '▼'} {growth} % From Last Week
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-4/12 justify-between" style={{ background: '' }}>
|
|
||||||
<img
|
|
||||||
src={toAbsoluteUrl('/media/file-types/chart.svg')}
|
|
||||||
className="dark:hidden h-5 h-8"
|
|
||||||
alt="Chart Icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <hr className="" />
|
|
||||||
<div className="">
|
|
||||||
{type.map((data: any, index: number) => (
|
|
||||||
<div key={data.label}>
|
|
||||||
<div className="flex justify-between mb-1">
|
|
||||||
<div className="w-8/12 text-sm">{data.label}</div>
|
|
||||||
<div className="w-4/12 text-sm text-end">
|
|
||||||
{count === 'sum' ? fCurrency(data.total) : data.total}{' '}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{index !== type.length - 1 && <hr className="border-dashed my-2" />}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
103
src/pages/dashboards/home/blocks/TransactionValue.tsx
Normal file
103
src/pages/dashboards/home/blocks/TransactionValue.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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 = "";
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
default:
|
||||||
|
type = "Unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionData.push({
|
||||||
|
label: type,
|
||||||
|
value: responseTransactionValue?.data[i].total_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-full max-w-md mt-5">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="w-14 text-right text-sm text-gray-700">{item.value}{item.unit}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransactionValue;
|
||||||
Reference in New Issue
Block a user