From 7db3e72476964af99590a83d3f91e3356f2094ea Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Wed, 23 Apr 2025 17:00:02 +0700 Subject: [PATCH 01/19] update dashboard view --- src/config/api.config.ts | 2 + .../dashboards/home/DashboardHomePage.tsx | 226 ++++++++++++++---- src/pages/dashboards/home/blocks/Card.tsx | 43 ++-- .../home/blocks/TransactionValue.tsx | 103 ++++++++ 4 files changed, 297 insertions(+), 77 deletions(-) create mode 100644 src/pages/dashboards/home/blocks/TransactionValue.tsx 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..c2c53b2 100644 --- a/src/pages/dashboards/home/DashboardHomePage.tsx +++ b/src/pages/dashboards/home/DashboardHomePage.tsx @@ -20,6 +20,7 @@ import BalanceCard from './blocks/BalanceCard'; import { getAuth } from '@/auth'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; +import TransactionValue from './blocks/TransactionValue'; // sum -> nominal, count-> total type CountType = 'sum' | 'count'; @@ -37,6 +38,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 +178,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 +245,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/TransactionValue.tsx b/src/pages/dashboards/home/blocks/TransactionValue.tsx new file mode 100644 index 0000000..63c43d5 --- /dev/null +++ b/src/pages/dashboards/home/blocks/TransactionValue.tsx @@ -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(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 ( +
+
+

Transaction Value

+
+
+ {transactionData.map((item, index) => ( +
+ {item.label} +
+
+
+
+
+ {item.value}{item.unit} +
+ ))} +
+
+ ); +}; + +export default TransactionValue; From 1b4ac298b81e6c563fbc5b170a5cb5838e9549a2 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Apr 2025 18:41:51 +0700 Subject: [PATCH 02/19] fix detail member --- .../manage-members/CustomerDetailModal.tsx | 616 ------------------ .../manage-members/blocks/DetailMember.tsx | 8 +- 2 files changed, 7 insertions(+), 617 deletions(-) delete mode 100644 src/pages/members/manage-members/CustomerDetailModal.tsx diff --git a/src/pages/members/manage-members/CustomerDetailModal.tsx b/src/pages/members/manage-members/CustomerDetailModal.tsx deleted file mode 100644 index e7c2110..0000000 --- a/src/pages/members/manage-members/CustomerDetailModal.tsx +++ /dev/null @@ -1,616 +0,0 @@ -import React, { useState, useEffect } from "react"; -import axios from 'axios'; -import { Dialog,DialogActions,DialogContent,DialogTitle,TextField,Button,MenuItem,Select,InputLabel,FormControl,Typography, - InputAdornment,Grid,Box,List,ListItem, -} from "@mui/material"; -import UploadFileIcon from "@mui/icons-material/UploadFile"; -import Divider from '@mui/material/Divider'; -import { initialMember } from "./Columns"; -import { apiConfig } from '@/config/api.config'; -import ConfirmDialog from '@/components/confirm'; -import { toast } from 'sonner'; -const BASE_URL_MASTER_DATA = apiConfig.service_master_data; -const BASE_URL_CUSTOMER = apiConfig.service_customer; -// MAIN PAGE -const CustomerDialog = ({ open, handleClose, handleSubmit, initialData, viewStats, handleReject, page, fetchCustomers }: any) => { - const [formData, setFormData] = useState(initialData || initialMember); - const [viewOnly, setViewOnly] = useState(viewStats || false); - const [municipios, setMunicipios] = useState([]); - const [aldeias, setAldeias] = useState([]); - const [postoAdm, setPostoAdm] = useState([]); - const [sucos, setSucos] = useState([]); - const [profession, setProfession] = useState([]); - const [groupData] = useState({ - reguler: `This fill can not be empty!`, - premium: `This field required only for Premium or Agent`, - agent: `This field required only for Agent` - }) - - useEffect(() => { - setFormData(initialData || {}); // Sync formData when initialData changes - fetchMasterData() - }, [initialData]); - - async function fetchMasterData() { - try { - let getProfession = await axios.get(`${BASE_URL_MASTER_DATA}/profession/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setProfession(getProfession.data.data.list) - let getMunicipios = await axios.get(`${BASE_URL_MASTER_DATA}/municipios/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setMunicipios(getMunicipios.data.data.list) - let getPostoAdms = await axios.get(`${BASE_URL_MASTER_DATA}/postoadms/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setPostoAdm(getPostoAdms.data.data.list) - let getSucos = await axios.get(`${BASE_URL_MASTER_DATA}/sucos/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setSucos(getSucos.data.data.list) - let getAldeias = await axios.get(`${BASE_URL_MASTER_DATA}/aldeias/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setAldeias(getAldeias.data.data.list) - } catch (error) { - console.log(error); - } - } - - const handleChange = async (e: any) => { - const { name, value } = e.target; - if (name === 'municipio' || name === 'posto_adms' || name === 'suco') await getMasterAfter(name, value); - if (name === "file_selfie" || name === "photouser" || name === 'file_document_id' || name === "file_document_id_selfie" || - name === "file_commercial_license") { // FOR FILE ONLY - setFormData({ ...formData, [name]: e.target.files[0] }); - } else { - setFormData({ ...formData, [name]: value }); - } - }; - - async function getMasterAfter(name: string, id: any) { - if (name === 'municipio') { - let getMunicipiosPosto = await axios.get(`${BASE_URL_MASTER_DATA}/municipios/postoadms/${id}`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setPostoAdm(getMunicipiosPosto.data.data) - } - if (name === 'posto_adms') { - let getPostoSuco = await axios.get(`${BASE_URL_MASTER_DATA}/postoadms/sucos/${id}`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setSucos(getPostoSuco.data.data) - } - if (name === 'suco') { - let getSucoAldeias = await axios.get(`${BASE_URL_MASTER_DATA}/sucos/aldeias/${id}`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setAldeias(getSucoAldeias.data.data) - } - } - - const onSubmit = () => { - if (page === 'kyc' && !formData.description) return toast.warning(`Description for approval needed!`) - handleSubmit(formData); - // handleClose(); - }; - - const onReject = () => { - handleReject(formData); - handleClose(); - } - - function generateDate(date: any, type: any) { - if (!date) return '' - const today = new Date(date); - if (type === 'datetime') return today.toISOString().replace('T', ' ').substring(0, 19); - return today.toISOString().split('T')[0]; - } - - return ( - - Customer Form ({viewOnly ? 'View' : 'Edit'}) - -
- Customer Data - Created: {generateDate(formData.created_at, 'datetime')} -
- { - formData.isneedapproval == 1 ? ( -
- User Request for approval -
- ) : ('') - } - - - {/* {groupData.reguler} */} - - - {fileTextFile("Photo", formData.photouser, "photouser", handleChange)} - - - - - - - - Gender - - - - Identity Type - - - - {/* AGENT & PREMIUM DATA */} - - - - - - {fileTextFile("File Selfie", formData.file_selfie, "file_selfie", handleChange)} - {"file_selfie"} - {fileTextFile("File Document", formData.file_document_id, "file_document_id", handleChange)} - {"file_document_id"} - {fileTextFile("File Document & Selfie", formData.file_document_id_selfie, "file_document_id_selfie", handleChange)} - {"file_document_id_selfie"} - {fileTextFile("File Commercial License", formData.file_commercial_license, "file_commercial_license", handleChange)} - {"file_commercial_license"} - {/* AGENT & PREMIUM DATA */} - - {/* */} - - Profession - - - {/* */} - {/* */} - - Status - - - - - Municipio - - - - - Posto - - - - - Suco - - - - - Aldeia - - - - - Bank - - Bank Name - - - - - - {/* {getAdmAccess(page, formData, handleClose, fetchCustomers)} */} - { page === 'kyc' ? ( - <> - Approval - - - ) : (<> - {getAdmAccess(page, formData, handleClose, fetchCustomers, viewOnly, setViewOnly)} - - {formData.id ? showCustomerWallet(formData.id) : ""} - ) - } -
- - { - page === 'kyc' ? ( - - ) : ('') - } - - { - formData.isneedapproval == 1 && page === 'kyc' ? ( - - ) : ('') - } - - -
- ); -}; - -export default CustomerDialog; - -function fileTextFile(label: string, value: any, name: string, handleChange: any) { - return ( - - - {/* */} - - ), - }} - /> - ) -} -// ACCESS ADM -function getAdmAccess(page: string, data: any, handleClose: any, fetchCustomers: any, viewOnly: any, setViewOnly: any) { - const [dialogOpen, setDialogOpen] = useState(false); - const [dialogType, setDialogType] = useState(''); - const [changeGroup, setChangeGroup] = useState(''); - const [changeGroupD, setChangeGroupD] = useState(false); - const [groups, setGroups] = useState([]); - - useEffect(() => { - fetchGroups() - }, []); - - const fetchGroups = async () =>{ - try { - let getGroups = await axios.get(`${BASE_URL_CUSTOMER}/groups/list`, { - params: { - limit: 50, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - } - }); - setGroups(getGroups.data.data.list); - } catch (error: any) { - console.error(error.message); - toast.error(error.message) - } - } - - const handleYes = async () => { - try { - if (dialogType === "update status") { - let statusNext = getPinStatus(data.status).res; - if (statusNext) await axios.put(`${BASE_URL_CUSTOMER}/customer/statuspin`, { customerid: data.id, status: statusNext }) - else toast.error("Handle Active/Suspend only") - toast.success("Success Update Status") - } - if (dialogType === "reset pin") { - if (data.id) await axios.post(`${BASE_URL_CUSTOMER}/customer/resetpin`, { customerid: data.id }) - else throw({ message: 'data.id not found' }) - toast.success("Pin will send to customer MSISDN") - } - } catch (error: any) { - toast.error(error.message) - } finally { - setDialogOpen(false) - handleClose() - } - } - - function buttonStatus() { - setDialogType('update status') - setDialogOpen(true) - } - - function buttonResetPin() { - setDialogType('reset pin') - setDialogOpen(true) - } - - async function buttonChangeGroup() { - try { - let dataObj = { - customerid: data.id, - destination_group: changeGroup - } - if (data.group_id === changeGroup) return toast.warning(`You update same group as the exist customer group`) - if (dataObj.customerid && dataObj.destination_group) { - await axios.post(`${BASE_URL_CUSTOMER}/customer/change-group`, dataObj) - } - await fetchCustomers() - toast.success('Success Change group') - } catch (error: any) { - console.log(error); - toast.error(error.message) - } finally { - await fetchCustomers() - setChangeGroupD(false) - handleClose() - } - } - - function openChangeGroupDialog() { - setChangeGroup(data.group_id); - setChangeGroupD(true) - } - - if (page !== "kyc") { - return ( - - Access Administration - - - - Pin Status : {getPinStatus(data.status).msg} - - - - Reset PIN - - - - - - Change Group - - - - Edit Member - {/* */} - - - - - - setChangeGroupD(false)} fullWidth> - - Are you sure to change customer Group? - - Destination Group - - - - - - - - - - setDialogOpen(false)} - title="Confirm Action" - content={`Are you sure you want to ${dialogType}?`} - onYes={handleYes} - onNo={() => setDialogOpen(false)} - /> - - ) - } else { - return ('') - } -} - -function getPinStatus(status: string) { - if (status === "Y") return { - msg: 'Active', - btn: 'Block PIN', - res: 'Block' - }; - if (status === "N") return { - msg: 'Not Active', - btn: 'Activate PIN', - res: null - }; - if (status === "P") return { - msg: 'Suspend PIN', - btn: 'Unblock PIN', - res: 'UnBlock' - }; - if (status === "O") return { - msg: 'Suspend OTP', - btn: 'Unblock OTP', - res: null - }; - return { - msg: "None", - btn: 'No status found', - res: null - } -} -// CUSTOMER WALLET -function showCustomerWallet(customerid: any) { - const [customerWallet, setCustomerWallet] = useState([]); - if (!customerid) return '' - - useEffect(() => { - fetchCustomerWallet() - }, []); - - async function fetchCustomerWallet() { - try { - let getCustWallet = await axios.get(`${BASE_URL_CUSTOMER}/customer/wallet`, { params: { customerid: customerid }}); - setCustomerWallet(getCustWallet.data.data.wallets) - } catch (error: any) { - toast.error(error.message) - } - } - - return ( - - Wallet Member - - - {customerWallet.length ? (customerWallet.map((item:any, index:any) => ( - - - - Name - - {item.wallet.name} - - - - Description - {item.wallet.description} - - - Balance - {item.amount} - - - Transaction Today - {item.transaction_number_today} - - - {index < customerWallet.length - 1 && } - - ))): "No wallet"} - - - - ) -} \ No newline at end of file diff --git a/src/pages/members/manage-members/blocks/DetailMember.tsx b/src/pages/members/manage-members/blocks/DetailMember.tsx index aa0a951..05c3cb0 100644 --- a/src/pages/members/manage-members/blocks/DetailMember.tsx +++ b/src/pages/members/manage-members/blocks/DetailMember.tsx @@ -40,6 +40,11 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa const [postoAdm, setPostoAdm] = useState([]); const [sucos, setSucos] = useState([]); const [groups, setGroups] = useState([]); + const [identity_type] = useState([ + { id: 'eleitoral_id', name: 'Eleitoral ID' }, + { id: 'bihete_de_identidade', name: 'Bihete de Identidade' }, + { id: 'passport', name: 'Passport' }, + ]); const [genders] = useState([ { name: 'Male',id: 'M' }, { name: 'Female',id: 'F' }]) const [previewImg, setPreviewImg] = useState({ status: false, @@ -215,7 +220,9 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Email', 'email', 'text', true, viewOnly): ''} {/* {generateList(formData, handleChange, status, 'status', 'Status', null, true)} */} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Mother Fullname', 'mother_fullname', 'text', true, viewOnly): ''} + {(formData.id || dialogType === "create") ? generateList(formData, handleChange, identity_type, 'identity_type', 'Identity Type', false, false): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Identity Number', 'identity_number', 'text', false, viewOnly): ''} + {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Address', 'address', 'text', true, viewOnly): ''}
{(formData.id || dialogType === "create") ? generateList(formData, handleChange, profession, 'profession', 'Profession', false, false): ''} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, municipios, 'municipio_id', 'Municipio', false, false): ''} @@ -238,7 +245,6 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa )}
- {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Address', 'address', 'text', true, viewOnly): ''}

Bank Information

{(formData.id || dialogType === "create") ? generateList(formData, handleChange, banks, 'bank_name', 'Bank Name', false, false): ''} From 73abc8c2d7c651748319b82018aeb3d5686a7e50 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Apr 2025 18:50:19 +0700 Subject: [PATCH 03/19] fix detail member --- src/pages/members/manage-members/blocks/DetailMember.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/members/manage-members/blocks/DetailMember.tsx b/src/pages/members/manage-members/blocks/DetailMember.tsx index 05c3cb0..bfc4d54 100644 --- a/src/pages/members/manage-members/blocks/DetailMember.tsx +++ b/src/pages/members/manage-members/blocks/DetailMember.tsx @@ -221,8 +221,8 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa {/* {generateList(formData, handleChange, status, 'status', 'Status', null, true)} */} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Mother Fullname', 'mother_fullname', 'text', true, viewOnly): ''} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, identity_type, 'identity_type', 'Identity Type', false, false): ''} - {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Identity Number', 'identity_number', 'text', false, viewOnly): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Address', 'address', 'text', true, viewOnly): ''} + {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Identity Number', 'identity_number', 'text', false, viewOnly): ''}
{(formData.id || dialogType === "create") ? generateList(formData, handleChange, profession, 'profession', 'Profession', false, false): ''} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, municipios, 'municipio_id', 'Municipio', false, false): ''} From fd251919b35a7416a335014ce30508fa1d18fd7a Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Apr 2025 23:56:52 +0700 Subject: [PATCH 04/19] fix search group --- src/pages/members/manage-members/Columns.tsx | 14 +++++++ .../members/manage-members/ManageMembers.tsx | 14 +++++++ .../manage-members/blocks/ListToolBar.tsx | 40 +++++++++++++------ 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/pages/members/manage-members/Columns.tsx b/src/pages/members/manage-members/Columns.tsx index 6039aba..ae7159d 100644 --- a/src/pages/members/manage-members/Columns.tsx +++ b/src/pages/members/manage-members/Columns.tsx @@ -62,6 +62,20 @@ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef { + return ( + + ); + } + }, { accessorKey: 'group_name', header: ({ column }) => { diff --git a/src/pages/members/manage-members/ManageMembers.tsx b/src/pages/members/manage-members/ManageMembers.tsx index d87a38a..545b816 100644 --- a/src/pages/members/manage-members/ManageMembers.tsx +++ b/src/pages/members/manage-members/ManageMembers.tsx @@ -11,10 +11,12 @@ import { DataGridProvider } from '@/components'; import { toast } from 'sonner'; import { Breadcrumbs, Link } from '@mui/material'; const BASE_URL_MASTER_DATA = apiConfig.service_master_data; +const BASE_URL_CUSTOMER = apiConfig.service_customer; const BASE_URL = apiConfig.service_customer; import { Helmet } from 'react-helmet'; import ListToolbar from './blocks/ListToolBar'; import { RefreshCw } from 'lucide-react'; + const ManageMembers = () => { const [loading, setLoading] = useState(false); const [members, setMembers] = useState([]); @@ -24,6 +26,7 @@ const ManageMembers = () => { const [isDialogOpen, setIsDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogType, setDialogType] = useState(''); + const [groups, setGroups] = useState([]); const [isReloading, setIsReloading] = useState(false); const closeDialog = () => { setIsDialogOpen(false); @@ -69,6 +72,16 @@ const ManageMembers = () => { } }); setProfession(getProfession.data.data.list); + let getGroups = await axios.get(`${BASE_URL_CUSTOMER}/groups/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC' + } + }); + setGroups(getGroups.data.data.list); } catch (error: any) { toast.error(error.message); console.log(error); @@ -259,6 +272,7 @@ const ManageMembers = () => { createMember={createMember} onReload={handleReload} isReloading={isReloading} + groups={groups} /> } onRowSelectionChange={(selected, table: any) => { diff --git a/src/pages/members/manage-members/blocks/ListToolBar.tsx b/src/pages/members/manage-members/blocks/ListToolBar.tsx index 89c8a1c..3815442 100644 --- a/src/pages/members/manage-members/blocks/ListToolBar.tsx +++ b/src/pages/members/manage-members/blocks/ListToolBar.tsx @@ -3,14 +3,22 @@ import { UserPlus } from 'lucide-react'; import { KeenIcon, useDataGrid } from '@/components'; import { DefaultTooltip } from '@/components'; import { useState } from 'react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; interface ListToolbarProps { createMember: () => void; onReload: () => void; isReloading: boolean; + groups: any; } -const ListToolbar = ({ createMember, onReload, isReloading }: ListToolbarProps) => { +const ListToolbar = ({ createMember, onReload, isReloading, groups }: ListToolbarProps) => { const [groupFilter, setGroupFilter] = useState(''); const [usernameFilter, setUsernameFilter] = useState(''); @@ -20,9 +28,11 @@ const ListToolbar = ({ createMember, onReload, isReloading }: ListToolbarProps) table.getColumn('username')?.setFilterValue(e.target.value); }; - const handleGroupChange = (e: React.ChangeEvent) => { - setGroupFilter(e.target.value); - table.getColumn('group_name')?.setFilterValue(e.target.value); + const handleGroupChange = (e: any) => { + let value = e.target.value; + if (value === '__all__') value = ''; + setGroupFilter(value); + table.getColumn('group_name')?.setFilterValue(value); }; return ( @@ -35,15 +45,21 @@ const ListToolbar = ({ createMember, onReload, isReloading }: ListToolbarProps) placeholder="Search Username" value={usernameFilter} onChange={handleUsernameChange} - className="input input-sm w-40" - /> - +
- +
+ + + +
diff --git a/src/pages/dashboards/home/blocks/MemberActivity.tsx b/src/pages/dashboards/home/blocks/MemberActivity.tsx new file mode 100644 index 0000000..bdfd43e --- /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 = 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 ( +
+
+

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 index 63c43d5..bb6c53d 100644 --- a/src/pages/dashboards/home/blocks/TransactionValue.tsx +++ b/src/pages/dashboards/home/blocks/TransactionValue.tsx @@ -10,7 +10,7 @@ interface Props { } const TransactionValue = ({ startdate, enddate }: Props) => { - const { GetData } = useCallApi(); + const { GetData } = useCallApi(); const [responseTransactionValue, setResponseTransactionValue] = useState(null); useEffect(() => { @@ -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++) { - let type = ""; + 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; @@ -61,13 +62,13 @@ const TransactionValue = ({ startdate, enddate }: Props) => { type = "Return"; break; default: - type = "Unknown"; + type = "Unknown"; break; } 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 ( -
+

Transaction Value

-
- {transactionData.map((item, index) => ( -
- {item.label} -
-
-
+ {transactionData.length > 0 ? ( +
+ {transactionData.map((item, index) => ( +
+ {item.label} +
+
+
+
+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit}
- {item.value}{item.unit} -
- ))} -
+ ))} +
+ ) : ( +
+ No Data Available +
+ )}
); }; diff --git a/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx index 199a8bc..39f5b07 100644 --- a/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx +++ b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx @@ -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 = () => { Transaction Details + {/* Tabs Navigation */}
@@ -207,7 +210,7 @@ const DetailTransaction = () => {

Description

-

{transactionDetails?.description}

+

{transactionDetails?.description || '-'}

Name

From 3c15508b34ea8073d23b60c9dbba5bb385606993 Mon Sep 17 00:00:00 2001 From: Wikzyy Date: Thu, 24 Apr 2025 11:32:12 +0700 Subject: [PATCH 07/19] add confirmation dialog and fix error toast message --- .../TransactionDisbursement.tsx | 192 +++++++++++++----- .../transaction/topup/TransactionTopup.tsx | 130 +++++++++--- 2 files changed, 243 insertions(+), 79 deletions(-) diff --git a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx index 72c660d..984a8ed 100644 --- a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx +++ b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx @@ -1,31 +1,47 @@ -import { Container, DataGridInner } from '@/components'; +import { Alert, Container, DataGridInner } from '@/components'; import { TransactionDisbursementProvider } from './hooks/TransactionDisbursementContext'; import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { useState ,useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; import { toast } from 'sonner'; import { getAuth } from '@/auth'; +import { RefreshCw } from 'lucide-react'; const TransactionDisbursement = () => { - const [form, setForm] = useState({ + const initialForm: { + msisdn: string; + amount: string; + pin: string; + } = { msisdn: '', amount: '', pin: '' - }); + }; + + const [form, setForm] = useState(initialForm); const [wallets, setWallets] = useState([]); const { GetData, PostData } = useCallApi(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); const parsedUser = getAuth()?.user; const API_URL = apiConfig.transaction; - const API_URL_WALLET = apiConfig.service_wallet + const API_URL_WALLET = apiConfig.service_wallet; + const [alert, setAlert] = useState({ + show: false, + message: '' + }); useEffect(() => { const fetchWallets = async () => { try { - const response = await GetData(`${API_URL_WALLET}/dashboard/balance/account/${parsedUser.customer.id}`, {}); + const response = await GetData( + `${API_URL_WALLET}/dashboard/balance/account/${parsedUser.customer.id}`, + {} + ); if (response?.status === true) { setWallets(response.data || []); } else { @@ -39,30 +55,50 @@ const TransactionDisbursement = () => { fetchWallets(); }, []); - const handleSubmit = async (e: any) => { - e.preventDefault(); - console.log('Submitted Data:', form); - if (form.amount == '' || form.msisdn == '' || form.pin == '') { - toast.warning('Please fill in all required fields.') - return - } + const doPostData = async (form: typeof initialForm) => { + setIsSubmitting(true); + try { - let requestTopup = await PostData(`${API_URL}/transaction/topup-downline`, { + let response = await PostData(`${API_URL}/transaction/topup-downline`, { msisdn_destination: form.msisdn, amount: form.amount, pin: form.pin - }) - if (requestTopup?.status == true) { - toast.success('Success Request Topup') + }); + if (response?.status == true) { + toast.success('Success Request Topup'); } else { - toast.warning(`${requestTopup?.message}`) + toast.error(`${response?.message?.message}`); } - } catch (error) { - toast.warning('Failed') + } catch (error: any) { + const errorMessage = + error?.response?.data?.message || error?.message || 'Something went wrong'; + toast.error(errorMessage); + setAlert({ show: true, message: errorMessage }); + } finally { + setIsSubmitting(false); + setShowConfirmation(false); } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (form.amount == '' || form.msisdn == '' || form.pin == '') { + setAlert({ + show: true, + message: 'Please fill in all required fields.' + }); + return; + } + setAlert({ show: false, message: '' }); + setShowConfirmation(true); // TODO: Kirim ke backend atau proses lainnya }; + const handleCancelSubmit = () => { + setShowConfirmation(false); + }; + return ( <> @@ -70,7 +106,9 @@ const TransactionDisbursement = () => { -

MANAGE TRANSACTION DISBURSEMENT SALDO

+

+ MANAGE TRANSACTION DISBURSEMENT SALDO +

Dashboard @@ -90,51 +128,97 @@ const TransactionDisbursement = () => {

{wallet.wallet}

- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(wallet.amount)} + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + wallet.amount + )}

))}
+
+ {alert.show && ( + +

{alert.message}

+
+ )} {/* form */} -
-
- * - setForm({ ...form, msisdn: e.target.value })} - /> -
-
- * - setForm({ ...form, amount: e.target.value })} - /> -
-
- * - setForm({ ...form, pin: e.target.value })} - /> -
-
- -
-
+
+
+ + * + setForm({ ...form, msisdn: e.target.value })} + /> +
+
+ + * + setForm({ ...form, amount: e.target.value })} + /> +
+
+ + * + setForm({ ...form, pin: e.target.value })} + /> +
+
+ +
+
+ + {showConfirmation && ( +
+
+

Confirm Transaction

+

+ Are you sure you want to disbursement saldo of{' '} + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + Number(form.amount) + )}{' '} + + ? +

+
+ + +
+
+
+ )} diff --git a/src/pages/transaction/topup/TransactionTopup.tsx b/src/pages/transaction/topup/TransactionTopup.tsx index a05fb95..1b339a0 100644 --- a/src/pages/transaction/topup/TransactionTopup.tsx +++ b/src/pages/transaction/topup/TransactionTopup.tsx @@ -1,31 +1,46 @@ -import { Container, DataGridInner } from '@/components'; +import { Alert, Container, DataGridInner } from '@/components'; import { TransactionTopupProvider } from './hooks/TransactionTopupContext'; import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; import { toast } from 'sonner'; import { getAuth } from '@/auth'; +import { RefreshCw } from 'lucide-react'; const TransactionTopup = () => { - const [form, setForm] = useState({ + const initialState: { + topupAmount: string; + pin: string; + } = { topupAmount: '', pin: '' + }; + + const [form, setForm] = useState(initialState); + const [alert, setAlert] = useState({ + show: false, + message: '' }); const [wallets, setWallets] = useState([]); const { GetData, PostData } = useCallApi(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); const parsedUser = getAuth()?.user; const API_URL = apiConfig.transaction; - const API_URL_WALLET = apiConfig.service_wallet + const API_URL_WALLET = apiConfig.service_wallet; useEffect(() => { const fetchWallets = async () => { try { - const response = await GetData(`${API_URL_WALLET}/dashboard/balance/account/${parsedUser.customer.id}`, {}); + const response = await GetData( + `${API_URL_WALLET}/dashboard/balance/account/${parsedUser.customer.id}`, + {} + ); if (response?.status === true) { setWallets(response.data || []); } else { @@ -39,30 +54,49 @@ const TransactionTopup = () => { fetchWallets(); }, []); - const handleSubmit = async (e: any) => { - e.preventDefault(); - console.log('Submitted Data:', form); + const doPostData = async (form: typeof initialState) => { + setIsSubmitting(true); - if (form.pin == '' || form.topupAmount == '') { - toast.warning('Please fill in all required fields.') - return - } try { - let requestTopup = await PostData(`${API_URL}/transaction/request-topup`, { + let response = await PostData(`${API_URL}/transaction/request-topup`, { amount: form.topupAmount, pin: form.pin - }) - if (requestTopup?.status == true) { - toast.success('Success Request Topup') + }); + console.log(response); + if (response?.status == true) { + toast.success('Success Request Topup'); } else { - toast.warning(`${requestTopup?.message}`) + toast.warning(`${response?.message}`); } - } catch (error) { - toast.warning('Failed') + } catch (error: any) { + const errorMessage = + error?.response?.data?.message || error?.message || 'Something went wrong'; + toast.error(errorMessage); + } finally { + setIsSubmitting(false); + setShowConfirmation(false); } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + // console.log('Submitted Data:', form); + + if (form.pin == '' || form.topupAmount == '') { + setAlert({ show: true, message: 'Please fill in all required fields.' }); + return; + } + + setAlert({ show: false, message: '' }); + setShowConfirmation(true); + // TODO: Kirim ke backend atau proses lainnya }; + const handleCancelSubmit = () => { + setShowConfirmation(false); + }; + return ( <> @@ -70,7 +104,9 @@ const TransactionTopup = () => { -

MANAGE TRANSACTION TOPUP REQUEST

+

+ MANAGE TRANSACTION TOPUP REQUEST +

Dashboard @@ -82,6 +118,7 @@ const TransactionTopup = () => { Topup + {/* Wallet Section */}

Your Wallets

@@ -90,7 +127,9 @@ const TransactionTopup = () => {

{wallet.wallet}

- {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(wallet.amount)} + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + wallet.amount + )}

))} @@ -99,10 +138,16 @@ const TransactionTopup = () => {
+ {alert.show && ( + +

{alert.message}

+
+ )} {/* form */} -
+
- * + + * { />
- * + + * { />
- +
+ {showConfirmation && ( +
+
+

Confirm Transaction

+

+ Are you sure you want to request a topup of{' '} + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + Number(form.topupAmount) + )}{' '} + {' '} + ? +

+
+ + +
+
+
+ )} From a9eb67c0cf1ad40de494219dadc1bc354c1adfdb Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Thu, 24 Apr 2025 11:40:03 +0700 Subject: [PATCH 08/19] update --- .../blocks/ApprovalDialog.tsx | 30 +++++++++++++++++-- .../blocks/DetailApprovalTransaction.tsx | 1 + 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx index 03f8069..270bef9 100644 --- a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx @@ -8,6 +8,7 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { @@ -26,7 +27,7 @@ const API_URL = apiConfig.transaction; const ApprovalDialog = () => { const { GetData, PostData } = useCallApi(); - const { reload } = useDataGrid(); + const { reload } = useDataGrid(); const { showApprovalDialog, @@ -40,6 +41,7 @@ const ApprovalDialog = () => { transaction_code: '', status: '', notes: '', + pin: '' }); const [alert, setAlert] = useState({ @@ -63,8 +65,9 @@ const ApprovalDialog = () => { const response = await PostData(`${API_URL}/transaction/set-approval`, { id_transaction: transactionDetails.id, - status: formField.status, notes: formField.notes, + status: formField.status, + pin: formField.pin, }); if (response?.status === false) { @@ -97,8 +100,9 @@ const ApprovalDialog = () => { if (showApprovalDialog) { setFormField({ transaction_code: '', - status: '', notes: '', + status: '', + pin:'' }); setTransactionDetails(null); setAlert({ show: false, message: '' }); @@ -142,6 +146,7 @@ const ApprovalDialog = () => { Approval Transaction +
@@ -165,6 +170,25 @@ const ApprovalDialog = () => {
+
+ + +
+ + setFormField((prev) => ({ + ...prev, + pin: e.target.value, + })) + } + /> +
+
{formField.status === 'N' && (
diff --git a/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx index 7ddb01c..6a15c17 100644 --- a/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx +++ b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx @@ -58,6 +58,7 @@ const DetailApprovalTransaction = () => { Transaction Details + {/* Tabs Navigation */}
From 74966c3dbeb085801bf244de9edd9753988da050 Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Thu, 24 Apr 2025 11:41:31 +0700 Subject: [PATCH 09/19] update pin in approval transaction --- .../transaction/approval-transaction/blocks/ApprovalDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx index 270bef9..5eac1cb 100644 --- a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx @@ -175,6 +175,7 @@ const ApprovalDialog = () => {
Date: Thu, 24 Apr 2025 12:06:10 +0700 Subject: [PATCH 10/19] update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) 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", From 9001d75f20f1daa07eacf01778a07259c883c2c2 Mon Sep 17 00:00:00 2001 From: bagusajisaputroo Date: Thu, 24 Apr 2025 13:15:31 +0700 Subject: [PATCH 11/19] removed delete on currency, conversion, and wallet rule --- .../master/conversion/hooks/ManageConversionContext.tsx | 6 +++--- src/pages/master/currency/hooks/ManageCurrencyContext.tsx | 6 +++--- .../master/walletRule/hooks/ManageWalletRuleContext.tsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) 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' } } From 908d96382951ad47633f0076480d08087f61587e Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Thu, 24 Apr 2025 14:07:33 +0700 Subject: [PATCH 12/19] update --- .../transaction/approval-transaction/blocks/ApprovalDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx index 5eac1cb..016fd45 100644 --- a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx @@ -176,7 +176,7 @@ const ApprovalDialog = () => {
Date: Thu, 24 Apr 2025 14:19:43 +0700 Subject: [PATCH 13/19] add dropdown msisdn on disbursement module - set limit 50, filter implemented --- .../TransactionDisbursement.tsx | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx index 984a8ed..208b3ae 100644 --- a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx +++ b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx @@ -4,7 +4,7 @@ import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; import { toast } from 'sonner'; @@ -24,17 +24,57 @@ const TransactionDisbursement = () => { const [form, setForm] = useState(initialForm); const [wallets, setWallets] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [customerMsisdn, setCustomerMsisdn] = useState<{ value: string; label: string }[]>([]); + const [searchTerm, setSearchTerm] = useState(''); + const [dropdownOpen, setDropdownOpen] = useState(false); const { GetData, PostData } = useCallApi(); const [isSubmitting, setIsSubmitting] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const parsedUser = getAuth()?.user; const API_URL = apiConfig.transaction; const API_URL_WALLET = apiConfig.service_wallet; + const API_URL_CUSTOMER = apiConfig.service_customer; + const dropdownRef = useRef(null); + const [alert, setAlert] = useState({ show: false, message: '' }); + const fetchCustomerMsisdn = async (sorting: any, filterValue: string) => { + setIsLoading(true); + const filter: any = + filterValue.trim().length === 0 ? {} : { msisdn: { like: `%${filterValue}%` } }; + + const query: any = { + limit: 50, + page: 1, + with_deleted: false, + order_field: sorting[0].id, + order_direction: sorting[0].desc ? 'DESC' : 'ASC' + }; + + if (filter && Object.keys(filter).length > 0) { + query.filter = JSON.stringify(filter); + // query.page = page + 1; + } + + try { + const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); + setCustomerMsisdn( + response?.data.list.map((item: any) => ({ + value: item.msisdn, + label: `${item.msisdn} - ${item.fullname}` + })) + ); + } catch (error) { + toast.error('Failed to fetch customer msisdn'); + } finally { + setIsLoading(false); + } + }; + useEffect(() => { const fetchWallets = async () => { try { @@ -53,6 +93,18 @@ const TransactionDisbursement = () => { }; fetchWallets(); + fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], ''); + + const handleClickOutside = (event: any) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; }, []); const doPostData = async (form: typeof initialForm) => { @@ -99,6 +151,22 @@ const TransactionDisbursement = () => { setShowConfirmation(false); }; + const handleMsisdnSearch = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + setDropdownOpen(true); + fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], e.target.value); + }; + + const handleMsisdnSelect = (msisdn: string) => { + setForm({ ...form, msisdn }); + setDropdownOpen(false); + setSearchTerm(msisdn); + }; + + const filteredMsisdn = customerMsisdn + .filter((item) => item.label.toLowerCase().includes(searchTerm.toLowerCase())) + .slice(0, 10); + return ( <> @@ -147,16 +215,40 @@ const TransactionDisbursement = () => { )} {/* form */} -
- +
+ * - setForm({ ...form, msisdn: e.target.value })} - /> +
+ setDropdownOpen(true)} + /> + {dropdownOpen && ( +
+ {filteredMsisdn.length > 0 ? ( + filteredMsisdn.map((item, index) => ( +
handleMsisdnSelect(item.value)} + > + {item.label} +
+ )) + ) : ( +
+ {isLoading ? 'Loading...' : 'No results found'} +
+ )} +
+ )} +
+
* From ebd21c089a8ca6570a587f180e8b8f34ff31edcb Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Thu, 24 Apr 2025 14:31:27 +0700 Subject: [PATCH 14/19] update graph in dashboard --- src/pages/dashboards/home/blocks/MemberActivity.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/dashboards/home/blocks/MemberActivity.tsx b/src/pages/dashboards/home/blocks/MemberActivity.tsx index bdfd43e..172c019 100644 --- a/src/pages/dashboards/home/blocks/MemberActivity.tsx +++ b/src/pages/dashboards/home/blocks/MemberActivity.tsx @@ -30,19 +30,19 @@ const MemberActivity = ({ startdate, enddate }: Props) => { fetchDataTransactionValue(); }, [startdate, enddate, GetData]); - const percentage = responseTransactionValue?.data?.total_customer_active_percentage ?? 0; + 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 = 180 - (percentage / 100) * 180; // 180° (kiri bawah) ke 0° (kanan bawah) - const radians = (angle * Math.PI) / 180; // Mengubah derajat ke radian + 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); - const y = center + pointerLength * Math.sin(radians); + 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; From 77831a2dec0e278b7c0e459192169dab367a61ce Mon Sep 17 00:00:00 2001 From: Wikzyy Date: Thu, 24 Apr 2025 14:33:08 +0700 Subject: [PATCH 15/19] add debounce for searching msisdn on disbursement saldo --- .../disbursement-saldo/TransactionDisbursement.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx index 208b3ae..dcc6842 100644 --- a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx +++ b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx @@ -43,12 +43,11 @@ const TransactionDisbursement = () => { }); const fetchCustomerMsisdn = async (sorting: any, filterValue: string) => { - setIsLoading(true); const filter: any = filterValue.trim().length === 0 ? {} : { msisdn: { like: `%${filterValue}%` } }; const query: any = { - limit: 50, + limit: 1, page: 1, with_deleted: false, order_field: sorting[0].id, @@ -152,9 +151,13 @@ const TransactionDisbursement = () => { }; const handleMsisdnSearch = (e: React.ChangeEvent) => { + setIsLoading(true); setSearchTerm(e.target.value); setDropdownOpen(true); - fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], e.target.value); + const timer = setTimeout(() => { + fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], e.target.value); + }, 500); + return () => clearTimeout(timer); }; const handleMsisdnSelect = (msisdn: string) => { From 132861fcb5777e09e4f46d6f7f32a11ae430908f Mon Sep 17 00:00:00 2001 From: Wikzyy Date: Thu, 24 Apr 2025 14:33:54 +0700 Subject: [PATCH 16/19] fix limit to 25 --- .../transaction/disbursement-saldo/TransactionDisbursement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx index dcc6842..c6a6ff6 100644 --- a/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx +++ b/src/pages/transaction/disbursement-saldo/TransactionDisbursement.tsx @@ -47,7 +47,7 @@ const TransactionDisbursement = () => { filterValue.trim().length === 0 ? {} : { msisdn: { like: `%${filterValue}%` } }; const query: any = { - limit: 1, + limit: 25, page: 1, with_deleted: false, order_field: sorting[0].id, From a52bd9e951c9da40a67508307f892748a992262e Mon Sep 17 00:00:00 2001 From: bagusajisaputroo Date: Thu, 24 Apr 2025 14:35:19 +0700 Subject: [PATCH 17/19] added msisdn on table --- .../hooks/ManageKycDeletionContext.tsx | 9 +++++++++ src/pages/members/kyc/Columns.tsx | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/pages/members/kyc-delete-member/hooks/ManageKycDeletionContext.tsx b/src/pages/members/kyc-delete-member/hooks/ManageKycDeletionContext.tsx index 48e4608..e36ab10 100644 --- a/src/pages/members/kyc-delete-member/hooks/ManageKycDeletionContext.tsx +++ b/src/pages/members/kyc-delete-member/hooks/ManageKycDeletionContext.tsx @@ -209,6 +209,15 @@ const ManageKycDeletionContextProvider = ({ children }: { children: React.ReactN headerClassName: 'w-[350px]' } }, + { + accessorFn: (row) => row.msisdn, + id: 'msdisdn', + header: ({ column }) => , + enableSorting: true, + meta: { + headerClassName: 'w-[350px]' + } + }, { accessorFn: (row) => row.registered_email, id: 'email', diff --git a/src/pages/members/kyc/Columns.tsx b/src/pages/members/kyc/Columns.tsx index 55d3d28..5002030 100644 --- a/src/pages/members/kyc/Columns.tsx +++ b/src/pages/members/kyc/Columns.tsx @@ -70,6 +70,20 @@ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef { + return ( + + ); + } + }, { accessorKey: 'group_name', header: ({ column }) => { From 56883fda1226836321c9e5dde42e513c32ecb599 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 24 Apr 2025 15:32:57 +0700 Subject: [PATCH 18/19] fix nationality --- src/pages/members/manage-members/blocks/DetailMember.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/members/manage-members/blocks/DetailMember.tsx b/src/pages/members/manage-members/blocks/DetailMember.tsx index bfc4d54..a1c4190 100644 --- a/src/pages/members/manage-members/blocks/DetailMember.tsx +++ b/src/pages/members/manage-members/blocks/DetailMember.tsx @@ -70,9 +70,9 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa name === "file_commercial_license") { // FOR FILE ONLY setFormData({ ...formData, [name]: e.target.files[0] }); } else if(name === "nationality") { + setFormData({ ...formData, [name]: value }); let getNationality = await axios.get(`${URL_NATIONALITY}/${value}`); setNationality(getNationality.data.data) - setFormData({ ...formData, [name]: value }); } else { if (name === 'municipio_id' || name === 'posto_adms_id' || name === 'suco_id') await getMasterAfter(name, value); if (name==='msisdn') setFormData({ ...formData, [name]: value.replace(/\D/g, '') }) From bb5db5019d627f6d0ac0eee698c287cacf3dcc6d Mon Sep 17 00:00:00 2001 From: bagusajisaputroo Date: Thu, 24 Apr 2025 16:56:38 +0700 Subject: [PATCH 19/19] feedbackmember+detail+review --- .../feedback-member/FeedbackMember.tsx | 15 +- .../feedback-member/blocks/EditDialog.tsx | 189 +++++++ .../feedback-member/blocks/FeedbackDetail.tsx | 481 +++++++++++------- .../hooks/ManageFeedbackMemberContext.tsx | 142 +++--- .../hooks/useManageFeedbackMemberContext.tsx | 4 +- 5 files changed, 568 insertions(+), 263 deletions(-) create mode 100644 src/pages/members/feedback-member/blocks/EditDialog.tsx 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

+
+ +
+ + +
+
+ +