Wa-Message

This commit is contained in:
Rizqika 2024-12-31 11:33:31 +07:00
parent 2f97160893
commit e8cde416a5
15 changed files with 1201 additions and 583 deletions

1
Backend/rekan_veri_be Submodule

@ -0,0 +1 @@
Subproject commit b7bb5e81c38e443cca46785e23e8785f3a2cb3f5

39
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-keywords": "^5.1.0", "ajv-keywords": "^5.1.0",
"axios": "^1.7.9",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"react": "^18.3.1", "react": "^18.3.1",
@ -27,6 +28,7 @@
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-select": "^5.8.2", "react-select": "^5.8.2",
"sweetalert2": "^11.15.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@ -5135,6 +5137,29 @@
"node": ">=4" "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": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -13351,6 +13376,11 @@
"node": ">= 0.10" "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": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@ -15476,6 +15506,15 @@
"node": ">=4" "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": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",

View File

@ -11,6 +11,7 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ajv-keywords": "^5.1.0", "ajv-keywords": "^5.1.0",
"axios": "^1.7.9",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"react": "^18.3.1", "react": "^18.3.1",
@ -22,6 +23,7 @@
"react-router-dom": "^6.28.0", "react-router-dom": "^6.28.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-select": "^5.8.2", "react-select": "^5.8.2",
"sweetalert2": "^11.15.3",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@ -4,7 +4,7 @@ import DeepMenu from './DeepMenu';
const SubMenu = ({ heading, target, iconClass, subMenus, activeMenu, onMenuClick }) => { const SubMenu = ({ heading, target, iconClass, subMenus, activeMenu, onMenuClick }) => {
return ( return (
<> <>
<div className="sb-sidenav-menu-heading">{heading}</div> {/* <div className="sb-sidenav-menu-heading">{heading}</div> */}
<a <a
className={`nav-link collapsed ${activeMenu === heading ? 'active' : ''}`} // Menggunakan kelas active className={`nav-link collapsed ${activeMenu === heading ? 'active' : ''}`} // Menggunakan kelas active
href="#" href="#"

View File

@ -0,0 +1,5 @@
const CustomLabel = ({ children, ...props }) => {
return <label {...props}>{children}</label>;
};
export default CustomLabel;

View File

@ -3,9 +3,12 @@ import Sidebar from "./Sidebar/Sidebar";
import Main from "./Main"; import Main from "./Main";
import Footer from "./Footer"; import Footer from "./Footer";
import CustomLabel from "./common/CustomLabel";
export { export {
Navbar, Navbar,
Sidebar, Sidebar,
Main, Main,
Footer, Footer,
CustomLabel
} }

View File

@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import { faTimes, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
import Select from 'react-select' import Select from 'react-select'
import { ServerDownAnimation } from '../../../../assets/images'; import { ServerDownAnimation } from '../../../../assets/images';
import { CustomLabel} from '../../../../components';
const Enroll = () => { 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 (
<label {...props}>
{children}
</label>
);
};
// Handle Server Down // Handle Server Down
if (!isServer) { if (!isServer) {

View File

@ -4,6 +4,7 @@ import { faChevronLeft, faChevronDown, faTimes, faImage } from '@fortawesome/fr
import { FileUploader } from 'react-drag-drop-files'; import { FileUploader } from 'react-drag-drop-files';
import Select from 'react-select' import Select from 'react-select'
import { ServerDownAnimation } from '../../../../assets/images'; import { ServerDownAnimation } from '../../../../assets/images';
import { CustomLabel} from '../../../../components';
const fileTypes = ["JPG", "JPEG"]; // Allowed file types 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 (
<label {...props}>
{children}
</label>
);
};
if (!isServer) { if (!isServer) {
return ( return (
<div style={{ textAlign: 'center', marginTop: '50px' }}> <div style={{ textAlign: 'center', marginTop: '50px' }}>

View File

@ -4,6 +4,7 @@ import { faChevronDown, faTimes } from '@fortawesome/free-solid-svg-icons';
import Select from 'react-select' import Select from 'react-select'
import { ServerDownAnimation } from '../../../../assets/images'; import { ServerDownAnimation } from '../../../../assets/images';
import { FileUploader } from 'react-drag-drop-files'; import { FileUploader } from 'react-drag-drop-files';
import { CustomLabel} from '../../../../components';
const Verify = () => { const Verify = () => {
const BASE_URL = process.env.REACT_APP_BASE_URL; 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 (
<label {...props}>
{children}
</label>
);
};
const handleInputChangeApplication = (newInputValue) => { const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID // Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) { if (newInputValue.length <= 15) {

View File

@ -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 { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
import { NoAvailable } from '../../../assets/icon'; 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 // Pagination Component
const Pagination = ({ currentPage, totalPages, onPageChange }) => { const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const handlePrev = () => { const handlePrev = () => {
@ -106,6 +108,10 @@ const Transaction = () => {
const [transactionData, setTransactionData] = useState([]); const [transactionData, setTransactionData] = useState([]);
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
const dataPerPage = 10; // Data per page (10 data per page) 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 = [ const buttonData = [
{ label: 'Copy', enabled: true }, { label: 'Copy', enabled: true },
@ -116,31 +122,51 @@ const Transaction = () => {
{ label: 'Column Visibility', enabled: true }, { label: 'Column Visibility', enabled: true },
]; ];
// Fetch transaction data from API
// Generate 691 dummy transactions const fetchTransactionData = async (page, limit, search = '', sortColumn = 'tf.id', sortOrder = 'asc') => {
const generateDummyData = (numOfItems) => { try {
const transactionData = []; 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}`, {
for (let i = 1; i <= numOfItems; i++) { method: 'GET',
transactionData.push({ headers: { 'accept': 'application/json', 'x-api-key': API_KEY },
transactionId: `TX${String(i).padStart(3, '0')}`, });
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, if (!response.ok) {
dataSent: `${Math.floor(Math.random() * 100) + 50}MB`, throw new Error(`HTTP error! status: ${response.status}`);
endPoint: `Endpoint ${Math.floor(Math.random() * 5) + 1}`, }
subjectId: `S${String(i).padStart(3, '0')}`, const result = await response.json();
serviceCharged: `$${(Math.random() * 50 + 5).toFixed(2)}`, const { data, total } = result.details.details.data;
mode: Math.random() > 0.5 ? 'Online' : 'Offline', setTransactionData(data);
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], setTotalPages(Math.ceil(total / dataPerPage));
}); } catch (error) {
} console.error('Error fetching transaction data:', error);
}
return transactionData;
}; };
// 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(() => { useEffect(() => {
setTransactionData(generateDummyData(97513)); // count data dummy transactions fetchTransactionData(currentPage, dataPerPage, searchTerm, sortColumn, sortOrder);
}, []); fetchPaginationData(currentPage, dataPerPage);
}, [currentPage, searchTerm, sortColumn, sortOrder]);
// Sorting function // Sorting function
const sortData = (data, config) => { const sortData = (data, config) => {
@ -178,8 +204,14 @@ const Transaction = () => {
setCurrentPage(page); setCurrentPage(page);
}; };
// Calculate total pages based on the data and data per page // Handle search
const totalPages = Math.ceil(transactionData.length / dataPerPage); 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 // Paginated data
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage); const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
@ -279,11 +311,12 @@ const Transaction = () => {
</div> </div>
{/* Search Bar with Icon */} {/* Search Bar with Icon */}
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}> <div className="input-group" style={{ width: '350px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search Application Name or Subject ID" // Placeholder text
className="form-control" className="form-control"
onChange={handleSearch} // Add onChange handler
/> />
<span className="input-group-text"> <span className="input-group-text">
<i className="fas fa-search"></i> {/* FontAwesome search icon */} <i className="fas fa-search"></i> {/* FontAwesome search icon */}
@ -297,59 +330,40 @@ const Transaction = () => {
<table className="table table-bordered" style={styles.tableContainer}> <table className="table table-bordered" style={styles.tableContainer}>
<thead> <thead>
<tr> <tr>
<th>No.</th> {/* Kolom untuk Nomor Urut */}
<th> <th>
<button className="btn" onClick={() => handleSort('transactionId')}> <button className="btn" onClick={() => handleSort('transaction_id')}>
Transaction ID Transaction ID
{sortConfig.key === 'transactionId' && {sortConfig.key === 'transaction_id' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />) (sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
} }
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />} {sortConfig.key !== 'transaction_id' && <FaSort style={styles.iconMarginLeft} />}
</button> </button>
</th> </th>
<th> <th>
<button className="btn" onClick={() => handleSort('applicationName')}> <button className="btn" onClick={() => handleSort('application_name')}>
Application Name Application Name
{sortConfig.key === 'applicationName' && {sortConfig.key === 'application_name' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />) (sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
} }
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />} {sortConfig.key !== 'application_name' && <FaSort style={styles.iconMarginLeft} />}
</button> </button>
</th> </th>
<th> <th>
<button className="btn" onClick={() => handleSort('dataSent')}> <button className="btn" onClick={() => handleSort('endpoint')}>
Data Sent
{sortConfig.key === 'dataSent' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'dataSent' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('endPoint')}>
End Point End Point
{sortConfig.key === 'endPoint' && {sortConfig.key === 'endpoint' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />) (sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
} }
{sortConfig.key !== 'endPoint' && <FaSort style={styles.iconMarginLeft} />} {sortConfig.key !== 'endpoint' && <FaSort style={styles.iconMarginLeft} />}
</button> </button>
</th> </th>
<th> <th>
<button className="btn" onClick={() => handleSort('subjectId')}> <button className="btn" onClick={() => handleSort('subject_id')}>
Subject ID Subject ID
{sortConfig.key === 'subjectId' && {sortConfig.key === 'subject_id' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />) (sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
} }
{sortConfig.key !== 'subjectId' && <FaSort style={styles.iconMarginLeft} />} {sortConfig.key !== 'subject_id' && <FaSort style={styles.iconMarginLeft} />}
</button>
</th>
<th>
<button className="btn" onClick={() => handleSort('serviceCharged')}>
Service Charged
{sortConfig.key === 'serviceCharged' &&
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
}
{sortConfig.key !== 'serviceCharged' && <FaSort style={styles.iconMarginLeft} />}
</button> </button>
</th> </th>
<th> <th>
@ -377,15 +391,10 @@ const Transaction = () => {
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((transaction, index) => ( paginatedData.map((transaction, index) => (
<tr key={index}> <tr key={index}>
{/* Kolom Nomor Urut */} <td>{transaction.transaction_id}</td>
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */} <td>{transaction.application_name}</td>
<td>{transaction.endpoint}</td>
<td>{transaction.transactionId}</td> <td>{transaction.subject_id || 'N/A'}</td>
<td>{transaction.applicationName}</td>
<td>{transaction.dataSent}</td>
<td>{transaction.endPoint}</td>
<td>{transaction.subjectId}</td>
<td>{transaction.serviceCharged}</td>
<td>{transaction.mode}</td> <td>{transaction.mode}</td>
<td>{transaction.status}</td> <td>{transaction.status}</td>
</tr> </tr>
@ -440,4 +449,3 @@ const styles = {
marginLeft: '0.7rem', // Adjust as needed marginLeft: '0.7rem', // Adjust as needed
}, },
}; };

View File

@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react' import React, { useState, useRef, useEffect } from 'react'
import { ServerDownAnimation } from '../../../../assets/images'; import { ServerDownAnimation } from '../../../../assets/images';
import Select from 'react-select' import Select from 'react-select'
import Swal from 'sweetalert2';
const BASE_URL = process.env.REACT_APP_BASE_URL const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY const API_KEY = process.env.REACT_APP_API_KEY
@ -38,15 +39,32 @@ const Verify = ({ onVerify, generateId }) => {
.then(data => { .then(data => {
if (data.status_code === 201) { if (data.status_code === 201) {
setError(''); // Clear any existing error message 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 onVerify(); // Trigger verify callback
} else { } else {
setError(data.details.message || "Verification failed"); setError(data.details.message || "Verification failed");
// Show error alert
Swal.fire({
icon: 'error',
title: 'Error',
text: data.details.message || "Verification failed",
});
} }
}) })
.catch(error => { .catch(error => {
console.error('Verification failed:', error); console.error('Verification failed:', error);
setError("Failed to verify OTP"); setError("Failed to verify OTP");
// Show error alert
Swal.fire({
icon: 'error',
title: 'Error',
text: "Failed to verify OTP",
});
}); });
}; };
@ -183,13 +201,13 @@ const Auth = () => {
const [generateId, setgenerateId] = useState(''); const [generateId, setgenerateId] = useState('');
const handleApplicationChange = (selectedOption) => { const handleApplicationChange = (selectedOption) => {
const selectedId = selectedOption.value; const selectedId = selectedOption?.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId)); const selectedApp = applicationIds.find(app => app.id === selectedId);
setApplicationId(selectedOption ? selectedOption.value : ''); setApplicationId(selectedId);
if (selectedApp) { if (selectedApp) {
setSelectedQuota(selectedApp.quota); setSelectedQuota(selectedApp.quota || 0);
} }
}; };
@ -203,6 +221,7 @@ const Auth = () => {
setInputValueApplication(newInputValue); setInputValueApplication(newInputValue);
} }
}; };
useEffect(() => { useEffect(() => {
const fetchData = () => { const fetchData = () => {
setIsLoading(true); setIsLoading(true);
@ -284,6 +303,7 @@ const Auth = () => {
</div> </div>
); );
} }
const handleVerify = () => { const handleVerify = () => {
setShowPreview(true); // Show the preview when verified setShowPreview(true); // Show the preview when verified
}; };
@ -336,12 +356,27 @@ const Auth = () => {
console.log('API Response:', data); console.log('API Response:', data);
if (data.status_code === 201 && data.details.message === "Successfully") { if (data.status_code === 201 && data.details.message === "Successfully") {
setgenerateId(data.details.data.id); setgenerateId(data.details.data.id);
setShowVerify(false); setShowVerify(true);
alert('Authentication request successful!'); Swal.fire({
icon: 'success',
title: 'Success',
text: 'Authentication request successful!',
});
} else {
Swal.fire({
icon: 'error',
title: 'Error',
text: 'Failed to authenticate.',
});
} }
}) })
.catch(error => { .catch(error => {
console.error('Request failed:', error); console.error('Request failed:', error);
Swal.fire({
icon: 'error',
title: 'Error',
text: `Failed to authenticate: ${error.message}`,
});
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
@ -349,7 +384,6 @@ const Auth = () => {
} }
}; };
return ( return (
<> <>
<div style={styles.section}> <div style={styles.section}>
@ -494,10 +528,10 @@ const Auth = () => {
</div> </div>
{showVerify && <Verify phoneId={phoneId} onVerify={handleVerify} generateId={generateId} />} {showVerify && <Verify phoneId={phoneId} onVerify={handleVerify} generateId={generateId} />}
<Preview otpLength={otpId} /> {showPreview && <Preview otpLength={otpId} />}
</> </>
); );
} };
export default Auth; export default Auth;

View File

@ -1,470 +1,6 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef } 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 { SingleMessage, BulkMessage } from './Messages';
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 <label {...props}>{children}</label>;
};
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 (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <p className="text-danger">{errors.applicationId}</p>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>0</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">+62</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => {
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}
/>
</div>
{errors.phoneId && <p className="text-danger">{errors.phoneId}</p>}
</div>
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="variable_1"
className="form-control"
placeholder="Variables 1"
value={variable_1}
onChange={(e) => setVariable_1(e.target.value)}
maxLength={15}
/>
</div>
{errors.variable_1 && <p className="text-danger">{errors.variable_1}</p>}
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="templateName"
className="form-control"
placeholder="Template Name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
maxLength={15}
/>
</div>
{errors.templateName && <p className="text-danger">{errors.templateName}</p>}
</div>
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="variable_2"
className="form-control"
placeholder="Variables 2"
value={variable_2}
onChange={(e) => setVariable_2(e.target.value)}
maxLength={15}
/>
</div>
{errors.variable_2 && <p className="text-danger">{errors.variable_2}</p>}
</div>
</div>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
{/* Preview Section */}
{preview && (
<div style={styles.preview}>
<div style={styles.waMessagePreview}>
<p><strong>[Sips & Synergy Executive]</strong></p>
<p>{preview.message}</p>
<p>{preview.details}</p>
<button className="btn btn-primary mt-3">Maps Event Location</button>
</div>
</div>
)}
</div>
);
};
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 (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <p className="text-danger">{errors.applicationId}</p>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>0</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<input
type="text"
id="templateName"
className="form-control"
placeholder="Template Name"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
maxLength={15}
/>
</div>
{errors.templateName && <p className="text-danger">{errors.templateName}</p>}
</div>
</div>
{/* Upload Section */}
<div className='col-md-6'>
<div className="row form-group mt-4">
<CustomLabel htmlFor="uploadPhoto" style={styles.customLabel}>
Upload Face Photo
</CustomLabel>
<FileUploader
handleChange={handleImageUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => {
// Ensure files array is not empty
if (files.length > 0) {
handleImageUpload(files[0]);
}
}}
children={
<div style={styles.uploadArea}>
<FontAwesomeIcon icon={faCloudUploadAlt} style={styles.uploadIcon} />
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
<a href="#" onClick={() => fileInputRef.current.click()}>Browse</a>
<p className="text-muted">Recommended size: 300x300 (Max File Size: 2MB)</p>
<p className="text-muted">Supported file types: JPG, JPEG</p>
</div>
}
/>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept="image/jpeg, image/jpg"
onChange={e => {
if (e.target.files.length > 0) {
handleImageUpload(e.target.files[0]);
}
}}
/>
{errors.imageData && <small style={{ color: 'red' }}>{errors.imageData}</small>}
</div>
</div>
{/* Display uploaded image name */}
{selectedImageName && (
<div className="col-md-6 mt-4">
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faImage} style={styles.imageIcon} />
<div style={{ marginRight: '18rem', marginTop: '0.2rem' }}>
<h5>Uploaded File:</h5>
<p>{selectedImageName}</p>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
onClick={handleImageCancel}
aria-label="Remove uploaded image"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
</div>
)}
<div style={styles.selectWrapper}>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
</div>
</div>
);
};
const Message = () => { const Message = () => {
const [isSelectOpen, setIsSelectOpen] = useState(false); const [isSelectOpen, setIsSelectOpen] = useState(false);
@ -533,6 +69,26 @@ const Message = () => {
export default Message; export default Message;
const styles = { 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: { selectWrapper: {
position: 'relative', position: 'relative',
marginTop: '0', marginTop: '0',
@ -654,7 +210,7 @@ const styles = {
uploadText: { uploadText: {
color: '#1f2d3d', color: '#1f2d3d',
fontWeight: '400', fontWeight: '400',
fontSize: '16px', fontSize: '12px',
lineHeight: '13px', lineHeight: '13px',
}, },
fileWrapper: { fileWrapper: {
@ -665,8 +221,8 @@ const styles = {
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '10px',
justifyContent: 'space-between', justifyContent: 'space-between',
height: '65px',
}, },
imageIcon: { imageIcon: {
color: '#0542cc', color: '#0542cc',

View File

@ -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 (
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<img
src={ServerDownAnimation}
alt="Server Down Animation"
style={{ width: '18rem', height: '18rem', marginBottom: '20px' }}
/>
<h2 style={{ color: 'red' }}>Server tidak dapat diakses</h2>
<p>{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}>
Coba Lagi
</button>
</div>
);
}
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={handleApplicationChange}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <small style={{ color: 'red' }}>{errors.applicationId}</small>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
{console.log(selectedQuota)}
<span style={styles.quotaText}>{selectedQuota}</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<Select
id="templateId"
value={templateOptions.find(option => option.value === templateId)}
onChange={handleTemplateChange}
options={templateOptions}
placeholder="Select Template"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
/>
{errors.templateName && <small style={{ color: 'red' }}>{errors.templateName}</small>}
</div>
</div>
{/* Upload Section */}
<div className='col-md-6'>
<div className="row form-group mt-4">
<div style={{ display: 'flex', alignItems: 'center' }}>
<label htmlFor="uploadFile" style={styles.customLabel}>
Upload File (CSV or Excel)
</label>
<button onClick={downloadTemplate} className="btn btn-link">Download Template</button>
</div>
<FileUploader
handleChange={handleFileUpload}
name="file"
types={fileTypes}
multiple={false}
onDrop={(files) => {
// Ensure files array is not empty
if (files.length > 0) {
handleFileUpload(files[0]);
}
}}
// Modify the FileUploader children prop to prevent double file dialog
children={
<div style={styles.uploadArea}>
<FontAwesomeIcon icon={faCloudUploadAlt} style={styles.uploadIcon} />
<p style={styles.uploadText}>Drag and Drop Here</p>
<p>Or</p>
{/* Remove the onClick handler here since it's causing the double trigger */}
<span style={{ color: '#0542CC', cursor: 'pointer' }}>Browse</span>
<p className="text-muted">Supported file types: CSV, XLSX</p>
</div>
}
/>
<input
type="file"
id="fileUpload"
ref={fileInputRef}
style={{ display: 'none' }}
accept=".csv, .xlsx"
onChange={e => {
if (e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
}}
/>
{errors.fileData && <small style={{ color: 'red' }}>{errors.fileData}</small>}
</div>
</div>
{/* Display uploaded file name */}
{selectedFileName && (
<div className="col-md-6 mt-4">
<div style={styles.fileWrapper}>
<FontAwesomeIcon icon={faFileExcel} style={styles.imageIcon} />
<div style={styles.fileDetails}>
<h5 style={styles.uploadedFileTitle}>Uploaded File:</h5>
<p style={styles.uploadedFileName}>{selectedFileName}</p>
</div>
<button
style={styles.closeButton}
onClick={handleFileCancel}
aria-label="Remove uploaded file"
>
<FontAwesomeIcon
icon={faTimes}
style={styles.closeIcon}
/>
</button>
</div>
</div>
)}
<div style={styles.selectWrapper}>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
</div>
</div>
);
};
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',
},
};

View File

@ -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 (
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<img
src={ServerDownAnimation}
alt="Server Down Animation"
style={{ width: '18rem', height: '18rem', marginBottom: '20px' }}
/>
<h2 style={{ color: 'red' }}>Server tidak dapat diakses</h2>
<p>{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}>
Coba Lagi
</button>
</div>
);
}
return (
<div>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
<div className="form-group row align-items-center">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<select
id="applicationId"
className="form-control"
style={styles.select}
value={applicationId}
onChange={handleApplicationChange}
onFocus={handleFocus}
onBlur={handleBlur}
>
<option value="">Select Application ID</option>
{applicationIds.map((app) => (
<option key={app.id} value={app.id}>
{app.name}
</option>
))}
</select>
<FontAwesomeIcon
icon={isSelectOpen ? faChevronDown : faChevronLeft}
style={styles.chevronIcon}
/>
</div>
{errors.applicationId && <p className="text-danger">{errors.applicationId}</p>}
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
{console.log(selectedQuota)}
<span style={styles.quotaText}>{selectedQuota}</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">Phone Number</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => {
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}
/>
</div>
{errors.phoneId && <p className="text-danger">{errors.phoneId}</p>}
</div>
<div className="col-md-6">
<Select
id="templateId"
value={templateOptions.find(option => option.value === templateId)}
onChange={handleTemplateChange}
options={templateOptions}
placeholder="Select Template"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
/>
{errors.templateId && <p className="text-danger">{errors.templateId}</p>}
{/* Description Preview */}
{desc && (
<div className="alert alert-info">
<strong>Selected Template Description:</strong> {desc}
</div>
)}
</div>
</div>
{/* Dynamic Variable Inputs */}
<div className="form-group row mt-4">
{variables.map((_, index) => (
<div className="col-md-6 mb-3" key={index}>
<div className="input-group">
<input
type="text"
className="form-control"
placeholder={`Variable ${index + 1}`}
value={variables[index]}
onChange={(e) => {
const newVars = [...variables];
newVars[index] = e.target.value;
setVariables(newVars);
}}
/>
</div>
</div>
))}
</div>
<div style={styles.submitButton}>
<button onClick={handleButtonClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make SMS Demo</p>
</button>
</div>
{/* Preview Section */}
{preview && (
<div style={styles.preview}>
<div style={styles.waMessagePreview}>
<p><strong>[Sips & Synergy Executive]</strong></p>
<p>{preview.message}</p>
<p>{preview.details}</p>
<button className="btn btn-primary mt-3">Maps Event Location</button>
</div>
</div>
)}
</div>
);
};
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',
},
};

View File

@ -0,0 +1,7 @@
import SingleMessage from "./SingleMessage";
import BulkMessage from "./BulkMessage";
export {
SingleMessage,
BulkMessage
}