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/pages/members/agent-balance/AgentBalance.tsx b/src/pages/members/agent-balance/AgentBalance.tsx index ccb9351..e0b77a0 100644 --- a/src/pages/members/agent-balance/AgentBalance.tsx +++ b/src/pages/members/agent-balance/AgentBalance.tsx @@ -104,7 +104,7 @@ const AgentBalance = () => { const handleUpdate = async (data: any) => { await getQrCode(data.id); - setFormData(data) + setFormData(data); setDialogType('update'); setIsDialogOpen(true); }; @@ -116,14 +116,14 @@ const AgentBalance = () => { // 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 }}); + setBarcode(``); + let getToken = await axios.get(`${BASE_URL}/token/get`, { params: { id: customerId } }); console.log(getToken.data.data); 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`) @@ -193,7 +193,11 @@ const AgentBalance = () => {
- +
@@ -203,20 +207,20 @@ const AgentBalance = () => { }; // ==================== -const PaymentCard = ({ barcode, agent_name, customerId }:any) => { +const PaymentCard = ({ barcode, agent_name, customerId }: any) => { const [token_legacy, setToken_legacy] = useState(''); - const cardRef:any = useRef(null); + 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; @@ -225,14 +229,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'); @@ -244,32 +248,49 @@ 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) 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); } - } + }; return (
- {/* Card content to render */} -
-
-

- {agent_name} -

+
+ Card Frame + +
+

{agent_name}

+ {barcode && ( - Barcode +
+ Barcode +
)} + +
+ +
+
+

Imprime husi : Telin Digital Solution

+

Imprime : 01–06–2025

+
+
- +
- {/* Download Button */}
- -
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 ( +
+
+ + {}} + /> +
+
+ ); + }; + + const generateImage = (label: string, name: string, photoUrl: any) => { + return ( + <> + {previewImg.status && ( + + )} +
+ +
+ {photoUrl ? ( +
+ {name} onClickImg(imagePreview(photoUrl))} + /> +
+ ) : ( + No Data + )} +
+
+ + ); + }; + + const generatePinStatusField = (label: string, status: string) => { + const pinStatus = getPinStatus(status); + + if (!pinStatus.showField) { + return null; + } + + return ( +
+
+ +
+ {pinStatus.msg} + {pinStatus.canUnblock && ( + + )} + {!pinStatus.canUnblock && !pinStatus.canBlock && ( + + )} +
+
+
+ ); + }; + + 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 */} +
+ +
+
+
+
+
+
+ + setConfirmDialog(false)} + /> + + ); +}; + +function PreviewImage({previewImgDialog, handlePreviewImgDialog, modalImage}: any) { + return ( + handlePreviewImgDialog(open)}> + + + + + Preview + + + + ); +} + +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 ( +
+
+
+
+
+ + +
+ +
+ + setSearchValue(e.target.value)} + placeholder={getPlaceholder()} + className="w-full h-12 text-base" + /> +
+ +
+ + +
+
+
+
+
+ ); +}; + +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 ( + <> + + + ); + }, + 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

+ +
+ +
+ + 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 = () => { setTypeValue(value)}> @@ -197,7 +196,6 @@ const ListToolbar = () => { -
diff --git a/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx index 8a3b15b..dfa0451 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; @@ -299,24 +300,15 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { 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 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,10 +321,6 @@ 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) { @@ -340,7 +328,30 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { } }); - // 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 = {}; + + // 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 +361,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 +394,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { }; + return ( { try { const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); - console.log(response?.data); setCustomerMsisdn( response?.data.list.map((item: any) => ({ value: item.id, @@ -145,10 +144,10 @@ const WithdrawInput = () => { return () => clearTimeout(timer); }; - const handleMsisdnSelect = (msisdn: string) => { - setForm({ ...form, id_customer: msisdn }); + const handleMsisdnSelect = (item: { value: string; label: string }) => { + setForm({ ...form, id_customer: item.value }); setDropdownOpen(false); - setSearchTerm(msisdn); + setSearchTerm(item.label); }; useEffect(() => { @@ -201,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 => { } /> } /> } /> + } /> + } /> } />