Wa-Message
This commit is contained in:
parent
2f97160893
commit
e8cde416a5
1
Backend/rekan_veri_be
Submodule
1
Backend/rekan_veri_be
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit b7bb5e81c38e443cca46785e23e8785f3a2cb3f5
|
39
package-lock.json
generated
39
package-lock.json
generated
@ -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",
|
||||||
|
@ -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": {
|
||||||
|
@ -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="#"
|
||||||
|
5
src/components/common/CustomLabel.jsx
Normal file
5
src/components/common/CustomLabel.jsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const CustomLabel = ({ children, ...props }) => {
|
||||||
|
return <label {...props}>{children}</label>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomLabel;
|
@ -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
|
||||||
}
|
}
|
@ -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) {
|
||||||
|
@ -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' }}>
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
554
src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx
Normal file
554
src/screens/Wa/Verify/Section/Messages/BulkMessage.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
433
src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx
Normal file
433
src/screens/Wa/Verify/Section/Messages/SingleMessage.jsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
7
src/screens/Wa/Verify/Section/Messages/index.js
Normal file
7
src/screens/Wa/Verify/Section/Messages/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import SingleMessage from "./SingleMessage";
|
||||||
|
import BulkMessage from "./BulkMessage";
|
||||||
|
|
||||||
|
export {
|
||||||
|
SingleMessage,
|
||||||
|
BulkMessage
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user