diff --git a/.woodpecker.yml b/.woodpecker.yml index 949665f..9b44684 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -49,23 +49,35 @@ steps: commands: - npm i typescript -g - npm run build:production - - echo '✅ success compile typescript and ready to deploy' + - echo '✅ success compile typescript' when: branch: - master + depends_on: [setup] - - name: deploy + - name: build-image image: docker commands: - docker builder prune -f - docker container prune -f - docker-compose build - - docker-compose up -d --no-deps --build --remove-orphans volumes: - /var/run/docker.sock:/var/run/docker.sock when: branch: - master + depends_on: [setup, build-src] + + - name: deploy + image: docker + commands: + - docker-compose up -d --no-deps --remove-orphans + volumes: + - /var/run/docker.sock:/var/run/docker.sock + when: + branch: + - master + depends_on: [setup, build-src, build-image] - name: telegram failed notify image: appleboy/drone-telegram @@ -119,3 +131,4 @@ steps: - success branch: - master + depends_on: [setup, build-src, build-image, deploy] diff --git a/public/media/images/fixed-card.png b/public/media/images/fixed-card.png new file mode 100644 index 0000000..93f612a Binary files /dev/null and b/public/media/images/fixed-card.png differ diff --git a/src/components/confirm.tsx b/src/components/confirm.tsx index 01591c5..3fb9cff 100644 --- a/src/components/confirm.tsx +++ b/src/components/confirm.tsx @@ -21,8 +21,8 @@ const ConfirmDialog = ({ open, onClose, title, content, onYes, onNo, onOpenChang - No - onClickYes()}>Yes + No + onClickYes()}>Yes diff --git a/src/pages/dashboards/home/blocks/TransactionValue.tsx b/src/pages/dashboards/home/blocks/TransactionValue.tsx index 97f1fc8..d88151e 100644 --- a/src/pages/dashboards/home/blocks/TransactionValue.tsx +++ b/src/pages/dashboards/home/blocks/TransactionValue.tsx @@ -79,29 +79,29 @@ const TransactionValue = ({ startdate, enddate }: Props) => { case "M": type = "Withdrawal Agent"; break; - case 'O': - type = 'TOP UP AGENT'; + case 'O': + type = 'Top Up Agent'; break; - case 'S': - type = 'TRANSFER P24'; + case 'S': + type = 'Transfer P24'; break; - case 'I': - type = 'WIJTDRAW MERCHANT'; + case 'I': + type = 'Withdraw Merchant'; break; - case 'D': - type = 'DONATION'; + case 'D': + type = 'Donation'; break; - case 'F': - type = 'FEE'; + case 'F': + type = 'Fee'; break; - case 'V': - type = 'REVERSAL'; + case 'V': + type = 'Reversal'; break; - case 'C': - type = 'CASHBACK CASH'; + case 'C': + type = 'Cashback Cash'; break; - case 'H': - type = 'CASHBACK POINT'; + case 'H': + type = 'Cashback Point'; break; default: type = "Unknown"; @@ -136,7 +136,8 @@ const TransactionValue = ({ startdate, enddate }: Props) => { > - {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit} + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(item.value)}{item.unit} ))} diff --git a/src/pages/members/agent-balance/AgentBalance.tsx b/src/pages/members/agent-balance/AgentBalance.tsx index df4ac80..4a1282b 100644 --- a/src/pages/members/agent-balance/AgentBalance.tsx +++ b/src/pages/members/agent-balance/AgentBalance.tsx @@ -44,42 +44,44 @@ const AgentBalance = () => { const [dataBalance, setDataBalance] = useState([]); const [formData, setFormData] = useState(initBalance); const [pageIndex, setPageIndex] = useState(1); + const [currentPage, setCurrentPage] = useState(0); const [pageSize, setPageSize] = useState(10); const [dialogType, setDialogType] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const [isReloading, setIsReloading] = useState(false); const [barcode, setBarcode] = useState(''); + const [token_legacy, setToken_legacy] = useState(''); // const [selectedMember, setSelectedMember] = useState(initBalance); useEffect(() => { - fetchAgentBalance(); + // fetchAgentBalance(); }, []); - async function fetchAgentBalance(): Promise { - setIsReloading(true); - try { - let balances = await axios.get(`${BASE_URL}/customer/agent-balance`, { - params: { - limit: 20, - page: 1, - with_deleted: false, - order_field: 'created_at', - order_direction: 'DESC' - } - }); - let temp = 1; - let resBalance = balances.data.data.list.map((el: any) => { - el.no = temp++; - if (!el.agent_name) el.agent_name = '' - return el; - }); - setDataBalance(resBalance); - } catch (error: any) { - alert(error.message); - console.log(error); - } finally { - setIsReloading(false); - } + const fetchAgentBalance = async ( + page: number, + limit: number, + sorting: any, + filter: any + ) => { + filter = filter.length ? filter : {}; + let balances = await axios.get(`${BASE_URL}/customer/agent-balance`, { + params: { + limit: limit, + page: page + 1, // +1 because your API uses 1-based pages + with_deleted: false, + order_field: 'created_at', + order_direction: 'DESC', + filter: JSON.stringify(filter), + specialFilter: true + } + }); + let temp = 1; + let resBalance = balances.data.data.list.map((el: any) => { + el.no = temp++; + if (!el.agent_name) el.agent_name = '' + return el; + }); + return { data: resBalance, totalCount: balances?.data.data.total_count } } const openDialog = () => setIsDialogOpen(true); @@ -95,27 +97,30 @@ const AgentBalance = () => { const handleUpdate = async (data: any) => { await getQrCode(data.id); - setFormData(data) + setFormData(data); setDialogType('update'); setIsDialogOpen(true); + let getTokenLegacy = await axios.get(`${BASE_URL}/token/get-token-legacy`, { params: { id: data.id } }); + let temp = getTokenLegacy?.data?.data?.token_legacy || ''; + setToken_legacy(temp) }; - const handleReload = () => { + const handleReload = async () => { setIsReloading(true); - fetchAgentBalance(); + await fetchAgentBalance(currentPage, pageSize, '',''); + closeDialog() + setIsReloading(false); + // fetchAgentBalance(); }; - async function getQrCode(customerId:any): Promise { + async function getQrCode(customerId: any): Promise { try { - setBarcode(``) - let getToken = await axios.get(`${BASE_URL}/token/get`, { params: { id: customerId }}); - console.log(getToken.data.data); + setBarcode(``); + let getToken = await axios.get(`${BASE_URL}/token/get`, { params: { id: customerId } }); setBarcode(getToken.data.data.qrCode); } catch (error: any) { - toast.warning(error.message) + toast.warning(error.message); console.log(error); - } finally { - // setBarcode(`https://miro.medium.com/v2/resize:fit:250/1*DcceOyLkzU--EzP4PYKd_g.png`) } } @@ -154,31 +159,42 @@ const AgentBalance = () => { {isReloading && ( )} - - } - layout={{ card: true }} - serverSide={false} - onRowSelectionChange={(selected, table: any) => { - const selectedRow = table.getSelectedRowModel().rows[0]; - if (selectedRow) handleUpdate(selectedRow.original); - }} - /> + {!isReloading && ( + fetchAgentBalance(pageIndex, pageSize, sorting, columnFilters) + } + toolbar={ + + } + onRowSelectionChange={(selected, table: any) => { + const selectedRow = table.getSelectedRowModel().rows[0]; + if (selectedRow) handleUpdate(selectedRow.original); + }} + /> + )} {/* */} - + @@ -188,20 +204,19 @@ const AgentBalance = () => { }; // ==================== -const PaymentCard = ({ barcode, agent_name, customerId }:any) => { - const [token_legacy, setToken_legacy] = useState(''); - const cardRef:any = useRef(null); +const PaymentCard = ({ barcode, agent_name, customerId, token_legacy, setToken_legacy, handleReload }: any) => { + const cardRef: any = useRef(null); const waitForImageLoad = (img: HTMLImageElement): Promise => { return new Promise((resolve) => { if (img.complete && img.naturalHeight !== 0) { resolve(); } else { - toast.warning(`No QR Code`) + toast.warning(`No QR Code`); img.onload = () => resolve(); } }); - }; + }; const handleDownload = async () => { if (!cardRef.current) return; @@ -210,14 +225,14 @@ const PaymentCard = ({ barcode, agent_name, customerId }:any) => { if (img) { await waitForImageLoad(img); } else { - return toast.warning(`No QR Code`) + return toast.warning(`No QR Code`); } const canvas = await html2canvas(cardRef.current, { useCORS: true, allowTaint: false, backgroundColor: 'white', - scale: 2, + scale: 2 }); const dataUrl = canvas.toDataURL('image/png'); @@ -229,32 +244,51 @@ const PaymentCard = ({ barcode, agent_name, customerId }:any) => { const updateTokenLegacy = async () => { try { - if (!token_legacy) toast.warning(`Token Legacy can not be empty`) - await axios.post(`${BASE_URL}/token/insert-token-legacy`, { customerid: customerId, token_legacy }); - toast.success(`Success Update Token Legacy`) - } catch (error:any) { - toast.error(error.message) + if (!token_legacy) return toast.warning(`Token Legacy can not be empty`); + await axios.post(`${BASE_URL}/token/insert-token-legacy`, { + customerid: customerId, + token_legacy + }); + toast.success(`Success Update Token Legacy`); + await handleReload(); + } catch (error: any) { + let err_msg = error?.response?.data?.error || error.message + toast.error(err_msg); } - } + }; return ( - {/* Card content to render */} - - - - {agent_name} - + + + + + {agent_name} + {barcode && ( - + + + )} + + + + + + Imprime husi : Telin Digital Solution + Imprime : 01–06–2025 + + - + @@ -264,17 +298,22 @@ const PaymentCard = ({ barcode, agent_name, customerId }:any) => { className="input" name="token_legacy" value={token_legacy} - onChange={(e:any) => setToken_legacy(e.target.value)} + onChange={(e: any) => setToken_legacy(e.target.value)} /> - {/* Download Button */} - + Print QR - + Update to Token Legacy diff --git a/src/pages/members/agent-balance/Column.tsx b/src/pages/members/agent-balance/Column.tsx index d58c615..49f9fcf 100644 --- a/src/pages/members/agent-balance/Column.tsx +++ b/src/pages/members/agent-balance/Column.tsx @@ -101,6 +101,20 @@ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef { + return ( + column.toggleSorting(column.getIsSorted() === 'asc')} + > + Deposit + + + ); + } + }, { accessorKey: 'balance_merchant', header: ({ column }) => { @@ -129,20 +143,6 @@ export const getColumns = (handleUpdate: (data: any) => void): ColumnDef { - return ( - column.toggleSorting(column.getIsSorted() === 'asc')} - > - Deposit - - - ); - } - }, { id: 'actions', cell: ({ row }) => { diff --git a/src/pages/members/agent-balance/ListToolbar.tsx b/src/pages/members/agent-balance/ListToolbar.tsx index 5e9f421..d066840 100644 --- a/src/pages/members/agent-balance/ListToolbar.tsx +++ b/src/pages/members/agent-balance/ListToolbar.tsx @@ -9,58 +9,64 @@ interface ListToolBarProps { } const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) => { - const { table } = useDataGrid(); + const { table, reload } = useDataGrid(); const [usernameFilter, setUsernameFilter] = useState(''); const [msisdn, setMsisdn] = useState(''); + const [typeSearchValue, setSearchTypeValue] = useState(); + const [searchValue, setSearchValue] = useState(); const [fullname, setFullname] = useState(''); const [merchantName, setMerchantName] = useState(''); const handleUsernameChange = (e: React.ChangeEvent) => { setUsernameFilter(e.target.value); - table.getColumn('username')?.setFilterValue(e.target.value); }; const filtermsisdn = (e: React.ChangeEvent) => { setMsisdn(e.target.value); - table.getColumn('msisdn')?.setFilterValue(e.target.value); }; const filterfullname = (e: React.ChangeEvent) => { setFullname(e.target.value); - table.getColumn('fullname')?.setFilterValue(e.target.value); }; const filtermerchantname = (e: React.ChangeEvent) => { setMerchantName(e.target.value); - table.getColumn('agent_name')?.setFilterValue(e.target.value); + }; + + const handleSearch = (e: React.ChangeEvent) => { + e.preventDefault() + table.resetPageIndex() + table.getColumn('username')?.setFilterValue(usernameFilter); + table.getColumn('msisdn')?.setFilterValue(msisdn); + table.getColumn('fullname')?.setFilterValue(fullname); + table.getColumn('agent_name')?.setFilterValue(merchantName); + // if (!merchantName && !fullname && !msisdn && !usernameFilter) reload() }; return ( - - - - - - - - - - - - - - - - - - + + handleSearch(e)}> + + + + + + + + + + + + + + + + + - {/* - Add Data - */} {isReloading ? ( diff --git a/src/pages/members/kyc/Kyc.tsx b/src/pages/members/kyc/Kyc.tsx index 49a2514..9dd9e95 100644 --- a/src/pages/members/kyc/Kyc.tsx +++ b/src/pages/members/kyc/Kyc.tsx @@ -63,7 +63,7 @@ const Kyc = () => { setMembers(resMembers); let getProfession: any = await axios.get(`${BASE_URL_MASTER_DATA}/profession/list`, { params: { - limit: 50, + limit: 75, page: 1, with_deleted: false, order_field: 'name', diff --git a/src/pages/members/manage-members/ManageMembers.tsx b/src/pages/members/manage-members/ManageMembers.tsx index 0e58214..931ba81 100644 --- a/src/pages/members/manage-members/ManageMembers.tsx +++ b/src/pages/members/manage-members/ManageMembers.tsx @@ -42,79 +42,6 @@ const ManageMembers = () => { fetchMasters(); }, []); - // async function fetchCustomers(): Promise { - // setIsReloading(true); - // try { - // let customers = await axios.get(`${BASE_URL}/customer/list`, { - // params: { - // limit: pageSize, - // page: 1 + currentPage, - // with_deleted: false, - // order_field: 'created_at', - // order_direction: 'DESC' - // } - // }); - // let temp = 1; - // let resMembers = customers.data.data.list.map((el: any) => { - // el.no = temp++; - // if (el.date_birth) { - // const d = new Date(el.date_birth); - // el.date_birth = d.toLocaleString('sv-SE'); - // } - // el.name = el.fullname; - // return el; - // }); - // setMembers(resMembers); - // return { - // data: resMembers, - // totalCount: customers.data.data.total_count, - // } - // } catch (error: any) { - // toast.error(error.message); - // console.log(error); - // } finally { - // setIsReloading(false); - // } - // } - - // const fetchCustomers = useCallback( - // async ({ pageIndex, pageSize }:any) => { - // setIsReloading(true); - // try { - // const res = await axios.get(`${BASE_URL}/customer/list`, { - // params: { - // limit: pageSize, - // page: pageIndex + 1, // +1 because your API uses 1-based pages - // with_deleted: false, - // order_field: 'created_at', - // order_direction: 'DESC', - // }, - // }); - - // const customers = res.data.data.list.map((el: any, index: number) => ({ - // ...el, - // no: pageIndex * pageSize + index + 1, - // date_birth: el.date_birth - // ? new Date(el.date_birth).toLocaleString('sv-SE') - // : null, - // name: el.fullname, - // })); - - // return { - // data: customers, - // totalCount: res.data.data.total_count, - // }; - // } catch (error: any) { - // toast.error(error.message); - // console.error(error); - // return { data: [], totalCount: 0 }; - // } finally { - // setIsReloading(false); - // } - // }, - // [] - // ); - const fetchCustomers = async ( page: number, limit: number, @@ -138,10 +65,8 @@ const ManageMembers = () => { const customers = res.data.data.list.map((el: any, index: number) => ({ ...el, - no: page * pageSize + index + 1, - date_birth: el.date_birth - ? new Date(el.date_birth).toLocaleString('sv-SE') - : null, + no: (page) * limit + index + 1, + date_birth: el.date_birth ? new Date(el.date_birth).toLocaleString('sv-SE') : null, name: el.fullname, })); return { data: customers, totalCount: res?.data.data.total_count }; diff --git a/src/pages/members/manage-members/blocks/AdmAccess.tsx b/src/pages/members/manage-members/blocks/AdmAccess.tsx index 15ace75..bc3da46 100644 --- a/src/pages/members/manage-members/blocks/AdmAccess.tsx +++ b/src/pages/members/manage-members/blocks/AdmAccess.tsx @@ -112,11 +112,11 @@ export default function AdmAccess({page,groups,formData,handleClose,fetchCustome Pin Status: {getPinStatus(formData.status).msg} - buttonStatus(e)}>{getPinStatus(formData.status).btn} + buttonStatus(e)}>{getPinStatus(formData.status).btn} Reset PIN - buttonResetPin(e)}>Reset PIN + buttonResetPin(e)}>Reset PIN @@ -124,11 +124,11 @@ export default function AdmAccess({page,groups,formData,handleClose,fetchCustome Change Group - openChangeGroupDialog(e)}>Change Group + openChangeGroupDialog(e)}>Change Group Edit Member - setViewOnly(!viewOnly)}>{viewOnly ? 'Open Edit' : 'Close Edit'} + setViewOnly(!viewOnly)}>{viewOnly ? 'Open Edit' : 'Close Edit'} diff --git a/src/pages/members/manage-members/blocks/CustomerWallet.tsx b/src/pages/members/manage-members/blocks/CustomerWallet.tsx index 3522cb7..f94e6e2 100644 --- a/src/pages/members/manage-members/blocks/CustomerWallet.tsx +++ b/src/pages/members/manage-members/blocks/CustomerWallet.tsx @@ -45,7 +45,8 @@ export default function CustomerWallet({customerid}: any) { {customerWallet.map((item:any, index) => ( !formData[key]); + + if (missingFields.length > 0) { + const missingNames = missingFields.map((key:any) => requiredFields[key]).join(", "); + return toast.warning(`Required fields cannot be empty: ${missingNames}!`); } - if (dialogType === 'create') { - if (!formData.msisdn||!formData.email||!formData.fullname||!formData.username||!formData.mother_fullname||!formData.address||!formData.nationality||!formData.date_birth||!formData.gender) { - return toast.warning(`Required fields cannot be empty: msisdn, email, fullname, username, mother Full Name, Address, Nationality, Date Of Birth, Gender.!`) - } - } - // if (page === 'kyc' && !formData.description) return toast.warning(`Description for approval needed!`) + // dialogType === 'update' handleSubmit(formData); } @@ -208,26 +213,21 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa - {alert.show && ( - - {alert.message} - - )} + + {alert.show && ({alert.message})} {/* */} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, groups, 'group_id', 'Group', true, dialogType === "update"||false): ''} - {/* {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Group', 'group_name', 'text', true, true): ''} */} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Username', 'username', 'text', true, viewOnly): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Full Name', 'fullname', 'text', true, viewOnly): ''} {(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'Photo', 'photouser', previewImg, setPreviewImg): ''} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, genders, 'gender', 'Gender', true, false): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Date of Birth', 'date_birth', 'date', true, viewOnly): ''} - {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Phone Number', 'msisdn', 'text', true, dialogType === "create"?false:true): ''} + {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Phone Number', 'msisdn', 'text', true, (dialogType === "create"||formData.group_name === 'Agent')?false:true): ''} {(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") ? generateInput(formData, handleChange, 'Mother Fullname', 'mother_fullname', 'text', false, viewOnly): ''} {(formData.id || dialogType === "create") ? generateList(formData, handleChange, identity_type, 'identity_type', 'Identity Type', 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): ''} @@ -286,20 +286,20 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa ) : ""} - btnPrevDef(e)} color="secondary">{ viewOnly ? (`Open Edit`) : (`Close Edit`) } + btnPrevDef(e)} color="secondary">{ viewOnly ? (`Open Edit`) : (`Close Edit`) } handleAddDialog(false)}>Cancel {/* buttonOnSubmit(e, formData)} variant="default">Save Changes */} { formData.isneedapproval == 1 && page === 'kyc' ? ( - Reject + Reject ) : ('') } - buttonOnSubmit(e)} color="primary" variant="default" disabled={isAdmin}> + buttonOnSubmit(e)} color="primary" variant="default" disabled={isAdmin}> { formData.id ? (formData.isneedapproval == 1 && page === 'kyc' ? (`Edit & Approve`) : (`Edit`)) : ("Create") } - + diff --git a/src/pages/members/manage-members/blocks/ListToolBar.tsx b/src/pages/members/manage-members/blocks/ListToolBar.tsx index b9d2837..6cdaa26 100644 --- a/src/pages/members/manage-members/blocks/ListToolBar.tsx +++ b/src/pages/members/manage-members/blocks/ListToolBar.tsx @@ -41,10 +41,9 @@ const ListToolbar = ({ createMember, onReload, isReloading, groups }: ListToolba if (userloginrole === 'Admin') setisAdmin(true); } - const handleSearch = (e: React.ChangeEvent) => { e.preventDefault() - // setSearchValue(e.target.value) + table.resetPageIndex() if (typeSearchValue === 'username') { table.getColumn('username')?.setFilterValue(searchValue); table.getColumn('msisdn')?.setFilterValue(''); diff --git a/src/pages/members/search-member/SearchMember.tsx b/src/pages/members/search-member/SearchMember.tsx new file mode 100644 index 0000000..83e951d --- /dev/null +++ b/src/pages/members/search-member/SearchMember.tsx @@ -0,0 +1,47 @@ +import { Container, DataGridInner } from '@/components'; +// import { +// ManageTransferTypeContext, +// ManageTransferTypeContextProvider +// } from './hooks/ManageTransferTypeContext'; +// import AddDialog from './blocks/AddDialog'; +// import { Breadcrumbs, Link } from '@mui/material'; +// import DeleteDialog from './blocks/DeleteDialog'; +// import { EditDialog } from './blocks/EditDialog'; +import { Helmet } from 'react-helmet'; +import { ManageSearchMemberContextProvider } from './hooks/ManageSearchMember'; +import { Breadcrumbs, Link } from '@mui/material'; + +const SearchMember = () => { + + return ( + <> + + TPAY | Manage Search Member + + + + Search Member + + + Dashboard + + + + Members + + + + Manage Search Member + + + + + + + + + > + ); +}; + +export default SearchMember; diff --git a/src/pages/members/search-member/blocks/DetailMember.tsx b/src/pages/members/search-member/blocks/DetailMember.tsx new file mode 100644 index 0000000..b87ec87 --- /dev/null +++ b/src/pages/members/search-member/blocks/DetailMember.tsx @@ -0,0 +1,390 @@ +import { useState, useContext, useRef } from 'react'; +import axios from 'axios'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogBody +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import ConfirmDialog from '@/components/confirm'; +import { apiConfig } from '@/config/api.config'; +import { useManageSearchMemberContext } from '../hooks/useManageSearchMemberContext'; + +const BASE_URL_CUSTOMER = apiConfig.service_customer; + +const DetailMember = () => { + const { + showEditDialog, + handleEditDialog, + selectedCustomerData, + doGetSearchMemberListData, + refreshTable + } = useManageSearchMemberContext(); + + const [confirmDialog, setConfirmDialog] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const [previewImg, setPreviewImg] = useState({ + status: false, + img: '' + }); + const parentRef = useRef(null); + + const formatDate = (dateString: string) => { + if (!dateString) return 'No Data'; + try { + return new Date(dateString).toLocaleDateString('en-GB'); + } catch { + return 'Invalid Date'; + } + }; + + const getPinStatus = (status: string) => { + if (status === 'Y') + return { + msg: 'Active', + btn: 'Block PIN', + canBlock: true, + canUnblock: false, + variant: 'destructive' as const, + showField: false + }; + if (status === 'N') + return { + msg: 'Not Active', + btn: 'PIN Not Active', + canBlock: false, + canUnblock: false, + variant: 'secondary' as const, + showField: true + }; + if (status === 'P') + return { + msg: 'Suspend PIN', + btn: 'Unblock PIN', + canBlock: false, + canUnblock: true, + variant: 'default' as const, + showField: true + }; + if (status === 'O') + return { + msg: 'Suspend OTP', + btn: 'Unblock OTP', + canBlock: false, + canUnblock: true, + variant: 'default' as const, + showField: true + }; + return { + msg: 'Unknown Status', + btn: 'Unknown', + canBlock: false, + canUnblock: false, + variant: 'secondary' as const, + showField: true + }; + }; + + const handleUnblockPin = async () => { + if (!selectedCustomerData?.id) { + toast.error('Customer data not available'); + return; + } + + try { + setIsUpdating(true); + await axios.put(`${BASE_URL_CUSTOMER}/customer/statuspin`, { + customerid: selectedCustomerData.id, + status: 'UnBlock' + }); + + toast.success('PIN successfully unblocked'); + + handleEditDialog(false, null); + + setTimeout(() => { + refreshTable(); + }, 500); + + } catch (error: any) { + toast.error(error.message || 'Failed to unblock PIN'); + } finally { + setIsUpdating(false); + setConfirmDialog(false); + } + }; + + const handleAddDialog = (show: boolean) => { + handleEditDialog(show, show ? selectedCustomerData : null); + }; + + const imagePreview = (file: any) => { + if (file && typeof file === 'object' && file.type && file.type.startsWith('image/')) { + const previewURL = URL.createObjectURL(file); + return previewURL; + } + return file; + }; + + const handlePrevImg = (par: boolean) => { + if (par) setPreviewImg({ status: true, img: '' }); + if (!par) setPreviewImg({ status: false, img: '' }); + }; + + const onClickImg = (img: any) => { + setPreviewImg({ status: true, img }); + }; + + const generateInput = (label: string, name: string, value: any, type: string = 'text', required: boolean = false, disabled: boolean = true) => { + const displayValue = value || ''; + + function generateDate(isoString: string) { + return isoString.slice(0, 10); + } + + return ( + + + + {label}{required ? "*" : ""} + + {}} + /> + + + ); + }; + + const generateImage = (label: string, name: string, photoUrl: any) => { + return ( + <> + {previewImg.status && ( + + )} + + + {label} + + + {photoUrl ? ( + + onClickImg(imagePreview(photoUrl))} + /> + + ) : ( + No Data + )} + + + > + ); + }; + + const generatePinStatusField = (label: string, status: string) => { + const pinStatus = getPinStatus(status); + + if (!pinStatus.showField) { + return null; + } + + return ( + + + + {label} + + + {pinStatus.msg} + {pinStatus.canUnblock && ( + setConfirmDialog(true)} + disabled={isUpdating} + > + {isUpdating ? 'Unblocking...' : pinStatus.btn} + + )} + {!pinStatus.canUnblock && !pinStatus.canBlock && ( + + {pinStatus.btn} + + )} + + + + ); + }; + + if (!selectedCustomerData) { + return ( + handleAddDialog(open)}> + + + Member - Detail + + + + No data available + + + + ); + } + + return ( + <> + handleAddDialog(open)}> + + + Member - Detail + + + + + + + + {/* Basic Information */} + {generateInput('Group', 'group_name', selectedCustomerData.group_name || selectedCustomerData.destinationGroup, 'text', true)} + {generateInput('Username', 'username', selectedCustomerData.username, 'text', true)} + {generateInput('Full Name', 'fullname', selectedCustomerData.fullname, 'text', true)} + {generateInput('Gender', 'gender', selectedCustomerData.gender, 'text', true)} + {generateInput('Date of Birth', 'date_birth', selectedCustomerData.date_birth, 'date', true)} + {generateInput('Phone Number', 'msisdn', selectedCustomerData.msisdn, 'text', true)} + {generateInput('Email', 'email', selectedCustomerData.email, 'text', true)} + {generateInput('Mother Fullname', 'mother_fullname', selectedCustomerData.mother_fullname, 'text', true)} + {generateInput('Identity Type', 'identity_type', selectedCustomerData.identity_type)} + {generateInput('Identity Number', 'identity_number', selectedCustomerData.identity_number)} + {generateInput('Address', 'address', selectedCustomerData.address, 'text', true)} + + {/* Location Information */} + {generateInput('Profession', 'profession', selectedCustomerData.profession)} + {generateInput('Municipio', 'municipio_name', selectedCustomerData.municipio_name)} + {generateInput('Posto', 'posto_adms_name', selectedCustomerData.posto_adms_name)} + {generateInput('Suco', 'suco_name', selectedCustomerData.suco_name)} + {generateInput('Aldeia', 'aldeia_name', selectedCustomerData.aldeia_name)} + {generateInput('Nationality', 'nationality', selectedCustomerData.nationality, 'text', true)} + {generateImage('Photo', 'photouser', selectedCustomerData.photouser)} + + + + + + {/* Bank Information Section */} + + Bank Information + + {generateInput('Bank Name', 'bank_name', selectedCustomerData.bank_name)} + {generateInput('Bank Account', 'bank_account', selectedCustomerData.bank_account)} + {generateInput('IBank Number', 'ibank_number', selectedCustomerData.ibank_number)} + + + + {/* Agent Information Section */} + + Agent Information + + {generateInput('License Number', 'license_number', selectedCustomerData.license_number)} + {generateInput('Merchant Name', 'agent_name', selectedCustomerData.agent_name)} + {generateInput('Merchant Address', 'merchantaddress', selectedCustomerData.merchantaddress)} + {generateInput('Longitude Merchant', 'longitudemerchant', selectedCustomerData.longitudemerchant)} + {generateInput('Latitude Merchant', 'latitudemerchant', selectedCustomerData.latitudemerchant)} + {generateImage('Photo Merchant', 'photomerchant', selectedCustomerData.photomerchant)} + {generateImage('File Selfie', 'file_selfie', selectedCustomerData.file_selfie)} + {generateImage('File Document', 'file_document_id', selectedCustomerData.file_document_id)} + {generateImage('File Document & Selfie', 'file_document_id_selfie', selectedCustomerData.file_document_id_selfie)} + {generateImage('File Commercial License', 'file_commercial_license', selectedCustomerData.file_commercial_license)} + + + + {/* Status Information Section */} + + Status Information + + {generatePinStatusField('PIN Status', selectedCustomerData.status || '')} + {generateInput('Created Date', 'created_at', formatDate(selectedCustomerData.created_at))} + + + + {/* Action Buttons */} + + handleAddDialog(false)}> + Close + + + + + + + + + setConfirmDialog(false)} + /> + > + ); +}; + +function PreviewImage({previewImgDialog, handlePreviewImgDialog, modalImage}: any) { + return ( + handlePreviewImgDialog(open)}> + + + + handlePreviewImgDialog(false)} + className="absolute top-2 right-2 text-white bg-red-600 hover:bg-red-800 rounded-full px-3 py-1 text-xl font-bold" + > + × + + + + + + ); +} + +export default DetailMember; \ No newline at end of file diff --git a/src/pages/members/search-member/blocks/FilterCard.tsx b/src/pages/members/search-member/blocks/FilterCard.tsx new file mode 100644 index 0000000..f635ee1 --- /dev/null +++ b/src/pages/members/search-member/blocks/FilterCard.tsx @@ -0,0 +1,103 @@ +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useState } from 'react'; + +const CustomerFilterForm = ({ + onFilter, + onReset +}: { + onFilter: (filters: { username?: string; msisdn?: string }) => void; + onReset?: () => void; +}) => { + const [searchType, setSearchType] = useState<'username' | 'msisdn'>('username'); + const [searchValue, setSearchValue] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!searchValue.trim()) { + alert('Please fill the search field'); + return; + } + + const filters = searchType === 'username' + ? { username: searchValue, msisdn: '' } + : { username: '', msisdn: searchValue }; + + onFilter(filters); + }; + + const handleReset = () => { + setSearchType('username'); + setSearchValue(''); + if (onReset) { + onReset(); + } + }; + + const getPlaceholder = () => { + return searchType === 'username' ? 'Enter username' : 'Enter phone number'; + }; + + const getLabel = () => { + return searchType === 'username' ? 'Username' : 'Phone Number'; + }; + + return ( + + + + + + + Type Search + + setSearchType(value)}> + + + + + Username + Phone Number + + + + + + + Search + + setSearchValue(e.target.value)} + placeholder={getPlaceholder()} + className="w-full h-12 text-base" + /> + + + + + Submit + + + Reset + + + + + + + ); +}; + +export default CustomerFilterForm; \ No newline at end of file diff --git a/src/pages/members/search-member/hooks/ManageSearchMember.tsx b/src/pages/members/search-member/hooks/ManageSearchMember.tsx new file mode 100644 index 0000000..56f18e6 --- /dev/null +++ b/src/pages/members/search-member/hooks/ManageSearchMember.tsx @@ -0,0 +1,338 @@ +import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components'; +import { Toaster } from '@/components/ui/sonner'; +import { Button } from '@/components/ui/button'; +import { apiConfig } from '@/config/api.config'; +import { useCallApi } from '@/hooks'; +import { ColumnDef } from '@tanstack/react-table'; +import { createContext, useCallback, useMemo, useState } from 'react'; +import CustomerDetailDialog from '../blocks/DetailMember'; +import CustomerFilterForm from '../blocks/FilterCard'; + +interface AccountProps { + id: string; + name: string; +} + +interface CustomerData { + group_id: string; + group_name: string; + municipio_id: string; + municipio_name: string; + posto_adms_id: string; + posto_adms_name: string; + suco_id: string; + suco_name: string; + aldeia_id: string; + aldeia_name: string; + id: string; + msisdn: string; + email: string; + fullname: string; + username: string; + mother_fullname: string; + agent_name: string; + bank_name: string; + bank_account: string; + ibank_number: string; + address: string; + longitude: string; + latitude: string; + nationality: string; + file_selfie: string; + file_document_id: string; + file_document_id_selfie: string; + file_commercial_license: string; + identity_number: string; + merchantaddress: string; + license_number: string; + date_birth: string; + gender: string; + status: string; + created_at: string; + isneedapproval: string; + identity_type: string; + longitudemerchant: string; + latitudemerchant: string; + profession: string; + created_by: string; + approval_description_premium: string; + approval_description_agent: string; + photouser: string; + photomerchant: string; + destinationGroup: string; + statusApproval: string; +} + +interface ContextProps { + showEditDialog: boolean; + handleEditDialog: (show: boolean, selectedData: CustomerData | null) => void; + showAddDialog: boolean; + handleAddDialog: (show: boolean) => void; + selectedCustomerData: CustomerData | null; + doGetSearchMemberListData: ( + page: number, + limit: number, + sorting: any, + filter: any + ) => Promise<{ data: any[]; totalCount: number }>; + showTable: boolean; + showFilterCard: boolean; + currentFilters: any; + handleCustomFilter: (filters: { username?: string; msisdn?: string }) => void; + handleResetFilter: () => void; + handleShowSearchAgain: () => void; + refreshTable: () => void; +} + +const initialProps: ContextProps = { + showEditDialog: false, + handleEditDialog: () => {}, + showAddDialog: false, + handleAddDialog: () => {}, + selectedCustomerData: null, + doGetSearchMemberListData: async () => ({ data: [], totalCount: 0 }), + showTable: false, + showFilterCard: true, + currentFilters: {}, + handleCustomFilter: () => {}, + handleResetFilter: () => {}, + handleShowSearchAgain: () => {}, + refreshTable: () => {} +}; + +const ManageSearchMemberContext = createContext(initialProps); +const API_URL = apiConfig.service_customer; + +const ManageSearchMemberContextProvider = ({ children }: { children: React.ReactNode }) => { + const [showEditDialog, setShowEditDialog] = useState(false); + const [showAddDialog, setShowAddDialog] = useState(false); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [selectedCustomerData, setSelectedCustomerData] = useState(null); + const { GetData } = useCallApi(); + + const [showTable, setShowTable] = useState(false); + const [showFilterCard, setShowFilterCard] = useState(true); + const [currentFilters, setCurrentFilters] = useState({}); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + const handleCustomFilter = useCallback((filters: { username?: string; msisdn?: string }) => { + const formattedFilters: any[] = []; + + if (filters.username?.trim()) { + formattedFilters.push({ id: "username", value: filters.username.trim() }); + } + + if (filters.msisdn?.trim()) { + formattedFilters.push({ id: "msisdn", value: filters.msisdn.trim() }); + } + + setCurrentFilters(formattedFilters); + setShowTable(true); + setShowFilterCard(false); + }, []); + + const handleResetFilter = useCallback(() => { + setCurrentFilters({}); + setShowTable(false); + setShowFilterCard(true); + }, []); + + const handleShowSearchAgain = useCallback(() => { + setShowFilterCard(true); + setShowTable(false); + }, []); + + const refreshTable = useCallback(() => { + setRefreshTrigger(prev => prev + 1); + }, []); + + const handleEditDialog = useCallback((show: boolean, selectedData: CustomerData | null) => { + setSelectedCustomerData(show ? selectedData : null); + setShowEditDialog(show); + }, []); + + const handleAddDialog = useCallback((show: boolean) => { + setShowAddDialog(show); + }, []); + + const doGetSearchMemberListData = useCallback( + async (page: any, limit: any, sorting: any, filter: any) => { + try { + const finalSorting = sorting.length === 0 ? [{ id: 'name', desc: true }] : sorting; + + const filterToUse = Object.keys(currentFilters).length > 0 ? currentFilters : (filter.length ? filter : []); + + const response = await GetData(`${API_URL}/customer/list`, { + limit: limit, + page: page + 1, + with_deleted: false, + order_field: 'username', + order_direction: 'DESC', + filter: JSON.stringify(filterToUse), + specialFilter: true + }); + + return { + data: response?.data?.list || [], + totalCount: response?.data?.total_count || 0 + }; + } catch (error) { + console.error('Fetch error:', error); + return { data: [], totalCount: 0 }; + } + }, + [GetData, currentFilters] + ); + + const columns = useMemo[]>( + () => [ + { + accessorFn: (row) => row.username, + id: 'username', + header: ({ column }) => , + enableSorting: true, + enableHiding: false + }, + { + accessorFn: (row) => row.fullname, + id: 'fullname', + header: ({ column }) => , + enableSorting: true, + enableHiding: false + }, + { + accessorFn: (row) => row.msisdn, + id: 'phone_number', + header: ({ column }) => , + enableSorting: true + }, + { + accessorFn: (row) => row.destinationGroup, + id: 'group', + header: ({ column }) => , + enableSorting: true + }, + { + accessorFn: (row) => row.email, + id: 'email', + header: ({ column }) => , + enableSorting: true + }, + { + accessorFn: (row) => new Date(row.created_at).toLocaleString(), + id: 'created_at', + header: ({ column }) => , + enableSorting: true + }, + { + id: 'actions', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + cell: (data) => { + const row = data.row.original; + return ( + <> + handleEditDialog(true, row)} + > + + + > + ); + }, + meta: { headerClassName: 'w-[100px]', cellClassName: 'text-center' } + } + ], + [handleEditDialog] + ); + + const defaultSorting = useMemo(() => [{ id: 'name', desc: true }], []); + + const contextValue = useMemo( + () => ({ + showEditDialog, + handleEditDialog, + showAddDialog, + handleAddDialog, + selectedCustomerData, + doGetSearchMemberListData, + showTable, + showFilterCard, + currentFilters, + handleCustomFilter, + handleResetFilter, + handleShowSearchAgain, + refreshTable + }), + [ + showEditDialog, + handleEditDialog, + showAddDialog, + handleAddDialog, + selectedCustomerData, + doGetSearchMemberListData, + showTable, + showFilterCard, + currentFilters, + handleCustomFilter, + handleResetFilter, + handleShowSearchAgain, + refreshTable + ] + ); + + return ( + + + + {showFilterCard && ( + + + Search Members + + + + )} + + {/* Table with Reset Button - Only visible after filter is applied */} + {showTable && ( + + {/* Reset Button above table */} + + Search Results + + Search Again + + + + + + doGetSearchMemberListData(pageIndex, pageSize, sorting, columnFilters) + } + > + {children} + + + + )} + + + + + ); +}; + +export {ManageSearchMemberContext, ManageSearchMemberContextProvider }; \ No newline at end of file diff --git a/src/pages/members/search-member/hooks/useManageSearchMemberContext.tsx b/src/pages/members/search-member/hooks/useManageSearchMemberContext.tsx new file mode 100644 index 0000000..042c29a --- /dev/null +++ b/src/pages/members/search-member/hooks/useManageSearchMemberContext.tsx @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +// import { ManageSearchMemberContext } from '../ManageSearchMember'; +import {ManageSearchMemberContext } from './ManageSearchMember'; + +export const useManageSearchMemberContext = () => { + const context = useContext(ManageSearchMemberContext); + if (!context) { + throw new Error('useManageSearchMemberContext must be used within ManageSearchMemberContextProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx b/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx index 6945079..b9bf885 100644 --- a/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx +++ b/src/pages/menu/manage-menu/hooks/ManageMenusContext.tsx @@ -237,7 +237,7 @@ const ManageMenusContextProvider = ({ children }: { children: React.ReactNode }) filter = filter.length === 0 ? {} : { name: { like: `%${filter[0].value?.toLowerCase()}%` } }; const query: any = { - limit, + limit:100, page: 1, with_deleted: false, order_field: sorting[0].id, diff --git a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx index 559857f..37da31e 100644 --- a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx @@ -14,13 +14,11 @@ import { const ListToolbar = () => { const { table, reload } = useDataGrid(); - // State untuk filter const [trxDate, settrxDate] = useState({ from: '', to: '' }); const [statusApproval, setStatusApproval] = useState(''); const [searchValue, setSearchValue] = useState(''); const [typeSearchValue, setSearchTypeValue] = useState(''); - // Set tanggal default saat pertama mount useEffect(() => { const today = new Date(); const formatDate = (date: Date) => date.toISOString().split('T')[0]; @@ -30,28 +28,21 @@ const ListToolbar = () => { }); }, []); - // Fungsi untuk apply semua filter sekaligus saat tombol Filter ditekan const handleFilterData = useCallback(() => { try { - if (!trxDate.from || !trxDate.to) { - toast.error('Please select From and To dates'); - return; - } + // if (!trxDate.from || !trxDate.to) { + // toast.error('Please select From and To dates'); + // return; + // } - // Set filter tanggal table.getColumn('transaction_date')?.setFilterValue(trxDate); - - // Set filter status approval table.getColumn('status_approve')?.setFilterValue(statusApproval || ''); - // Set filter search type sebagai column filter id 'searchtype' table.setColumnFilters((prevFilters) => { - // Hapus dulu filter dengan id 'searchtype' dan 'code' agar gak duplikat const filtered = prevFilters.filter( (f) => f.id !== 'searchtype' && f.id !== 'code' ); - // Tambahkan filter baru jika ada if (typeSearchValue) { filtered.push({ id: 'searchtype', value: typeSearchValue }); } @@ -62,7 +53,6 @@ const ListToolbar = () => { return filtered; }); - // Reset ke halaman pertama table.setPageIndex(0); } catch (error) { toast.error('Error applying filter'); @@ -79,7 +69,9 @@ const ListToolbar = () => { settrxDate({ ...trxDate, from: e.target.value })} + onChange={(event) => + settrxDate({ ...trxDate, from: event.target.value }) + } name="from" /> @@ -89,7 +81,9 @@ const ListToolbar = () => { settrxDate({ ...trxDate, to: e.target.value })} + onChange={(event) => + settrxDate({ ...trxDate, to: event.target.value }) + } name="to" /> @@ -110,7 +104,10 @@ const ListToolbar = () => { { + setSearchTypeValue(value); + settrxDate({ from: '', to: '' }); // clear tanggal saat search type dipilih + }} > @@ -132,7 +129,6 @@ const ListToolbar = () => { /> - {/* Tombol Filter */} Filter @@ -147,13 +143,11 @@ const ListToolbar = () => { const today = new Date(); const formatDate = (date: Date) => date.toISOString().split('T')[0]; - // Reset semua filter dan tanggal ke hari ini setSearchValue(''); setStatusApproval(''); settrxDate({ from: formatDate(today), to: formatDate(today) }); setSearchTypeValue(''); - // Reset filter di tabel table.getColumn('code')?.setFilterValue(''); table.getColumn('status_approve')?.setFilterValue(''); table.getColumn('transaction_date')?.setFilterValue(''); diff --git a/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx index 30c2afd..dd7642c 100644 --- a/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx +++ b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx @@ -53,12 +53,24 @@ const ManageApprovalTransactionContext = createContext(initialProp const API_URL = apiConfig.transaction; const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode }) => { + const [isLoadingDetail, setIsLoadingDetail] = useState(false); const [showDetailDialog, setShowDetailDialog] = useState(false); const [showApprovalDialog, setShowApprovalDialog] = useState(false); const [selectedTransactionId, setSelectedTransactionId] = useState(null); const [selectedTransactionIdForApproval, setSelectedTransactionIdForApproval] = useState(null); const [transaction, setTransaction] = useState([]); + const handleShowDetail = (transactionId: any) => { + setIsLoadingDetail(true); + setSelectedTransactionId(transactionId); + + // Simulasi loading (bisa ganti dengan fetch detail data jika perlu) + setTimeout(() => { + setShowDetailDialog(true); + setIsLoadingDetail(false); + }, 1000); // 500ms loading + }; + const { GetData } = useCallApi(); const navigate = useNavigate(); @@ -159,12 +171,12 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } }, { accessorFn: (row) => { - + switch (row.status) { case 'C': return 'COMPLETE'; case 'F': return 'FAILED'; case 'O': return 'ON PROCESS'; - case "P" : return 'PENDING'; + case "P": return 'PENDING'; case 'R': return 'ROLLBACK'; case 'S': return 'REVERSAL'; default: return 'UNKNOWN'; @@ -236,11 +248,8 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } return ( { - setSelectedTransactionId(row.id); - setShowDetailDialog(true); - }} + className={`btn btn-sm btn-icon btn-clear btn-light ${isLoadingDetail ? "cursor-not-allowed opacity-60" : ""}`} + onClick={() => !isLoadingDetail && handleShowDetail(row.id)} > @@ -276,40 +285,16 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } let enddate = today.toISOString().split('T')[0]; let transactioncode = ''; let approvalstatus = ''; + let searchType = ''; + let codeValue = ''; if (Array.isArray(filter)) { filter.forEach((f: any) => { - // console.log(f.id); if (f.id === 'transaction_date' && f.value?.from && f.value?.to) { startdate = f.value.from; enddate = f.value.to; } - if (f.id === 'code' && f.value) { - transactioncode = f.value; - } - - if (f.id === 'status_approve' && f.value) { - approvalstatus = f.value; - } - }); - } - - - // ✅ Declare formattedFilter early - const formattedFilter: any = { - "Transactions.transaction_date": { - from: `${startdate} 00:00:00`, - to: `${enddate} 23:59:59` - } - }; - - if (Array.isArray(filter)) { - let searchType = ''; - let codeValue = ''; - - // Ambil code dan searchtype dulu - filter.forEach((f: any) => { if (f.id === 'code' && f.value) { codeValue = f.value; } @@ -318,29 +303,35 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } searchType = f.value; } - if (f.id === 'transaction_date' && f.value?.from && f.value?.to) { - startdate = f.value.from; - enddate = f.value.to; - formattedFilter["Transactions.transaction_date"] = { - from: `${startdate} 00:00:00`, - to: `${enddate} 23:59:59` - }; + if (f.id === 'status_approve' && f.value) { + approvalstatus = f.value; } }); + } - // Terapkan berdasarkan searchtype + code - if (searchType && codeValue) { - if (searchType === "msisdn") { - formattedFilter["Transactions.origin_msisdn"] = { like: `%${codeValue}%` }; - } else if (searchType === "fullname") { - formattedFilter["Transactions.origin_name"] = { like: `%${codeValue}%` }; - } else if (searchType === "trxid") { - formattedFilter["Transactions.code"] = { like: `%${codeValue}%` }; - } + // 💡 Build formattedFilter only after values are finalized + const formattedFilter: any = {}; + + // ✅ Only add transaction_date filter if searchType is NOT filled + if (!searchType) { + formattedFilter["Transactions.transaction_date"] = { + from: `${startdate} 00:00:00`, + to: `${enddate} 23:59:59` + }; + } + + // ✅ Apply searchType filters if both searchType and codeValue exist + if (searchType && codeValue) { + if (searchType === "msisdn") { + formattedFilter["Transactions.origin_msisdn"] = { like: `%${codeValue}%` }; + } else if (searchType === "fullname") { + formattedFilter["Transactions.origin_name"] = { like: `%${codeValue}%` }; + } else if (searchType === "trxid") { + formattedFilter["Transactions.code"] = { like: `%${codeValue}%` }; } } - // Handle approval status filter + // ✅ Handle approval status formattedFilter["Transactions.status_approve"] = approvalstatus ? approvalstatus : { in: ["W", "Y", "N"] }; @@ -365,7 +356,7 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } } catch (error) { console.error('Error fetching transaction', error); - return { data: [], totalCount: 0 }; // Fail-safe fallback + return { data: [], totalCount: 0 }; } }; @@ -385,6 +376,32 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } > + {isLoadingDetail && ( + + + + + + Loading data... + + )} + { (table.getState().columnFilters.find(f => f.id === 'kind')?.value as string) ?? '' ); + const [status, setStatus] = useState( + (table.getState().columnFilters.find(f => f.id === 'status')?.value as string) ?? '' + ); + const { GetData } = useCallApi(); const API_URL = apiConfig.transaction; const formatDate = (date: Date): string => date.toISOString().split('T')[0]; - // Set tanggal default hari ini saat mount useEffect(() => { const today = new Date(); const formatted = formatDate(today); settrxDate({ from: formatted, to: formatted }); }, []); - // **Hilangkan semua useEffect yang auto apply filter saat input berubah** - // Karena kamu mau filter apply hanya lewat tombol Filter - - // Fungsi untuk apply SEMUA filter sekaligus saat tombol Filter diklik - const handleFilterData = useCallback(() => { - if (!trxDate.from || !trxDate.to) { - toast.error('Please select both From and To dates'); - return; + // ✅ Reset tanggal jika typeSearchValue dipilih + useEffect(() => { + if (typeSearchValue) { + settrxDate({ from: '', to: '' }); } + }, [typeSearchValue]); + + const handleFilterData = useCallback(() => { + // if (!trxDate.from || !trxDate.to) { + // toast.error('Please select both From and To dates'); + // return; + // } - // Terapkan filter tanggal table.getColumn('transaction_date')?.setFilterValue(trxDate); - // Terapkan filter jenis transaksi (kind) table.setColumnFilters((prev) => { - // Hapus filter 'kind' dan 'searchtype' agar bisa set ulang - const others = prev.filter(f => f.id !== 'kind' && f.id !== 'searchtype' && f.id !== 'code'); - // Bangun array baru dengan filter yang diinginkan + const others = prev.filter(f => f.id !== 'kind' && f.id !== 'searchtype' && f.id !== 'code' && f.id !== 'status'); const filters: any[] = [...others]; if (typeValue) filters.push({ id: 'kind', value: typeValue }); if (typeSearchValue) filters.push({ id: 'searchtype', value: typeSearchValue }); if (searchValue) filters.push({ id: 'code', value: searchValue }); - + if (status) filters.push({ id: 'status', value: status }); return filters; }); table.setPageIndex(0); - }, [trxDate, typeValue, typeSearchValue, searchValue, table]); + }, [trxDate, typeValue, typeSearchValue, searchValue, status, table]); const exporDataToExcel = async ( typeSearchValue: string, searchValue: string, trxDate: any, - typeValue: string + typeValue: string, + status: string ) => { try { const formattedFilter: any = { @@ -146,6 +149,7 @@ const ListToolbar = () => { name="to" /> + setTypeValue(value)}> @@ -173,6 +177,19 @@ const ListToolbar = () => { + setStatus(value)}> + + + + + PENDING + ON PROCESS + FAILED + COMPLETE + ROLLBACK + + + setSearchTypeValue(value)}> @@ -197,7 +214,6 @@ const ListToolbar = () => { Filter - @@ -209,7 +225,8 @@ const ListToolbar = () => { typeSearchValue || '', searchValue || '', trxDate || {}, - typeValue || '' + typeValue || '', + status || '' ); if (result) { @@ -236,6 +253,7 @@ const ListToolbar = () => { setSearchValue(''); setSearchTypeValue(''); setTypeValue(''); + setStatus(''); settrxDate({ from: formatDate(today), to: formatDate(today) }); table.setColumnFilters([]); table.setPageIndex(0); diff --git a/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx index 8a3b15b..f66ae91 100644 --- a/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx +++ b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx @@ -9,6 +9,7 @@ import { useNavigate } from 'react-router'; import DetailTransaction from '../blocks/DetailTransaction'; import ResendTransaction from '../blocks/ResendTransaction'; import RollbackTransaction from '../blocks/RollbackTransaction'; +import { start } from 'repl'; interface TransactionProps { id: number; @@ -46,6 +47,7 @@ const ManageTransactionContext = createContext(initialProps); const API_URL = apiConfig.transaction; const TransactionProvider = ({ children }: { children: React.ReactNode }) => { + const [isLoadingDetail, setIsLoadingDetail] = useState(false); const [showDetailDialog, setShowDetailDialog] = useState(false); const [showRollbackDialog, setShowRollbackDialog] = useState(false); const [selectedTransactionId, setSelectedTransactionId] = useState(null); @@ -57,6 +59,18 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { }; + const handleShowDetail = (transactionId: any) => { + setIsLoadingDetail(true); + setSelectedTransactionId(transactionId); + + // Simulasi loading (bisa ganti dengan fetch detail data jika perlu) + setTimeout(() => { + setShowDetailDialog(true); + setIsLoadingDetail(false); + }, 1000); // 500ms loading + }; + + const columns = useMemo[]>( () => [ { @@ -260,18 +274,17 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { enableHiding: false, cell: (data) => { const row = data.row.original; - const isVisible = (row.status === 'F' ? true : false || row.status === 'P' ? true : false) && row.status_approve !== 'W' ? true : false; + // const isVisible = (row.status === 'F' ? true : false || row.status === 'P' ? true : false) && row.status_approve !== 'W' ? true : false; return ( - { - setSelectedTransactionId(row.id); - setShowDetailDialog(true); - }} - > - - + + !isLoadingDetail && handleShowDetail(row.id)} + > + + + { const today = new Date(); const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - let startdate = firstDayOfMonth.toISOString().split('T')[0]; - let enddate = today.toISOString().split('T')[0]; + let startdate: string | undefined; + let enddate: string | undefined; let transactioncode = ''; let type = ''; - - // ✅ Declare formattedFilter early - const formattedFilter: any = { - "Transactions.transaction_date": { - from: `${startdate} 00:00:00`, - to: `${enddate} 23:59:59` - } - }; + let status = ''; + let searchType = ''; + let codeValue = ''; if (Array.isArray(filter)) { - let searchType = ''; - let codeValue = ''; - - // Ambil code dan searchtype dulu + // Ambil semua value dari filter terlebih dahulu filter.forEach((f: any) => { if (f.id === 'code' && f.value) { codeValue = f.value; @@ -329,18 +334,45 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { if (f.id === 'transaction_date' && f.value?.from && f.value?.to) { startdate = f.value.from; enddate = f.value.to; - formattedFilter["Transactions.transaction_date"] = { - from: `${startdate} 00:00:00`, - to: `${enddate} 23:59:59` - }; } if (f.id === 'kind' && f.value) { type = f.value; } + + if (f.id === 'status' && f.value) { + status = f.value; + } }); - // Terapkan berdasarkan searchtype + code + // Kalau tidak ada searchType, baru pakai tanggal + if (!searchType) { + if (!startdate || !enddate) { + startdate = firstDayOfMonth.toISOString().split('T')[0]; + enddate = today.toISOString().split('T')[0]; + } + } else { + // Kosongkan supaya tidak digunakan di bawah + startdate = undefined; + enddate = undefined; + } + + // Build formattedFilter setelah semua nilai diketahui + const formattedFilter: any = {}; + + if( status) { + formattedFilter["Transactions.status"] = status; + } + + // Tambahkan filter tanggal HANYA jika tidak ada searchType + if (startdate && enddate) { + formattedFilter["Transactions.transaction_date"] = { + from: `${startdate} 00:00:00`, + to: `${enddate} 23:59:59` + }; + } + + // Tambahkan filter berdasarkan searchType if (searchType && codeValue) { if (searchType === "msisdn") { formattedFilter["origin_customer.msisdn"] = { like: `%${codeValue}%` }; @@ -350,29 +382,31 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { formattedFilter["Transactions.code"] = { like: `%${codeValue}%` }; } } + + // Tambahkan filter jenis transaksi jika ada + if (type) { + formattedFilter["Transactions.kind"] = type; + } + + const response = await GetData(`${API_URL}/transaction/history`, { + limit, + page: page + 1, + with_deleted: false, + order_field: "Transactions.created_at", + order_direction: 'DESC', + filter: JSON.stringify(formattedFilter) + }); + + 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 }; } - - if (type) { - formattedFilter["Transactions.kind"] = type; - } - - const response = await GetData(`${API_URL}/transaction/history`, { - limit, - page: page + 1, - with_deleted: false, - order_field: "Transactions.created_at", - order_direction: 'DESC', - filter: JSON.stringify(formattedFilter) - }); - - 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 }; + return { data: [], totalCount: 0 }; } catch (error) { console.error('Error fetching transaction', error); @@ -381,6 +415,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { }; + return ( { > + {isLoadingDetail && ( + + + + + + Loading data... + + )} + { const initialForm: { - msisdn: string; + id_customer: string; amount: string; pin: string; purpose: string; } = { - msisdn: '', + id_customer: '', amount: '', pin: '', purpose: '' @@ -74,7 +74,7 @@ const WithdrawInput = () => { const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); setCustomerMsisdn( response?.data.list.map((item: any) => ({ - value: item.msisdn, + value: item.id, label: `${item.msisdn} - ${item.fullname}` })) ); @@ -91,12 +91,12 @@ const WithdrawInput = () => { try { let response = await PostData(`${API_URL}/transaction/transfer`, { - msisdn_destination: form.msisdn, + msisdn_destination: '1112007', amount: form.amount, pin: form.pin, purpose: form.purpose, id_transaction_type: '380f270b-1f44-406f-a196-d6f784d68eb4', - id_origin_customer: getAuth()?.user.customer.id, + id_origin_customer: form.id_customer, type: 'S' }); if (response?.status == true) { @@ -119,7 +119,7 @@ const WithdrawInput = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (form.amount == '' || form.msisdn == '' || form.pin == '') { + if (form.amount == '' || form.id_customer == '' || form.pin == '') { setAlert({ show: true, message: 'Please fill in all required fields.' @@ -144,10 +144,10 @@ const WithdrawInput = () => { return () => clearTimeout(timer); }; - const handleMsisdnSelect = (msisdn: string) => { - setForm({ ...form, msisdn }); + const handleMsisdnSelect = (item: { value: string; label: string }) => { + setForm({ ...form, id_customer: item.value }); setDropdownOpen(false); - setSearchTerm(msisdn); + setSearchTerm(item.label); }; useEffect(() => { @@ -200,7 +200,7 @@ const WithdrawInput = () => { handleMsisdnSelect(item.value)} + onClick={() => handleMsisdnSelect(item)} > {item.label} diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index ab42475..3544d1e 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -49,6 +49,7 @@ import AgentBalance from '@/pages/members/agent-balance/AgentBalance'; // DISBURSEMENT import HistoryTransactionDisbursement from '@/pages/disbursement/history-transaction/HistoryTransaction'; +import SearchMember from '@/pages/members/search-member/SearchMember'; import WithdrawalEmoney from '@/pages/transaction/withdrawl-emoney/WithdrawalEmoney'; // DISBURSEMENT @@ -94,6 +95,8 @@ const AppRoutingSetup = (): ReactElement => { } /> } /> } /> + } /> + } /> } />
- {agent_name} -
{agent_name}
Imprime husi : Telin Digital Solution
Imprime : 01–06–2025
Pin Status: {getPinStatus(formData.status).msg}
Reset PIN
Change Group
Edit Member