diff --git a/package.json b/package.json index 7745b80..89c60d4 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "react-query": "^3.39.3", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", + "recharts": "^2.15.3", "sonner": "^1.7.0", "styled-components": "^6.1.13", "stylis": "^4.3.4", diff --git a/src/config/api.config.ts b/src/config/api.config.ts index bfff0a4..47c5471 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -9,6 +9,7 @@ interface apiConfigProps { service_disbursement: string; service_notification: string; service_feedback: string; + api_dashboard:string } const API_URL = import.meta.env.VITE_APP_API_URL; @@ -24,6 +25,7 @@ const apiConfig: apiConfigProps = { transaction: `${API_URL}/x`, service_disbursement: `${API_URL}/s`, service_notification: `${API_URL}/n`, + api_dashboard: `${API_URL}/r`, nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/` }; diff --git a/src/pages/dashboards/home/DashboardHomePage.tsx b/src/pages/dashboards/home/DashboardHomePage.tsx index 5c90b52..c591184 100644 --- a/src/pages/dashboards/home/DashboardHomePage.tsx +++ b/src/pages/dashboards/home/DashboardHomePage.tsx @@ -20,6 +20,9 @@ import BalanceCard from './blocks/BalanceCard'; 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'; @@ -37,6 +40,98 @@ const DashboardHomePage = () => { from: 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(null); + + const fetchData = async () => { + const res = await GetData(`${API_URL}/card-statistic`, {}); + setResponseStatisticCard(res); + }; + + useEffect(() => { + fetchData(); + }, []); + + const [responseGraphic, setresponseGraphic] = useState(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 = {}; + 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 idCustomer = getAuth()?.user.customer?.id; // console.log(currentRole); @@ -85,21 +180,15 @@ const DashboardHomePage = () => { 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(() => { - setDateRange({ - from: moment(`${initialYear}-01-01`, 'YYYY-MM-DD').toDate(), - to: moment(`${initialYear}-12-31`, 'YYYY-MM-DD').toDate() - }); + const firstDay = getFirstDayOfMonth(); + const today = getToday(); + + setFromDate(firstDay); + setToDate(today); + + // reset filter yang lain kalau perlu + setDateRange({ from: firstDay, to: today }); setSelectedYear(initialYear); setCount('sum'); setChartType('line'); @@ -158,24 +247,76 @@ const DashboardHomePage = () => { ) : null} -
- -
- + {/* Cards */} +
+ + + + + + +
+ +
+
+ + +
- {/* - - */} +
- {/* Cards */} -
-
- {cardData.map((data: any) => ( -
- -
- ))} -
-
{/* Chart */} -
+
data.data)} + chartData={staticChartDataApiFetch} chartType={chartType} chartLegend={chartLegend} />
+ +
+ + + +
+ ); diff --git a/src/pages/dashboards/home/blocks/Card.tsx b/src/pages/dashboards/home/blocks/Card.tsx index 5d1d08f..d2f065a 100644 --- a/src/pages/dashboards/home/blocks/Card.tsx +++ b/src/pages/dashboards/home/blocks/Card.tsx @@ -4,46 +4,31 @@ import { toAbsoluteUrl } from '@/utils/Assets'; interface CardDataProduct { title: string; - total: string; - type: Array<{ label: string; value: string; total: string }>; - count: 'sum' | 'count'; + total: number; + growth: number; + surplus: boolean; + icon: string; } -const Card = ({ title, total, type, count }: CardDataProduct) => { +const Card = ({ title, total, growth, surplus,icon }: CardDataProduct) => { const { isRTL } = useLanguage(); return ( -
+
-
+
{title} - - {count === 'sum' ? fCurrency(total) : total} + {total} + + {surplus ? '▲' : '▼'} {growth} % From Last Week
-
- Chart Icon -
- {/*
-
- {type.map((data: any, index: number) => ( -
-
-
{data.label}
-
- {count === 'sum' ? fCurrency(data.total) : data.total}{' '} -
-
- {index !== type.length - 1 &&
} -
- ))} -
*/}
); diff --git a/src/pages/dashboards/home/blocks/MemberActivity.tsx b/src/pages/dashboards/home/blocks/MemberActivity.tsx new file mode 100644 index 0000000..172c019 --- /dev/null +++ b/src/pages/dashboards/home/blocks/MemberActivity.tsx @@ -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(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 = parseFloat((responseTransactionValue?.data?.total_customer_active_percentage ?? 0).toFixed(2)) ?? 0; + const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0; + const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0; + + // Hitung sudut pointer + const angle = (percentage / 100) * 180; // 0° (kiri) ke 180° (kanan) + const radians = (angle * Math.PI) / 180; + 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 - Math.PI); // offset agar mulai dari kiri + const y = center + pointerLength * Math.sin(radians - Math.PI); + + // 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 ( +
+
+

Member Activity

+
+ +
+ {/* Sidebar */} +
    +
  • + + Total Customer: {totalCustomer} +
  • +
  • + + Active Customer: {activeCustomer} +
  • +
+ + {/* Gauge Chart */} +
+
+

Active User

+
+ + {/* Background arc */} + + {/* Active arc */} + 50 ? 1 : 0} 1 ${arcX} ${arcY}`} + fill="none" + stroke="#34d399" + strokeWidth="10" + /> + {/* Pointer */} + + +
+ {percentage}% + 100% +
+
+
+
+
+
+ ); +}; + +export default MemberActivity; diff --git a/src/pages/dashboards/home/blocks/TransactionPieChart.tsx b/src/pages/dashboards/home/blocks/TransactionPieChart.tsx new file mode 100644 index 0000000..d7b768d --- /dev/null +++ b/src/pages/dashboards/home/blocks/TransactionPieChart.tsx @@ -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(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 ( +
+
+

Transaction Chart

+
+ +
+
+ + + + {data.map((entry, index) => ( + + ))} + + + +
+ +
+
Active Transaction
+ {data.map((entry, index) => ( +
+
+
+ {entry.name} +
+ {entry.value}% +
+ ))} +
+
+
+ ); +}; + +export default TransactionPieChart; diff --git a/src/pages/dashboards/home/blocks/TransactionValue.tsx b/src/pages/dashboards/home/blocks/TransactionValue.tsx new file mode 100644 index 0000000..bb6c53d --- /dev/null +++ b/src/pages/dashboards/home/blocks/TransactionValue.tsx @@ -0,0 +1,110 @@ +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(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; + 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 ( +
+
+

Transaction Value

+
+ {transactionData.length > 0 ? ( +
+ {transactionData.map((item, index) => ( +
+ {item.label} +
+
+
+
+
+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit} +
+ ))} +
+ ) : ( +
+ No Data Available +
+ )} +
+ ); +}; + +export default TransactionValue; diff --git a/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx index 8c4862b..c4418cd 100644 --- a/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx +++ b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx @@ -248,7 +248,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { limit, page: page + 1, with_deleted: false, - order_field: "id", + order_field: "execution_date", order_direction: 'DESC', filter: JSON.stringify(formattedFilter) }); @@ -286,7 +286,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { pagination={{ size: 10 }} toolbar={} layout={{ card: true }} - sorting={[{ id: 'id', desc: false }]} + sorting={[{ id: 'execution_date', desc: false }]} serverSide={true} onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => getTransactionLists(pageIndex, pageSize, sorting, columnFilters) diff --git a/src/pages/master/conversion/hooks/ManageConversionContext.tsx b/src/pages/master/conversion/hooks/ManageConversionContext.tsx index 730bae1..eceb4fa 100644 --- a/src/pages/master/conversion/hooks/ManageConversionContext.tsx +++ b/src/pages/master/conversion/hooks/ManageConversionContext.tsx @@ -137,16 +137,16 @@ const ManageConversionContextProvider = ({ children }: { children: React.ReactNo > - + */} ); }, - meta: { headerClassName: 'w-[100px]', cellClassName: 'text-center' } + meta: { headerClassName: 'w-[100px] text-center', cellClassName: 'text-center' } } ], [handleEditDialog, handleDeleteDialog] diff --git a/src/pages/master/currency/hooks/ManageCurrencyContext.tsx b/src/pages/master/currency/hooks/ManageCurrencyContext.tsx index f025f87..2795014 100644 --- a/src/pages/master/currency/hooks/ManageCurrencyContext.tsx +++ b/src/pages/master/currency/hooks/ManageCurrencyContext.tsx @@ -124,16 +124,16 @@ const ManageCurrencyContextProvider = ({ children }: { children: React.ReactNode > - + */} ); }, - meta: { headerClassName: 'w-[100px]', cellClassName: 'text-center' } + meta: { headerClassName: 'w-[100px] text-center', cellClassName: 'text-center' } } ], [handleEditDialog, handleDeleteDialog] diff --git a/src/pages/master/walletRule/hooks/ManageWalletRuleContext.tsx b/src/pages/master/walletRule/hooks/ManageWalletRuleContext.tsx index 14e66c3..eb25709 100644 --- a/src/pages/master/walletRule/hooks/ManageWalletRuleContext.tsx +++ b/src/pages/master/walletRule/hooks/ManageWalletRuleContext.tsx @@ -192,17 +192,17 @@ const ManageWalletRuleContextProvider = ({ children }: { children: React.ReactNo > - + */} ); }, meta: { - headerClassName: 'w-[100px]', + headerClassName: 'w-[100px] text-center', cellClassName: 'text-center' } } diff --git a/src/pages/members/feedback-member/FeedbackMember.tsx b/src/pages/members/feedback-member/FeedbackMember.tsx index d8d8810..242b47c 100644 --- a/src/pages/members/feedback-member/FeedbackMember.tsx +++ b/src/pages/members/feedback-member/FeedbackMember.tsx @@ -1,17 +1,23 @@ import { Container, DataGridInner } from '@/components'; -import { ManageFeedbackMemberProvider } from './hooks/ManageFeedbackMemberContext'; +import { ManageFeedbackMemberProvider } from './hooks/ManageFeedbackMemberContext'; // import EditDialog from './blocks/EditDialog'; // import DeleteDialog from './blocks/DeleteDialog'; import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; +import FeedbackDetail from './blocks/FeedbackDetail'; +import EditDialog from './blocks/EditDialog'; const FeedbackMemberMaster = () => { return ( <> - TPAY | Manage Provider + + TPAY | Manage Provider + -

Manage Feedback Member

+

+ Manage Feedback Member +

Dashboard @@ -28,9 +34,12 @@ const FeedbackMemberMaster = () => {
+ + {/* */} + {/* */}
diff --git a/src/pages/members/feedback-member/blocks/EditDialog.tsx b/src/pages/members/feedback-member/blocks/EditDialog.tsx new file mode 100644 index 0000000..53a0715 --- /dev/null +++ b/src/pages/members/feedback-member/blocks/EditDialog.tsx @@ -0,0 +1,189 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { useCallApi } from '@/hooks'; +import { apiConfig } from '@/config/api.config'; +import { getAuth } from '@/auth'; +import { useManageFeedbackContext } from '../hooks/useManageFeedbackMemberContext'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { X } from 'lucide-react'; + +const API_URL = apiConfig.service_feedback; + +// Define status types +type StatusCode = 'W' | 'N' | 'Y'; + +// Status display mapping for Select component +const statusDisplayMap: Record = { + W: 'Waiting Follow Up', + N: 'Rejected', + Y: 'Accepted' +}; + +const EditDialog = () => { + const [loading, setLoading] = useState(false); + const { PutData, GetData } = useCallApi(); + const { handleEditDialog, showEditDialog, selectedFeedback, handleDetailDialog, refreshData } = + useManageFeedbackContext(); + + const [reviewNotes, setReviewNotes] = useState(''); + const [status, setStatus] = useState('Y'); // Default to Accepted + + useEffect(() => { + if (showEditDialog && selectedFeedback) { + fetchFeedbackDetail(); + } + }, [showEditDialog, selectedFeedback]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const username = getAuth()?.user.username; + const payload = { + review_by: username, + review_notes: reviewNotes, + status: status + }; + + const response = await PutData(`${API_URL}/feedback/update/${selectedFeedback}`, payload); + + if (response?.status) { + toast.success('Feedback reviewed successfully'); + + // Close the edit dialog + handleEditDialog(false, null); + + // First close the detail dialog to reset its state + handleDetailDialog(false, null); + + // Refresh the main data grid first + refreshData(); + + // Wait a tiny bit then reopen the detail with refreshed data + setTimeout(() => { + handleDetailDialog(true, selectedFeedback); + }, 300); + } else { + toast.error(response?.message || 'Failed to update feedback'); + } + } catch (error) { + console.error('Error updating feedback:', error); + toast.error('An error occurred while updating feedback'); + } finally { + setLoading(false); + } + }; + + const fetchFeedbackDetail = async () => { + if (!selectedFeedback) return; + try { + setLoading(true); + const response = await GetData(`${API_URL}/feedback/detail/${selectedFeedback}`, { + id: selectedFeedback + }); + + if (!response || !response.data) { + console.error('Invalid response format'); + return; + } + + const data = response.data; + console.log('Fetched data:', data); + + setReviewNotes(data.review_notes || ''); + + const currentStatus = (data.status as StatusCode) || 'Y'; + setStatus(currentStatus); + } catch (error) { + console.error('Error fetching feedback details:', error); + } finally { + setLoading(false); + } + }; + + return ( + handleEditDialog(open, null)}> + +
+
+
+

Review Feedback

+

Review and update feedback status

+
+ +
+ +
+
+
+ +