update get data site points from api

This commit is contained in:
wayanrivan
2026-07-08 10:07:08 +07:00
parent bb2aa0e3cd
commit bf8daa74fa
24 changed files with 149 additions and 2783 deletions

View File

@ -1,4 +1,4 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
@ -6,6 +6,7 @@ GENERATE_SOURCEMAP=false
# VITE_APP_API_URL=http://127.0.0.1:4003/apitest # VITE_APP_API_URL=http://127.0.0.1:4003/apitest
# VITE_APP_API_URL=https://tpay.shiblysolution.id/api # VITE_APP_API_URL=https://tpay.shiblysolution.id/api
VITE_APP_API_URL=http://127.0.0.1:7001/api VITE_APP_API_URL=http://127.0.0.1:7001/api
VITE_APP_API_URL_MASTER_DATA=http://127.0.0.1:7002/api
# VITE_APP_API_URL=http://api-v2.tpay.tl/api # VITE_APP_API_URL=http://api-v2.tpay.tl/api
# VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api # VITE_APP_URL_TRANSACTION=https://tpay.shiblysolution.id/test/x/api
# VITE_APP_API_URL=http://127.0.0.1:4003/apitesting # VITE_APP_API_URL=http://127.0.0.1:4003/apitesting

View File

@ -1,4 +1,4 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false
VITE_APP_API_URL=https://api-v2.tpay.tl/api VITE_APP_API_URL=https://api-v2.tpay.tl/api

View File

@ -1,4 +1,4 @@
VITE_APP_NAME=tpay-dashboard-tl VITE_APP_NAME=revenue-tt-dashboard
VITE_APP_VERSION=1=9.1.1 VITE_APP_VERSION=1=9.1.1
GENERATE_SOURCEMAP=false GENERATE_SOURCEMAP=false

View File

@ -83,7 +83,7 @@ const Login = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>TPAY | Sign In</title> <title>REVENUE | Sign In</title>
</Helmet> </Helmet>
<div className="card max-w-[390px] w-full"> <div className="card max-w-[390px] w-full">
<form <form

View File

@ -14,7 +14,7 @@ import { type AuthModel, type UserModel } from '@/auth';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_user;
export const LOGIN_URL = `${API_URL}/login`; export const LOGIN_URL = `${API_URL}/login`;
export const GET_USER_URL = `${API_URL}/user/profile`; export const GET_USER_URL = `${API_URL}/user/profile`;
@ -87,7 +87,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
try { try {
const { data: user } = await getUser(); const { data: user } = await getUser();
// Perbarui auth yang sekarang dengan statusbalance
saveAuth({ saveAuth({
...auth, ...auth,
}); });

View File

@ -1,36 +1,41 @@
interface apiConfigProps { interface apiConfigProps {
service_dashboard: string; service_user: string;
service_customer: string;
service_master_data: string; service_master_data: string;
// service_master_data2: string;
service_transaction: string;
service_wallet: string;
transaction: string;
nationality: string;
service_disbursement: string;
service_notification: string;
service_feedback: string;
service_card: string;
api_dashboard:string
// service_dashboard: string;
// service_customer: string;
// // service_master_data2: string;
// service_transaction: string;
// service_wallet: string;
// transaction: string;
// nationality: string;
// service_disbursement: string;
// service_notification: string;
// service_feedback: string;
// service_card: string;
// api_dashboard:string;
} }
const API_URL = import.meta.env.VITE_APP_API_URL; const API_URL = import.meta.env.VITE_APP_API_URL;
const API_URL_MASTER_DATA = import.meta.env.VITE_APP_API_URL_MASTER_DATA;
const apiConfig: apiConfigProps = { const apiConfig: apiConfigProps = {
service_feedback: `${API_URL}/k`, service_user: `${API_URL}`,
service_master_data: `${API_URL_MASTER_DATA}`,
// service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`, // service_dashboard: `${API_URL}${import.meta.env.VITE_ENV != 'development' ? '/d' : ''}`,
service_dashboard: `${API_URL}`, // service_feedback: `${API_URL}/k`,
service_customer: `${API_URL}/c`, // service_customer: `${API_URL}/c`,
// service_master_data2: `${API_URL}/m`, // // service_master_data2: `${API_URL}/m`,
service_master_data: `${API_URL}/t`, // // service_master_data: `${API_URL}/t`,
service_transaction: `${API_URL}/tt`, // service_transaction: `${API_URL}/tt`,
service_wallet: `${API_URL}/w`, // service_wallet: `${API_URL}/w`,
transaction: `${API_URL}/x`, // transaction: `${API_URL}/x`,
service_disbursement: `${API_URL}/s`, // service_disbursement: `${API_URL}/s`,
service_notification: `${API_URL}/n`, // service_notification: `${API_URL}/n`,
api_dashboard: `${API_URL}/r`, // api_dashboard: `${API_URL}/r`,
service_card: `${API_URL}/a`, // service_card: `${API_URL}/a`,
nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/` // nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/`
}; };
export { apiConfig }; export { apiConfig };

View File

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

View File

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

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

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

View File

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

View File

@ -1,7 +1,37 @@
import { useEffect, useRef, useState, useMemo } from 'react'; import { useEffect, useRef, useState, useMemo } from 'react';
import L from 'leaflet'; import L from 'leaflet';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { sitePoints, SitePoint } from '../sitePoints'; import { apiConfig } from '@/config/api.config';
import { getAuth } from '@/auth';
interface SitePoint {
lat: number;
lng: number;
cell: string;
sector: string;
site: string;
location: string;
}
// --- KONFIGURASI API ---
const API_MASTER_DATA = apiConfig.service_master_data;
const fetchSitePoints = async (): Promise<SitePoint[]> => {
const token = getAuth()?.access_token;
const res = await fetch(`${API_MASTER_DATA}/site-points/all-data`, {
headers: {
accept: '*/*',
...(token ? { Authorization: `Bearer ${token}` } : {})
}
});
if (!res.ok) {
throw new Error(`Failed to fetch site points: ${res.status}`);
}
const json = await res.json();
return json.data?.list ?? json;
};
// --- TAMBAHAN DATA MOCK POLYGON UNTUK AREA KLASTER --- // --- TAMBAHAN DATA MOCK POLYGON UNTUK AREA KLASTER ---
// Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster // Dilengkapi data placeholder untuk kebutuhan Dialog Popup Klaster
@ -197,15 +227,63 @@ const SitesSatelliteMap = () => {
const markerLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup()); const markerLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
const polygonLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup()); const polygonLayerGroupRef = useRef<L.LayerGroup>(L.layerGroup());
const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map()); const markerByKeyRef = useRef<Map<string, L.Marker>>(new Map());
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedCount, setSelectedCount] = useState(sitePoints.length); const [sitePoints, setSitePoints] = useState<SitePoint[]>([]);
const [selectedCount, setSelectedCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const grouped = useMemo(() => groupByCoordinate(sitePoints), []); const grouped = useMemo(() => groupByCoordinate(sitePoints), [sitePoints]);
// --- 1. Fetch data site points dari API saat komponen mount ---
useEffect(() => { useEffect(() => {
if (!mapContainerRef.current || mapRef.current) return; let isMounted = true;
const loadData = async () => {
try {
setIsLoading(true);
const data = await fetchSitePoints();
if (isMounted) {
setSitePoints(data);
setSelectedCount(data.length);
setError(null);
}
} catch (err) {
if (isMounted) {
setError('Gagal memuat data site points');
console.error(err);
}
} finally {
if (isMounted) setIsLoading(false);
}
};
loadData();
return () => {
isMounted = false;
};
}, []);
// --- 2. Inisialisasi peta & render marker/polygon setelah data tersedia ---
useEffect(() => {
if (!mapContainerRef.current || sitePoints.length === 0) return;
// Kalau map sudah pernah dibuat sebelumnya, cukup refresh marker-nya saja
if (mapRef.current) {
markerLayerGroupRef.current.clearLayers();
markerByKeyRef.current.clear();
grouped.forEach((points, key) => {
const [lat, lng] = key.split(',').map(Number);
const marker = L.marker([lat, lng], { icon: pinIcon() });
marker.bindPopup(buildSignalPopupHtml(points, lat, lng), { maxWidth: 320, minWidth: 290 });
marker.addTo(markerLayerGroupRef.current);
markerByKeyRef.current.set(key, marker);
});
return;
}
const map = L.map(mapContainerRef.current, { const map = L.map(mapContainerRef.current, {
center: DILI_CENTER, center: DILI_CENTER,
@ -277,15 +355,19 @@ const SitesSatelliteMap = () => {
} }
}; };
map.on('popupopen', onPopupOpen); map.on('popupopen', onPopupOpen);
}, [grouped, sitePoints]);
// --- 3. Cleanup peta saat komponen benar-benar unmount ---
useEffect(() => {
return () => { return () => {
map.off('zoomend', handleZoomVisibility); if (mapRef.current) {
map.off('popupopen', onPopupOpen); mapRef.current.off();
map.remove(); mapRef.current.remove();
mapRef.current = null; mapRef.current = null;
}
markerByKeyRef.current.clear(); markerByKeyRef.current.clear();
}; };
}, [grouped]); }, []);
const handleSearch = (value: string) => { const handleSearch = (value: string) => {
setSearch(value); setSearch(value);
@ -312,7 +394,9 @@ const SitesSatelliteMap = () => {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-sm text-gray-600"> <div className="text-sm text-gray-600">
Showing <span className="font-semibold">{selectedCount}</span> from {sitePoints.length} points (Cell/Sector) {isLoading
? 'Memuat data...'
: `Showing ${selectedCount} from ${sitePoints.length} points (Cell/Sector)`}
</div> </div>
<input <input
type="text" type="text"
@ -320,11 +404,23 @@ const SitesSatelliteMap = () => {
onChange={(e) => handleSearch(e.target.value)} onChange={(e) => handleSearch(e.target.value)}
placeholder="Cari Cell, Sector, atau nama Site..." placeholder="Cari Cell, Sector, atau nama Site..."
className="input input-sm w-full sm:w-72" className="input input-sm w-full sm:w-72"
disabled={isLoading}
/> />
</div> </div>
{error && (
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
{error}
</div>
)}
<div className="relative w-full rounded-lg overflow-hidden border border-gray-200" style={{ height: '70vh' }}> <div className="relative w-full rounded-lg overflow-hidden border border-gray-200" style={{ height: '70vh' }}>
<div ref={mapContainerRef} className="w-full h-full" /> <div ref={mapContainerRef} className="w-full h-full" />
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/60">
<span className="text-sm text-gray-500">Memuat peta...</span>
</div>
)}
</div> </div>
</div> </div>
); );

View File

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

View File

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

View File

@ -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 };

View File

@ -1,4 +0,0 @@
export * from './Card';
export * from './DateRangePicker';
export * from './Chart';
export * from './YearPicker';

View File

@ -1,5 +1,2 @@
export * from './DashboardHomeContext'; export * from './DashboardHomeContext';
export * from './useDashboardHomeContext'; export * from './useDashboardHomeContext';
export * from './useFetchCardData';
export * from './useFetchChartData';
// export * from './useFetchYear';

View File

@ -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 };

View File

@ -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 };

View File

@ -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

View File

@ -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']
}
};