From e8cde416a59438cd548e237ab7978a9169bcbc8b Mon Sep 17 00:00:00 2001 From: Rizqika Date: Tue, 31 Dec 2024 11:33:31 +0700 Subject: [PATCH] Wa-Message --- Backend/rekan_veri_be | 1 + package-lock.json | 39 ++ package.json | 2 + src/components/Sidebar/SubMenu.jsx | 2 +- src/components/common/CustomLabel.jsx | 5 + src/components/index.js | 3 + .../FaceRecognition/Section/Enroll.jsx | 10 +- .../FaceRecognition/Section/Search.jsx | 10 +- .../FaceRecognition/Section/Verify.jsx | 10 +- .../Biometric/FaceRecognition/Transaction.jsx | 142 ++--- src/screens/Wa/Verify/Section/Auth.jsx | 74 ++- src/screens/Wa/Verify/Section/Message.jsx | 492 +--------------- .../Verify/Section/Messages/BulkMessage.jsx | 554 ++++++++++++++++++ .../Verify/Section/Messages/SingleMessage.jsx | 433 ++++++++++++++ .../Wa/Verify/Section/Messages/index.js | 7 + 15 files changed, 1201 insertions(+), 583 deletions(-) create mode 160000 Backend/rekan_veri_be create mode 100644 src/components/common/CustomLabel.jsx create mode 100644 src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx create mode 100644 src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx create mode 100644 src/screens/Wa/Verify/Section/Messages/index.js diff --git a/Backend/rekan_veri_be b/Backend/rekan_veri_be new file mode 160000 index 0000000..b7bb5e8 --- /dev/null +++ b/Backend/rekan_veri_be @@ -0,0 +1 @@ +Subproject commit b7bb5e81c38e443cca46785e23e8785f3a2cb3f5 diff --git a/package-lock.json b/package-lock.json index 142386c..100463c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@testing-library/user-event": "^13.5.0", "ajv": "^8.17.1", "ajv-keywords": "^5.1.0", + "axios": "^1.7.9", "bootstrap": "^5.3.3", "font-awesome": "^4.7.0", "react": "^18.3.1", @@ -27,6 +28,7 @@ "react-router-dom": "^6.28.0", "react-scripts": "^5.0.1", "react-select": "^5.8.2", + "sweetalert2": "^11.15.3", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -5135,6 +5137,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -13351,6 +13376,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -15476,6 +15506,15 @@ "node": ">=4" } }, + "node_modules/sweetalert2": { + "version": "11.15.3", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.15.3.tgz", + "integrity": "sha512-+0imNg+XYL8tKgx8hM0xoiXX3KfgxHDmiDc8nTJFO89fQEEhJlkecSdyYOZ3IhVMcUmoNte4fTIwWiugwkPU6w==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 7bcfde4..ce3709b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@testing-library/user-event": "^13.5.0", "ajv": "^8.17.1", "ajv-keywords": "^5.1.0", + "axios": "^1.7.9", "bootstrap": "^5.3.3", "font-awesome": "^4.7.0", "react": "^18.3.1", @@ -22,6 +23,7 @@ "react-router-dom": "^6.28.0", "react-scripts": "^5.0.1", "react-select": "^5.8.2", + "sweetalert2": "^11.15.3", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/components/Sidebar/SubMenu.jsx b/src/components/Sidebar/SubMenu.jsx index 0d88a98..cab03a7 100644 --- a/src/components/Sidebar/SubMenu.jsx +++ b/src/components/Sidebar/SubMenu.jsx @@ -4,7 +4,7 @@ import DeepMenu from './DeepMenu'; const SubMenu = ({ heading, target, iconClass, subMenus, activeMenu, onMenuClick }) => { return ( <> -
{heading}
+ {/*
{heading}
*/} { + return ; +}; + +export default CustomLabel; \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index 1d69192..20e826f 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -3,9 +3,12 @@ import Sidebar from "./Sidebar/Sidebar"; import Main from "./Main"; import Footer from "./Footer"; +import CustomLabel from "./common/CustomLabel"; + export { Navbar, Sidebar, Main, Footer, + CustomLabel } \ No newline at end of file diff --git a/src/screens/Biometric/FaceRecognition/Section/Enroll.jsx b/src/screens/Biometric/FaceRecognition/Section/Enroll.jsx index 4ec16ac..bf50ce8 100644 --- a/src/screens/Biometric/FaceRecognition/Section/Enroll.jsx +++ b/src/screens/Biometric/FaceRecognition/Section/Enroll.jsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import Select from 'react-select' import { ServerDownAnimation } from '../../../../assets/images'; +import { CustomLabel} from '../../../../components'; const Enroll = () => { @@ -322,15 +323,6 @@ const Enroll = () => { } }; - // handle Labeling - const CustomLabel = ({ overRide, children, ...props }) => { - // We intentionally don't pass `overRide` to the label - return ( - - ); - }; // Handle Server Down if (!isServer) { diff --git a/src/screens/Biometric/FaceRecognition/Section/Search.jsx b/src/screens/Biometric/FaceRecognition/Section/Search.jsx index 940f069..1e16322 100644 --- a/src/screens/Biometric/FaceRecognition/Section/Search.jsx +++ b/src/screens/Biometric/FaceRecognition/Section/Search.jsx @@ -4,6 +4,7 @@ import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/fr import { FileUploader } from 'react-drag-drop-files'; import Select from 'react-select' import { ServerDownAnimation } from '../../../../assets/images'; +import { CustomLabel} from '../../../../components'; const fileTypes = ["JPG", "JPEG"]; // Allowed file types @@ -319,15 +320,6 @@ const Search = () => { } }; - const CustomLabel = ({ overRide, children, ...props }) => { - // We intentionally don't pass `overRide` to the label - return ( - - ); - }; - if (!isServer) { return (
diff --git a/src/screens/Biometric/FaceRecognition/Section/Verify.jsx b/src/screens/Biometric/FaceRecognition/Section/Verify.jsx index f84db28..b028cb5 100644 --- a/src/screens/Biometric/FaceRecognition/Section/Verify.jsx +++ b/src/screens/Biometric/FaceRecognition/Section/Verify.jsx @@ -4,6 +4,7 @@ import { faChevronDown, faTimes } from '@fortawesome/free-solid-svg-icons'; import Select from 'react-select' import { ServerDownAnimation } from '../../../../assets/images'; import { FileUploader } from 'react-drag-drop-files'; +import { CustomLabel} from '../../../../components'; const Verify = () => { const BASE_URL = process.env.REACT_APP_BASE_URL; @@ -314,15 +315,6 @@ const Verify = () => { } }; - const CustomLabel = ({ overRide, children, ...props }) => { - // We intentionally don't pass `overRide` to the label - return ( - - ); - }; - const handleInputChangeApplication = (newInputValue) => { // Limit input to 15 characters for Application ID if (newInputValue.length <= 15) { diff --git a/src/screens/Biometric/FaceRecognition/Transaction.jsx b/src/screens/Biometric/FaceRecognition/Transaction.jsx index 3976893..6d195d0 100644 --- a/src/screens/Biometric/FaceRecognition/Transaction.jsx +++ b/src/screens/Biometric/FaceRecognition/Transaction.jsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react'; import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting import { NoAvailable } from '../../../assets/icon'; +const BASE_URL = process.env.REACT_APP_BASE_URL +const API_KEY = process.env.REACT_APP_API_KEY // Pagination Component const Pagination = ({ currentPage, totalPages, onPageChange }) => { const handlePrev = () => { @@ -106,6 +108,10 @@ const Transaction = () => { const [transactionData, setTransactionData] = useState([]); const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state const dataPerPage = 10; // Data per page (10 data per page) + const [totalPages, setTotalPages] = useState(1); // Total pages from API + const [searchTerm, setSearchTerm] = useState(''); + const [sortColumn, setSortColumn] = useState('tf.id'); + const [sortOrder, setSortOrder] = useState('asc'); const buttonData = [ { label: 'Copy', enabled: true }, @@ -115,32 +121,52 @@ const Transaction = () => { { label: 'Print', enabled: true }, { label: 'Column Visibility', enabled: true }, ]; - - // Generate 691 dummy transactions - const generateDummyData = (numOfItems) => { - const transactionData = []; - - for (let i = 1; i <= numOfItems; i++) { - transactionData.push({ - transactionId: `TX${String(i).padStart(3, '0')}`, - applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, - dataSent: `${Math.floor(Math.random() * 100) + 50}MB`, - endPoint: `Endpoint ${Math.floor(Math.random() * 5) + 1}`, - subjectId: `S${String(i).padStart(3, '0')}`, - serviceCharged: `$${(Math.random() * 50 + 5).toFixed(2)}`, - mode: Math.random() > 0.5 ? 'Online' : 'Offline', - status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], - }); - } - - return transactionData; + // Fetch transaction data from API + const fetchTransactionData = async (page, limit, search = '', sortColumn = 'tf.id', sortOrder = 'asc') => { + try { + console.log(`Fetching data for page: ${page}, limit: ${limit}, search: ${search}, sortColumn: ${sortColumn}, sortOrder: ${sortOrder}`); // Log page, limit, and search + const response = await fetch(`${BASE_URL}/trx_face/table-log?search=${search}&sortColumn=${sortColumn}&sortOrder=${sortOrder}&limit=${limit}&page=${page}`, { + method: 'GET', + headers: { 'accept': 'application/json', 'x-api-key': API_KEY }, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + const { data, total } = result.details.details.data; + setTransactionData(data); + setTotalPages(Math.ceil(total / dataPerPage)); + } catch (error) { + console.error('Error fetching transaction data:', error); + } }; - // Set the generated transaction data + const fetchPaginationData = async (page, limit, search = '') => { + try { + console.log(`Fetching pagination data for page: ${page}, limit: ${limit}, search: ${search}`); // Log page, limit, and search + const url = `${BASE_URL}/header_detail_param/paging?header_id=1&page=${page}&limit=${limit}&search=${search}`; + console.log(`Fetching URL: ${url}`); // Log the URL + const response = await fetch(url, { + method: 'GET', + headers: { 'accept': 'application/json', 'x-api-key': API_KEY }, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + const { data } = result.details; + setTotalPages(Math.ceil(data.length / dataPerPage)); + } catch (error) { + console.error('Error fetching pagination data:', error); + } + }; + + // Fetch data when component mounts and when currentPage changes useEffect(() => { - setTransactionData(generateDummyData(97513)); // count data dummy transactions - }, []); + fetchTransactionData(currentPage, dataPerPage, searchTerm, sortColumn, sortOrder); + fetchPaginationData(currentPage, dataPerPage); + }, [currentPage, searchTerm, sortColumn, sortOrder]); // Sorting function const sortData = (data, config) => { @@ -178,8 +204,14 @@ const Transaction = () => { setCurrentPage(page); }; - // Calculate total pages based on the data and data per page - const totalPages = Math.ceil(transactionData.length / dataPerPage); + // Handle search + const handleSearch = async (event) => { + const searchQuery = event.target.value; + setSearchTerm(searchQuery); + setCurrentPage(1); // Reset to first page on new search + await fetchPaginationData(1, dataPerPage, searchQuery); + await fetchTransactionData(1, dataPerPage, searchQuery); + }; // Paginated data const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage); @@ -279,11 +311,12 @@ const Transaction = () => {
{/* Search Bar with Icon */} -
+
{/* FontAwesome search icon */} @@ -297,59 +330,40 @@ const Transaction = () => { - {/* Kolom untuk Nomor Urut */} - - - {/* Kolom Nomor Urut */} - {/* Nomor urut berdasarkan halaman dan index */} - - - - - - - + + + + @@ -440,4 +449,3 @@ const styles = { marginLeft: '0.7rem', // Adjust as needed }, }; - diff --git a/src/screens/Wa/Verify/Section/Auth.jsx b/src/screens/Wa/Verify/Section/Auth.jsx index 5aa0c21..ca9032d 100644 --- a/src/screens/Wa/Verify/Section/Auth.jsx +++ b/src/screens/Wa/Verify/Section/Auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from 'react' import { ServerDownAnimation } from '../../../../assets/images'; import Select from 'react-select' +import Swal from 'sweetalert2'; const BASE_URL = process.env.REACT_APP_BASE_URL const API_KEY = process.env.REACT_APP_API_KEY @@ -38,15 +39,32 @@ const Verify = ({ onVerify, generateId }) => { .then(data => { if (data.status_code === 201) { setError(''); // Clear any existing error message - alert('OTP verification successful!'); // Show success alert + // Show success alert + Swal.fire({ + icon: 'success', + title: 'Success', + text: 'OTP verified successfully', + }); onVerify(); // Trigger verify callback } else { setError(data.details.message || "Verification failed"); + // Show error alert + Swal.fire({ + icon: 'error', + title: 'Error', + text: data.details.message || "Verification failed", + }); } }) .catch(error => { console.error('Verification failed:', error); setError("Failed to verify OTP"); + // Show error alert + Swal.fire({ + icon: 'error', + title: 'Error', + text: "Failed to verify OTP", + }); }); }; @@ -165,8 +183,8 @@ const Auth = () => { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const [applicationIds, setApplicationIds] = useState([]); - const [showVerify, setShowVerify] = useState(false); + const [applicationIds, setApplicationIds] = useState([]); + const [showVerify, setShowVerify] = useState(false); const [showPreview, setShowPreview] = useState(false); const [errors, setErrors] = useState({}); @@ -183,13 +201,13 @@ const Auth = () => { const [generateId, setgenerateId] = useState(''); const handleApplicationChange = (selectedOption) => { - const selectedId = selectedOption.value; - const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId)); + const selectedId = selectedOption?.value; + const selectedApp = applicationIds.find(app => app.id === selectedId); - setApplicationId(selectedOption ? selectedOption.value : ''); + setApplicationId(selectedId); if (selectedApp) { - setSelectedQuota(selectedApp.quota); + setSelectedQuota(selectedApp.quota || 0); } }; @@ -203,10 +221,11 @@ const Auth = () => { setInputValueApplication(newInputValue); } }; + useEffect(() => { const fetchData = () => { setIsLoading(true); - + fetch(`${BASE_URL}/application/list`, { method: 'GET', headers: { @@ -254,10 +273,10 @@ const Auth = () => { setIsLoading(false); }); }; - + fetchData(); }, []); - + // Server Down Component if (!isServer) { return ( @@ -284,6 +303,7 @@ const Auth = () => { ); } + const handleVerify = () => { setShowPreview(true); // Show the preview when verified }; @@ -302,7 +322,7 @@ const Auth = () => { const handleClick = async () => { if (validate()) { setIsLoading(true); - + const requestData = { application_id: parseInt(applicationId), phone: phoneId, @@ -312,14 +332,14 @@ const Auth = () => { is_test: true, mode_id: 9 }; - + console.log('Request Data:', requestData); - + // Add timeout to ensure channel stays open const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 30000) ); - + Promise.race([ fetch(`${BASE_URL}/wa/otp`, { method: 'POST', @@ -336,20 +356,34 @@ const Auth = () => { console.log('API Response:', data); if (data.status_code === 201 && data.details.message === "Successfully") { setgenerateId(data.details.data.id); - setShowVerify(false); - alert('Authentication request successful!'); + setShowVerify(true); + Swal.fire({ + icon: 'success', + title: 'Success', + text: 'Authentication request successful!', + }); + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Failed to authenticate.', + }); } }) .catch(error => { console.error('Request failed:', error); + Swal.fire({ + icon: 'error', + title: 'Error', + text: `Failed to authenticate: ${error.message}`, + }); }) .finally(() => { setIsLoading(false); }); } }; - - + return ( <>
@@ -494,10 +528,10 @@ const Auth = () => {
{showVerify && } - + {showPreview && } ); -} +}; export default Auth; diff --git a/src/screens/Wa/Verify/Section/Message.jsx b/src/screens/Wa/Verify/Section/Message.jsx index 351e65d..050eabb 100644 --- a/src/screens/Wa/Verify/Section/Message.jsx +++ b/src/screens/Wa/Verify/Section/Message.jsx @@ -1,470 +1,6 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronDown, faChevronLeft, faCloudUploadAlt, faTimes, faImage } from '@fortawesome/free-solid-svg-icons'; -import { FileUploader } from 'react-drag-drop-files'; +import React, { useState, useRef } from 'react'; - -const BASE_URL = process.env.REACT_APP_BASE_URL -const API_KEY = process.env.REACT_APP_API_KEY - -const fileTypes = ['JPG', 'JPEG']; - -const CustomLabel = ({ children, ...props }) => { - return ; -}; - - -const SingleMessage = ({ isSelectOpen, handleFocus, handleBlur }) => { - const [applicationId, setApplicationId] = useState(''); - const [phoneId, setPhoneId] = useState(''); - const [variable_1, setVariable_1] = useState(''); - const [templateName, setTemplateName] = useState(''); - const [variable_2, setVariable_2] = useState(''); - const [preview, setPreview] = useState(null); - const [errors, setErrors] = useState({}); - const [applicationIds, setApplicationIds] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const fetchApplicationIds = async () => { - setIsLoading(true); - const url = `${BASE_URL}/application/list`; - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'accept': 'application/json', - 'x-api-key': API_KEY, - }, - }); - - const data = await response.json(); - if (data.status_code === 200) { - setApplicationIds(data.details.data); - } else { - console.error('Failed to fetch data:', data.details.message); - } - } catch (error) { - console.error('Error fetching application IDs:', error); - } finally { - setIsLoading(false); - } - }; - - fetchApplicationIds(); - }, []); - - const handleValidation = () => { - const newErrors = {}; - if (!applicationId) newErrors.applicationId = "Applications ID is required."; - if (!phoneId) newErrors.phoneId = "Phone Number is required."; - if (!variable_1) newErrors.variable_1 = "Variable 1 is required."; - if (!templateName) newErrors.templateName = "Template Name is required."; - if (!variable_2) newErrors.variable_2 = "Variable 2 is required."; - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; // Return true if no errors - }; - - const handleButtonClick = () => { - if (handleValidation()) { - const formattedPhoneId = `08${phoneId}`; - - // Log the input data - console.log({ - applicationId, - formattedPhoneId, - variable_1, - templateName, - variable_2 - }); - - setPreview({ - message: `Thank you for RSVP, ${variable_1}! We're excited to see you soon.`, - details: `Check the details and mark on your calendar:\nDate: Thursday, October 10, 2024 | 6:00 PM - 9:00 PM\nLocation: Stories SCBD, South Jakarta, Fairground Building, SCBD Lot No.14` - }); - } - }; - - return ( -
- {/* Inject keyframes for the spinner */} - - - {isLoading && ( -
-
-
- )} -
-
-
- - -
- {errors.applicationId &&

{errors.applicationId}

} -
-
-

- Remaining Quota -

-
- 0 - (times) -
-
-
- -
-
-
- - +62 - - { - const value = e.target.value; - // Allow only digits and enforce length constraints - if (/^\d*$/.test(value) && value.length <= 14) { - setPhoneId(value); - } - }} - minLength={9} - maxLength={14} - /> -
- {errors.phoneId &&

{errors.phoneId}

} -
-
-
- setVariable_1(e.target.value)} - maxLength={15} - /> -
- {errors.variable_1 &&

{errors.variable_1}

} -
-
- -
-
-
- setTemplateName(e.target.value)} - maxLength={15} - /> -
- {errors.templateName &&

{errors.templateName}

} -
-
-
- setVariable_2(e.target.value)} - maxLength={15} - /> -
- {errors.variable_2 &&

{errors.variable_2}

} -
-
- -
- -
- - {/* Preview Section */} - {preview && ( -
-
-

[Sips & Synergy Executive]

-

{preview.message}

-

{preview.details}

- -
-
- )} -
- ); -}; - -const BulkMessage = ({ isSelectOpen, handleFocus, handleBlur, handleClick, onImageUpload }) => { - const [applicationId, setApplicationId] = useState(''); - const [templateName, setTemplateName] = useState(''); - const [imageData, setImageData] = useState(null); - const [selectedImageName, setSelectedImageName] = useState(''); - const [errors, setErrors] = useState({}); - const fileInputRef = useRef(null); - const [applicationIds, setApplicationIds] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - - useEffect(() => { - const fetchApplicationIds = async () => { - setIsLoading(true); - const url = `${BASE_URL}/application/list`; - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'accept': 'application/json', - 'x-api-key': API_KEY, - }, - }); - - const data = await response.json(); - if (data.status_code === 200) { - setApplicationIds(data.details.data); - } else { - console.error('Failed to fetch data:', data.details.message); - } - } catch (error) { - console.error('Error fetching application IDs:', error); - } finally { - setIsLoading(false); - } - }; - - fetchApplicationIds(); - }, []); - - const handleValidation = () => { - const newErrors = {}; - if (!applicationId) newErrors.applicationId = "Application ID is required."; - if (!templateName) newErrors.templateName = "Template Name is required."; - if (!imageData) newErrors.imageData = "Image is required."; - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleImageUpload = (file) => { - if (file) { - setImageData(file); - setSelectedImageName(file.name); - if (onImageUpload) { - onImageUpload(file); - } - } else { - setErrors((prevErrors) => ({ - ...prevErrors, - imageData: "Image upload failed." - })); - } - }; - - const handleButtonClick = () => { - if (handleValidation()) { - handleClick(); - console.log('Form Data:', { - applicationId, - templateName, - imageData - }); - } else { - console.log('Validation errors:', errors); - } - }; - - const handleImageCancel = () => { - setImageData(null); - setSelectedImageName(''); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - return ( -
- {/* Inject keyframes for the spinner */} - - - -
-
-
- - -
- {errors.applicationId &&

{errors.applicationId}

} -
-
-

- Remaining Quota -

-
- 0 - (times) -
-
-
- -
-
-
- setTemplateName(e.target.value)} - maxLength={15} - /> -
- {errors.templateName &&

{errors.templateName}

} -
-
- - {/* Upload Section */} -
-
- - Upload Face Photo - - { - // Ensure files array is not empty - if (files.length > 0) { - handleImageUpload(files[0]); - } - }} - children={ -
- -

Drag and Drop Here

-

Or

- fileInputRef.current.click()}>Browse -

Recommended size: 300x300 (Max File Size: 2MB)

-

Supported file types: JPG, JPEG

-
- } - /> - { - if (e.target.files.length > 0) { - handleImageUpload(e.target.files[0]); - } - }} - /> - {errors.imageData && {errors.imageData}} -
-
- - {/* Display uploaded image name */} - {selectedImageName && ( -
-
- -
-
Uploaded File:
-

{selectedImageName}

-
-
- -
-
-
- )} - -
-
- -
-
-
- ); -}; +import { SingleMessage, BulkMessage } from './Messages'; const Message = () => { const [isSelectOpen, setIsSelectOpen] = useState(false); @@ -533,6 +69,26 @@ const Message = () => { export default Message; const styles = { + loadingOverlay: { + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999 + }, + spinner: { + width: '50px', + height: '50px', + border: '5px solid #f3f3f3', + borderTop: '5px solid #0542CC', + borderRadius: '50%', + animation: 'spin 1s linear infinite' + }, selectWrapper: { position: 'relative', marginTop: '0', @@ -654,7 +210,7 @@ const styles = { uploadText: { color: '#1f2d3d', fontWeight: '400', - fontSize: '16px', + fontSize: '12px', lineHeight: '13px', }, fileWrapper: { @@ -665,8 +221,8 @@ const styles = { position: 'relative', display: 'flex', alignItems: 'center', - gap: '10px', justifyContent: 'space-between', + height: '65px', }, imageIcon: { color: '#0542cc', diff --git a/src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx b/src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx new file mode 100644 index 0000000..d4242b4 --- /dev/null +++ b/src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx @@ -0,0 +1,554 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronLeft, faCloudUploadAlt, faTimes, faFileExcel } from '@fortawesome/free-solid-svg-icons'; +import { FileUploader } from 'react-drag-drop-files'; +import Select from 'react-select' +import Swal from 'sweetalert2'; +import { ServerDownAnimation } from '../../../../../assets/images'; + +const BASE_URL = process.env.REACT_APP_BASE_URL +const API_KEY = process.env.REACT_APP_API_KEY + +const fileTypes = ['CSV', 'XLSX']; + + +const BulkMessage = ({ isSelectOpen, handleFocus, handleBlur, handleClick, onFileUpload }) => { + const [applicationId, setApplicationId] = useState(''); + const [templateName, setTemplateName] = useState(''); + const [fileData, setFileData] = useState(null); + const [selectedFileName, setSelectedFileName] = useState(''); + const [errors, setErrors] = useState({}); + const fileInputRef = useRef(null); + const [applicationIds, setApplicationIds] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isServer, setIsServer] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [templateOptions, setTemplateOptions] = useState([]); + const [templateId, setTemplateId] = useState(''); + const [selectedQuota, setSelectedQuota] = useState(0); + + useEffect(() => { + const fetchData = () => { + setIsLoading(true); + + fetch(`${BASE_URL}/application/list`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'x-api-key': API_KEY, + }, + }) + .then(response => response.json()) + .then(appData => { + if (appData.status_code === 200) { + setApplicationIds(appData.details.data); + return fetch(`${BASE_URL}/template/list?type=2`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'x-api-key': API_KEY, + }, + }); + } + setIsServer(false); + setErrorMessage('Failed to fetch application IDs'); + throw new Error('Failed to fetch application IDs'); + }) + .then(response => response.json()) + .then(templateData => { + if (templateData.status_code === 200) { + const templates = templateData.details.data.map(template => ({ + value: template.id, + label: template.name, + desc: template.desc, + })); + setTemplateOptions(templates); + setIsServer(true); + } else { + setIsServer(false); + setErrorMessage('Failed to fetch templates'); + throw new Error('Failed to fetch templates'); + } + }) + .catch(error => { + console.error('Error:', error); + setIsServer(false); + setErrorMessage(error.message || 'Server connection failed'); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + fetchData(); + }, []); + + const handleApplicationChange = (e) => { + const selectedId = e.target.value; + const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId)); + + setApplicationId(selectedId); + + if (selectedApp) { + setSelectedQuota(selectedApp.quota || 0); + } + }; + + const handleTemplateChange = (selectedOption) => { + setTemplateId(selectedOption.value); + setTemplateName(selectedOption.label); // Set templateName here + }; + + const handleValidation = () => { + const newErrors = {}; + if (!applicationId) newErrors.applicationId = "Application ID is required."; + if (!templateName) newErrors.templateName = "Template Name is required."; + if (!fileData) newErrors.fileData = "File is required."; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleFileUpload = (file) => { + if (file) { + setFileData(file); + setSelectedFileName(file.name); + if (onFileUpload) { + onFileUpload(file); + } + } else { + setErrors((prevErrors) => ({ + ...prevErrors, + fileData: "File upload failed." + })); + } + }; + + const handleButtonClick = () => { + if (handleValidation()) { + setIsLoading(true); + const formData = new FormData(); + formData.append('application_id', applicationId); + formData.append('template_id', templateId); + formData.append('is_test', 'true'); + formData.append('mode_id', '9'); + formData.append('file', fileData); + + // Log the data being sent + console.log('Form Data:', { + application_id: applicationId, + template_id: templateId, + is_test: 'true', + mode_id: '9', + file: fileData, + }); + + fetch(`${BASE_URL}/wa/bulk-message`, { + method: 'POST', + headers: { + 'x-api-key': API_KEY, + 'accept': 'application/json', + }, + body: formData, + }) + .then(response => response.json()) + .then(data => { + if (data.status_code === 200) { + Swal.fire({ + icon: 'success', + title: 'Success!', + text: 'Bulk message sent successfully', + }); + } else { + Swal.fire({ + icon: 'error', + title: 'Error!', + text: 'Failed to send bulk message', + }); + } + }) + .catch(error => { + console.error('Error:', error); + Swal.fire({ + icon: 'error', + title: 'Error!', + text: 'Failed to send bulk message', + }); + }) + .finally(() => { + setIsLoading(false); + }); + } else { + console.log('Validation errors:', errors); + } + }; + + const handleFileCancel = () => { + setFileData(null); + setSelectedFileName(''); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const downloadTemplate = async () => { + if (templateId) { + const url = `${BASE_URL}/template/download-excel/${templateId}`; + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'x-api-key': API_KEY, + }, + }); + if (!response.ok) { + throw new Error('Failed to download template'); + } + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = `template_${templateName}.xlsx`; + document.body.appendChild(a); + a.click(); + a.remove(); + } catch (error) { + Swal.fire({ + icon: 'error', + title: 'Error', + text: `Failed to download template: ${error.message}`, + }); + } + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Please select a template to download.', + }); + } + }; + + // Server Down Component + if (!isServer) { + return ( +
+ Server Down Animation +

Server tidak dapat diakses

+

{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}

+ +
+ ); + } + + return ( +
+ {/* Inject keyframes for the spinner */} + + + {isLoading && ( +
+
+
+ )} + +
+
+
+ + +
+ {errors.applicationId && {errors.applicationId}} +
+
+

+ Remaining Quota +

+
+ {console.log(selectedQuota)} + {selectedQuota} + (times) +
+
+
+ +
+
+ { + if (e.target.files.length > 0) { + handleFileUpload(e.target.files[0]); + } + }} + /> + {errors.fileData && {errors.fileData}} +
+
+ + {/* Display uploaded file name */} + {selectedFileName && ( +
+
+ +
+
Uploaded File:
+

{selectedFileName}

+
+ +
+
+ )} + +
+
+ +
+
+
+ ); +}; + +export default BulkMessage; + +const styles = { + loadingOverlay: { + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999 + }, + spinner: { + width: '50px', + height: '50px', + border: '5px solid #f3f3f3', + borderTop: '5px solid #0542CC', + borderRadius: '50%', + animation: 'spin 1s linear infinite' + }, + selectWrapper: { + position: 'relative', + marginTop: '0', + }, + select: { + width: '100%', + paddingRight: '30px', + flex: 1, + fontSize: '16px', + outline: 'none', + }, + chevronIcon: { + position: 'absolute', + right: '10px', + top: '50%', + transform: 'translateY(-50%)', + pointerEvents: 'none', + }, + remainingQuota: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + marginTop: '4px', + }, + quotaText: { + fontSize: '40px', + color: '#0542cc', + fontWeight: '600', + }, + timesText: { + marginLeft: '8px', + verticalAlign: 'super', + fontSize: '20px', + }, + submitButton: { + marginLeft: 'auto', + marginTop: '1rem', + textAlign: 'start', + position: 'relative', + zIndex: 1, + }, + uploadArea: { + backgroundColor: '#e6f2ff', + height: '250px', + cursor: 'pointer', + paddingTop: '22px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + border: '1px solid #ced4da', + borderRadius: '0.25rem', + }, + uploadIcon: { + fontSize: '40px', + color: '#0542cc', + marginBottom: '7px', + }, + uploadText: { + color: '#1f2d3d', + fontWeight: '400', + fontSize: '16px', + lineHeight: '13px', + }, + uploadError: { + color: 'red', + }, + fileWrapper: { + backgroundColor: '#fff', + border: '0.2px solid gray', + padding: '15px', + borderRadius: '5px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + height: 'auto', + flexWrap: 'wrap', + }, + imageIcon: { + color: '#0542cc', + fontSize: '24px', + marginBottom: '1rem', + }, + fileDetails: { + flex: 1, + marginLeft: '1rem', + marginRight: '1rem', + marginTop: '0.2rem', + minWidth: '0', // Ensures the text doesn't overflow + }, + uploadedFileTitle: { + fontSize: '1rem', + margin: '0', + wordWrap: 'break-word', // Ensures the text wraps within the container + }, + uploadedFileName: { + fontSize: '0.875rem', + margin: '0', + wordWrap: 'break-word', // Ensures the text wraps within the container + }, + closeButton: { + background: 'none', + border: 'none', + cursor: 'pointer', + }, + closeIcon: { + color: 'red', + fontSize: '26px', + }, + customLabel: { + fontWeight: 600, + fontSize: '14px', + color: '#212529' + }, + submitButton: { + marginTop: '20px', + }, +}; \ No newline at end of file diff --git a/src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx b/src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx new file mode 100644 index 0000000..a763eda --- /dev/null +++ b/src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx @@ -0,0 +1,433 @@ +import React, { useState, useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import Select from 'react-select' +import Swal from 'sweetalert2'; +import { ServerDownAnimation } from '../../../../../assets/images'; + +const BASE_URL = process.env.REACT_APP_BASE_URL +const API_KEY = process.env.REACT_APP_API_KEY + +const SingleMessage = ({ isSelectOpen, handleFocus, handleBlur }) => { + const [applicationId, setApplicationId] = useState(''); + const [phoneId, setPhoneId] = useState(''); + const [preview, setPreview] = useState(null); + const [errors, setErrors] = useState({}); + const [applicationIds, setApplicationIds] = useState([]); + const [selectedQuota, setSelectedQuota] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isServer, setIsServer] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [templateOptions, setTemplateOptions] = useState([]); + const [templateId, setTemplateId] = useState(''); + const [variables, setVariables] = useState([]); + const [desc, setDesc] = useState(''); + + useEffect(() => { + const fetchData = () => { + setIsLoading(true); + + fetch(`${BASE_URL}/application/list`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'x-api-key': API_KEY, + }, + }) + .then(response => response.json()) + .then(appData => { + if (appData.status_code === 200) { + setApplicationIds(appData.details.data); + return fetch(`${BASE_URL}/template/list?type=2`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'x-api-key': API_KEY, + }, + }); + } + setIsServer(false); + setErrorMessage('Failed to fetch application IDs'); + throw new Error('Failed to fetch application IDs'); + }) + .then(response => response.json()) + .then(templateData => { + if (templateData.status_code === 200) { + const templates = templateData.details.data.map(template => ({ + value: template.id, + label: template.name, + desc: template.desc, + })); + setTemplateOptions(templates); + setIsServer(true); + } else { + setIsServer(false); + setErrorMessage('Failed to fetch templates'); + throw new Error('Failed to fetch templates'); + } + }) + .catch(error => { + console.error('Error:', error); + setIsServer(false); + setErrorMessage(error.message || 'Server connection failed'); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + fetchData(); + }, []); + + const handleApplicationChange = (e) => { + const selectedId = e.target.value; + const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId)); + + setApplicationId(selectedId); + + if (selectedApp) { + setSelectedQuota(selectedApp.quota || 0); + } + }; + + + const handleTemplateChange = (selectedOption) => { + setTemplateId(selectedOption.value); + const selectedTemplate = templateOptions.find(template => template.value === selectedOption.value); + const placeholders = selectedTemplate ? selectedTemplate.desc.match(/\{([^}]+)\}/g) || [] : []; + setVariables(Array(placeholders.length).fill('')); + setDesc(selectedTemplate ? selectedTemplate.desc : ''); + }; + + + const handleValidation = () => { + const errors = {}; + if (!applicationId) errors.applicationId = 'Application ID is required.'; + if (!phoneId) errors.phoneId = 'Phone number is required.'; + if (!templateId) errors.templateId = 'Template selection is required.'; + if (variables.some(variable => variable.trim() === '')) errors.variables = 'All variables must be filled.'; + setErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleButtonClick = () => { + if (handleValidation()) { + // Start loading + setIsLoading(true); + + fetch(`${BASE_URL}/wa/message`, { + method: 'POST', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }, + body: JSON.stringify({ + application_id: parseInt(applicationId), + phone: phoneId, + template_id: parseInt(templateId), + is_test: true, + mode_id: 9, + vars: variables, + }), + }) + .then(response => response.json()) + .then(data => { + if (data.status_code === 201 || data.status_code === 200) { + // Decrease the quota + setSelectedQuota(prevQuota => prevQuota - 1); + + // Set preview data + setPreview({ + message: `Message sent successfully!`, + details: `Session ID: ${data.details.data.session_id}\nCreated at: ${new Date( + data.details.data.created_at + ).toLocaleString()}`, + }); + + // Show success alert + Swal.fire({ + icon: 'success', + title: 'Success!', + text: 'Message sent successfully', + }); + } else { + // Show error alert + Swal.fire({ + icon: 'error', + title: 'Error!', + text: 'Failed to send message', + showConfirmButton: true + }); + // Set error message + setErrors({ ...errors, api: 'Failed to send message' }); + } + }) + .catch(error => { + console.error('Error:', error); + setErrors({ ...errors, api: 'Failed to send message' }); + // Show error alert + Swal.fire({ + icon: 'error', + title: 'Error!', + text: 'Failed to send message', + showConfirmButton: true + }); + }) + .finally(() => { + // Stop loading + setIsLoading(false); + }); + } + }; + + // Server Down Component + if (!isServer) { + return ( +
+ Server Down Animation +

Server tidak dapat diakses

+

{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}

+ +
+ ); + } + + return ( +
+ {/* Inject keyframes for the spinner */} + + + {isLoading && ( +
+
+
+ )} +
+
+
+ + +
+ {errors.applicationId &&

{errors.applicationId}

} +
+
+

+ Remaining Quota +

+
+ {console.log(selectedQuota)} + {selectedQuota} + (times) +
+
+
+ +
+
+
+ + Phone Number + + { + const value = e.target.value; + // Allow only digits and enforce length constraints + if (/^\d*$/.test(value) && value.length <= 14) { + setPhoneId(value); + } + }} + minLength={9} + maxLength={14} + /> +
+ {errors.phoneId &&

{errors.phoneId}

} +
+ +
+ { + const newVars = [...variables]; + newVars[index] = e.target.value; + setVariables(newVars); + }} + /> +
+
+ ))} +
+ + + +
+ +
+ + {/* Preview Section */} + {preview && ( +
+
+

[Sips & Synergy Executive]

+

{preview.message}

+

{preview.details}

+ +
+
+ )} + + ); +}; + +export default SingleMessage; + +const styles = { + loadingOverlay: { + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999 + }, + spinner: { + width: '50px', + height: '50px', + border: '5px solid #f3f3f3', + borderTop: '5px solid #0542CC', + borderRadius: '50%', + animation: 'spin 1s linear infinite' + }, + selectWrapper: { + position: 'relative', + marginTop: '0', + }, + select: { + width: '100%', + paddingRight: '30px', + flex: 1, + fontSize: '16px', + outline: 'none', + }, + chevronIcon: { + position: 'absolute', + right: '10px', + top: '50%', + transform: 'translateY(-50%)', + pointerEvents: 'none', + }, + remainingQuota: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + marginTop: '4px', + }, + quotaText: { + fontSize: '40px', + color: '#0542cc', + fontWeight: '600', + }, + timesText: { + marginLeft: '8px', + verticalAlign: 'super', + fontSize: '20px', + }, + submitButton: { + marginLeft: 'auto', + marginTop: '1rem', + textAlign: 'start', + position: 'relative', + zIndex: 1, + }, + waMessagePreview: { + backgroundColor: '#F8F9FA', + padding: '15px', + borderRadius: '8px', + border: '1px solid #dcdcdc', + marginBottom: '20px', + }, + preview: { + marginTop: '20px', + }, +}; diff --git a/src/screens/Wa/Verify/Section/Messages/index.js b/src/screens/Wa/Verify/Section/Messages/index.js new file mode 100644 index 0000000..cae3b10 --- /dev/null +++ b/src/screens/Wa/Verify/Section/Messages/index.js @@ -0,0 +1,7 @@ +import SingleMessage from "./SingleMessage"; +import BulkMessage from "./BulkMessage"; + +export { + SingleMessage, + BulkMessage +} \ No newline at end of file
No. - - - - - - - - @@ -377,15 +391,10 @@ const Transaction = () => { {paginatedData.length > 0 ? ( paginatedData.map((transaction, index) => (
{(currentPage - 1) * dataPerPage + index + 1}{transaction.transactionId}{transaction.applicationName}{transaction.dataSent}{transaction.endPoint}{transaction.subjectId}{transaction.serviceCharged}{transaction.transaction_id}{transaction.application_name}{transaction.endpoint}{transaction.subject_id || 'N/A'} {transaction.mode} {transaction.status}