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 (
<>
-
{
+ 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 = () => {
- No. | {/* Kolom untuk Nomor Urut */}
- |
- handleSort('applicationName')}>
+ handleSort('application_name')}>
Application Name
- {sortConfig.key === 'applicationName' &&
+ {sortConfig.key === 'application_name' &&
(sortConfig.direction === 'asc' ? : )
}
- {sortConfig.key !== 'applicationName' && }
+ {sortConfig.key !== 'application_name' && }
|
- handleSort('dataSent')}>
- Data Sent
- {sortConfig.key === 'dataSent' &&
- (sortConfig.direction === 'asc' ? : )
- }
- {sortConfig.key !== 'dataSent' && }
-
- |
-
- handleSort('endPoint')}>
+ handleSort('endpoint')}>
End Point
- {sortConfig.key === 'endPoint' &&
+ {sortConfig.key === 'endpoint' &&
(sortConfig.direction === 'asc' ? : )
}
- {sortConfig.key !== 'endPoint' && }
+ {sortConfig.key !== 'endpoint' && }
|
- handleSort('subjectId')}>
+ handleSort('subject_id')}>
Subject ID
- {sortConfig.key === 'subjectId' &&
+ {sortConfig.key === 'subject_id' &&
(sortConfig.direction === 'asc' ? : )
}
- {sortConfig.key !== 'subjectId' && }
-
- |
-
- handleSort('serviceCharged')}>
- Service Charged
- {sortConfig.key === 'serviceCharged' &&
- (sortConfig.direction === 'asc' ? : )
- }
- {sortConfig.key !== 'serviceCharged' && }
+ {sortConfig.key !== 'subject_id' && }
|
@@ -377,15 +391,10 @@ const Transaction = () => {
{paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => (
|
- {/* Kolom Nomor Urut */}
- {(currentPage - 1) * dataPerPage + index + 1} | {/* Nomor urut berdasarkan halaman dan index */}
-
- {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} |
@@ -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}
-
Maps Event Location
-
-
- )}
-
- );
-};
-
-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 */}
-
-
- {/* 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 tidak dapat diakses
+
{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}
+
window.location.reload()}
+ style={{
+ padding: '10px 20px',
+ backgroundColor: '#0542cc',
+ color: '#fff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer'
+ }}>
+ Coba Lagi
+
+
+ );
+ }
+
+ return (
+
+ {/* Inject keyframes for the spinner */}
+
+
+ {isLoading && (
+
+ )}
+
+
+
+
+
+
+
+ {errors.applicationId &&
{errors.applicationId}}
+
+
+
+ Remaining Quota
+
+
+ {console.log(selectedQuota)}
+ {selectedQuota}
+ (times)
+
+
+
+
+
+
+
+
+
+ {/* Upload Section */}
+
+
+
+
+ Download Template
+
+
{
+ // Ensure files array is not empty
+ if (files.length > 0) {
+ handleFileUpload(files[0]);
+ }
+ }}
+ // Modify the FileUploader children prop to prevent double file dialog
+ children={
+
+
+
Drag and Drop Here
+
Or
+ {/* Remove the onClick handler here since it's causing the double trigger */}
+
Browse
+
Supported file types: CSV, XLSX
+
+ }
+
+ />
+ {
+ 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 tidak dapat diakses
+
{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}
+
window.location.reload()}
+ style={{
+ padding: '10px 20px',
+ backgroundColor: '#0542cc',
+ color: '#fff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer'
+ }}>
+ Coba Lagi
+
+
+ );
+ }
+
+ 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}
}
+
+
+
+
+
+
+ {/* Dynamic Variable Inputs */}
+
+ {variables.map((_, index) => (
+
+
+ {
+ const newVars = [...variables];
+ newVars[index] = e.target.value;
+ setVariables(newVars);
+ }}
+ />
+
+
+ ))}
+
+
+
+
+
+
+ {/* Preview Section */}
+ {preview && (
+
+
+
[Sips & Synergy Executive]
+
{preview.message}
+
{preview.details}
+
Maps Event Location
+
+
+ )}
+
+ );
+};
+
+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