Slicing UI
This commit is contained in:
parent
231c637fa9
commit
392a8a08fb
29
package-lock.json
generated
29
package-lock.json
generated
@ -19,6 +19,7 @@
|
||||
"bootstrap": "^5.3.3",
|
||||
"font-awesome": "^4.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-drop-files": "^2.4.0",
|
||||
"react-icons": "^5.3.0",
|
||||
@ -5973,6 +5974,15 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/copy-to-clipboard": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
|
||||
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.39.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||
@ -13344,6 +13354,19 @@
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"node_modules/react-copy-to-clipboard": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || 16 || 17 || 18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||
@ -15541,6 +15564,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"bootstrap": "^5.3.3",
|
||||
"font-awesome": "^4.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-drop-files": "^2.4.0",
|
||||
"react-icons": "^5.3.0",
|
||||
|
142
src/App.js
142
src/App.js
@ -13,53 +13,101 @@ import {
|
||||
CreateApps
|
||||
} from './screens/Home';
|
||||
import {
|
||||
FaceVerify,
|
||||
FaceSummary,
|
||||
FaceTransaction
|
||||
Verify as FaceVerify,
|
||||
Summary as FaceSummary,
|
||||
Transaction as FaceTransaction
|
||||
} from './screens/Biometric/FaceRecognition';
|
||||
|
||||
import {
|
||||
Enroll,
|
||||
VerifySection,
|
||||
Verify as VerifySection,
|
||||
Liveness,
|
||||
Compare,
|
||||
Search
|
||||
} from './screens/Biometric/FaceRecognition/Section';
|
||||
|
||||
import {
|
||||
ManageKtp,
|
||||
SummaryKtp,
|
||||
VerifyKtp,
|
||||
TransactionKtp
|
||||
Manage as ManageKtp,
|
||||
Summary as SummaryKtp,
|
||||
Verify as VerifyKtp,
|
||||
Transaction as TransactionKtp
|
||||
} from './screens/Biometric/OcrKtp';
|
||||
|
||||
import {
|
||||
SummaryNpwp,
|
||||
TransactionNpwp,
|
||||
VerifyNpwp
|
||||
Summary as SummaryNpwp,
|
||||
Transaction as TransactionNpwp,
|
||||
Verify as VerifyNpwp
|
||||
} from './screens/Biometric/OcrNpwp';
|
||||
|
||||
import {
|
||||
VerifySim,
|
||||
SummarySim,
|
||||
TransactionSim
|
||||
Verify as VerifySim,
|
||||
Summary as SummarySim,
|
||||
Transaction as TransactionSim
|
||||
} from './screens/Biometric/OcrSim';
|
||||
|
||||
import {
|
||||
VerifyDoc,
|
||||
SummaryDoc,
|
||||
TransactionDoc
|
||||
Verify as VerifyDoc,
|
||||
Summary as SummaryDoc,
|
||||
Transaction as TransactionDoc
|
||||
} from './screens/Biometric/OcrDocument';
|
||||
|
||||
import {
|
||||
SmsVerify
|
||||
Verify as SmsVerify
|
||||
} from './screens/Sms/Verification';
|
||||
|
||||
import {
|
||||
VerificationAnnoncement,
|
||||
VerificationOtp
|
||||
Annoncement as VerificationAnnoncement,
|
||||
Otp as VerificationOtp
|
||||
} from './screens/Sms/Verification/Section';
|
||||
|
||||
import {
|
||||
Settings as SmsOtpSettings,
|
||||
Summary as SmsOtpSummary,
|
||||
Transaction as SmsOtpTransaction,
|
||||
Detail as SmsOtpDetail
|
||||
} from './screens/Sms/OtpManagement';
|
||||
|
||||
import {
|
||||
Transaction as SmsOtpSummaryTransaction,
|
||||
Announcement as SmsOtpSummaryAnnouncement
|
||||
} from './screens/Sms/OtpManagement/Section';
|
||||
|
||||
import {
|
||||
BulkMessage as SmsAnnouncementBulk,
|
||||
Summary as SmsAnnouncementSummary,
|
||||
Transaction as SmsAnnouncementTransaction
|
||||
} from './screens/Sms/Announcements';
|
||||
|
||||
import {
|
||||
Transaction as SmsAnnouncementSummaryTransaction,
|
||||
Announcement as SmsAnnouncementSummaryAnnouncement
|
||||
} from './screens/Sms/Announcements/Section';
|
||||
|
||||
import {
|
||||
Blocked as BlockedSms,
|
||||
BlockNumber
|
||||
} from './screens/Sms/BlockedNumbers';
|
||||
|
||||
import {
|
||||
Anomaly as AnomalySms
|
||||
} from './screens/Sms/AnomalyReport';
|
||||
|
||||
import {
|
||||
Verify as VerifyWa
|
||||
} from './screens/Wa/Verify';
|
||||
|
||||
import {
|
||||
Auth as AuthWa,
|
||||
Message as MessageWa
|
||||
} from './screens/Wa/Verify/Section';
|
||||
|
||||
import {
|
||||
Registration as RegistrationWa,
|
||||
Profile as ProfileWa,
|
||||
Template as TemplateWa,
|
||||
Integration as IntegrationWa
|
||||
} from './screens/Wa/Manage';
|
||||
import { CreateSettings } from './screens/Wa/Manage/Content';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
@ -89,12 +137,6 @@ const App = () => {
|
||||
{/* Default route */}
|
||||
<Route index element={<Navigate to="face-enroll" />} />
|
||||
</Route>
|
||||
{/* Add routes for the verify section */}
|
||||
{/* <Route path="/face-enroll" element={<Enroll />} />
|
||||
<Route path="/face-verifysection" element={<VerifySection />} />
|
||||
<Route path="/face-liveness" element={<Liveness />} />
|
||||
<Route path="/face-compare" element={<Compare />} />
|
||||
<Route path="/face-search" element={<Search />} /> */}
|
||||
|
||||
{/* Biometric - Face Recognition (Summary) */}
|
||||
<Route path="/face-summary" element={<FaceSummary />} />
|
||||
@ -130,6 +172,54 @@ const App = () => {
|
||||
<Route index element={<Navigate to="sms-otp" />} />
|
||||
</Route>
|
||||
|
||||
{/* Sms Services - OTP (Settings) */}
|
||||
<Route path="/sms-otp-settings" element={<SmsOtpSettings />} />
|
||||
{/* Sms Services - OTP (Summary) */}
|
||||
<Route path="/sms-otp-summary/*" element={<SmsOtpSummary />}>
|
||||
{/* Anak rute */}
|
||||
<Route path="sms-otp-transaction" element={<SmsOtpSummaryTransaction />} />
|
||||
<Route path="sms-otp-announcement" element={<SmsOtpSummaryAnnouncement />} />
|
||||
{/* Default route */}
|
||||
<Route index element={<Navigate to="sms-otp-transaction" />} />
|
||||
</Route>
|
||||
{/* Sms Services - OTP (Transaction) */}
|
||||
<Route path="/sms-otp-transaction" element={<SmsOtpTransaction />} />
|
||||
{/* Sms Services - OTP (Detail) */}
|
||||
<Route path="/sms-otp-detail" element={<SmsOtpDetail />} />
|
||||
|
||||
{/* Sms Services - Announcements */}
|
||||
<Route path="/sms-announcement-bulk" element={<SmsAnnouncementBulk />} />
|
||||
<Route path="/sms-announcement-summary/*" element={<SmsAnnouncementSummary />}>
|
||||
{/* Anak rute */}
|
||||
<Route path="sms-announcement-transaction" element={<SmsAnnouncementSummaryTransaction />} />
|
||||
<Route path="sms-announcement-announcement" element={<SmsAnnouncementSummaryAnnouncement />} />
|
||||
{/* Default route */}
|
||||
<Route index element={<Navigate to="sms-announcement-transaction" />} />
|
||||
</Route>
|
||||
<Route path="/sms-announcement-transaction" element={<SmsAnnouncementTransaction />} />
|
||||
|
||||
{/* Sms Services - Block Numbers */}
|
||||
<Route path="/sms-block" element={<BlockedSms />}></Route>
|
||||
<Route path="/block-number" element={<BlockNumber />} />
|
||||
|
||||
{/* Sms Services - Anomaly Report */}
|
||||
<Route path="/sms-anomaly" element={<AnomalySms />}></Route>
|
||||
|
||||
{/* WhatsApp - Verify */}
|
||||
<Route path="/wa-verify/*" element={<VerifyWa />}>
|
||||
{/* Anak rute */}
|
||||
<Route path="wa-auth" element={<AuthWa />} />
|
||||
<Route path="wa-message" element={<MessageWa />} />
|
||||
{/* Default route */}
|
||||
<Route index element={<Navigate to="wa-auth" />} />
|
||||
</Route>
|
||||
|
||||
{/* WhatsApp - Manage */}
|
||||
<Route path="/wa-registration" element={<RegistrationWa />} />
|
||||
<Route path="/wa-profile" element={<ProfileWa />} />
|
||||
<Route path="/wa-template" element={<TemplateWa />} />
|
||||
<Route path="/wa-integration" element={<IntegrationWa />} />
|
||||
<Route path="/wa-createSettings" element={<CreateSettings />} />
|
||||
{/* <Route path="/sms-otp-settings" element={<SmsOtpSettings />} /> */}
|
||||
{/* Continue for each link */}
|
||||
|
||||
|
@ -11287,3 +11287,4 @@ body {
|
||||
background-color: red;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
|
BIN
src/assets/images/WAB-1.png
Normal file
BIN
src/assets/images/WAB-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/WAB-2.png
Normal file
BIN
src/assets/images/WAB-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -3,11 +3,15 @@ import Logo from './Logo.png';
|
||||
import DashboardImg from './dashboard-img.png';
|
||||
import DummyKtp from './Dummy-Ktp.png';
|
||||
import ServerDownAnimation from './server-down.gif';
|
||||
import WAB_1 from './WAB-1.png';
|
||||
import WAB_2 from './WAB-2.png';
|
||||
|
||||
export {
|
||||
ProfileImage,
|
||||
Logo,
|
||||
DashboardImg,
|
||||
DummyKtp,
|
||||
ServerDownAnimation
|
||||
ServerDownAnimation,
|
||||
WAB_1,
|
||||
WAB_2
|
||||
}
|
@ -137,10 +137,10 @@ const dataMenu = [
|
||||
name: 'WhatsApp Management', // Changed the name
|
||||
target: 'collapseWaManage',
|
||||
subMenus: [
|
||||
{ name: 'Register Business Account', link: '/wa-manage-register'}, // Changed the name
|
||||
{ name: 'WhatsApp Profile Settings', link: '/wa-manage-profile'}, // Changed the name
|
||||
{ name: 'Message Templates', link: '/wa-manage-template'}, // Changed the name
|
||||
{ name: 'Integration Settings', link: '/wa-manage-integration'}, // Changed the name
|
||||
{ name: 'Register Business Account', link: '/wa-registration'}, // Changed the name
|
||||
{ name: 'WhatsApp Profile Settings', link: '/wa-profile'}, // Changed the name
|
||||
{ name: 'Message Templates', link: '/wa-template'}, // Changed the name
|
||||
{ name: 'Integration Settings', link: '/wa-integration'}, // Changed the name
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Enroll from "./Enroll";
|
||||
import VerifySection from "./Verify";
|
||||
import Verify from "./Verify";
|
||||
import Compare from "./Compare"
|
||||
import Liveness from "./Liveness"
|
||||
import Search from "./Search"
|
||||
|
||||
export {
|
||||
Enroll,
|
||||
VerifySection,
|
||||
Verify,
|
||||
Compare,
|
||||
Liveness,
|
||||
Search
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
Compare,
|
||||
Liveness,
|
||||
Search,
|
||||
VerifySection
|
||||
Verify as VerifySection
|
||||
} from './Section';
|
||||
|
||||
const Verify = () => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import FaceVerify from "./Verify";
|
||||
import FaceSummary from "./Summary";
|
||||
import FaceTransaction from "./Transaction";
|
||||
import Verify from "./Verify";
|
||||
import Summary from "./Summary";
|
||||
import Transaction from "./Transaction";
|
||||
|
||||
export {
|
||||
FaceVerify,
|
||||
FaceSummary,
|
||||
FaceTransaction
|
||||
Verify,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import VerifyDoc from "./Verify";
|
||||
import SummaryDoc from "./Summary";
|
||||
import TransactionDoc from "./Transaction";
|
||||
import Verify from "./Verify";
|
||||
import Summary from "./Summary";
|
||||
import Transaction from "./Transaction";
|
||||
|
||||
export {
|
||||
VerifyDoc,
|
||||
SummaryDoc,
|
||||
TransactionDoc
|
||||
Verify,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
@ -353,7 +353,8 @@ const Verify = () => {
|
||||
color: '#721c24',
|
||||
fontSize: '14px',
|
||||
margin: '0',
|
||||
},loadingOverlay: {
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'fixed', // Gunakan fixed untuk overlay penuh layar
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import VerifyKtp from "./Verify";
|
||||
import ManageKtp from "./Manage";
|
||||
import SummaryKtp from "./Summary";
|
||||
import TransactionKtp from "./Transaction";
|
||||
import Verify from "./Verify";
|
||||
import Manage from "./Manage";
|
||||
import Summary from "./Summary";
|
||||
import Transaction from "./Transaction";
|
||||
|
||||
export {
|
||||
VerifyKtp,
|
||||
ManageKtp,
|
||||
SummaryKtp,
|
||||
TransactionKtp
|
||||
Verify,
|
||||
Manage,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import VerifyNpwp from "./Verify";
|
||||
import SummaryNpwp from "./Summary";
|
||||
import TransactionNpwp from "./Transaction";
|
||||
import Verify from "./Verify";
|
||||
import Summary from "./Summary";
|
||||
import Transaction from "./Transaction";
|
||||
|
||||
export {
|
||||
VerifyNpwp,
|
||||
SummaryNpwp,
|
||||
TransactionNpwp
|
||||
Verify,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import VerifySim from "./Verify";
|
||||
import SummarySim from "./Summary";
|
||||
import TransactionSim from "./Transaction";
|
||||
import Verify from "./Verify";
|
||||
import Summary from "./Summary";
|
||||
import Transaction from "./Transaction";
|
||||
|
||||
export {
|
||||
VerifySim,
|
||||
SummarySim,
|
||||
TransactionSim
|
||||
Verify,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
295
src/screens/Sms/Announcements/Bulk.jsx
Normal file
295
src/screens/Sms/Announcements/Bulk.jsx
Normal file
@ -0,0 +1,295 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { CloudDownload } from '../../../assets/icon';
|
||||
|
||||
const Bulk = () => {
|
||||
const BASE_URL = process.env.REACT_APP_BASE_URL
|
||||
const API_KEY = process.env.REACT_APP_API_KEY
|
||||
|
||||
const [selectedImageName, setSelectedImageName] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [applicationId, setApplicationId] = useState('');
|
||||
const [applicationIds, setApplicationIds] = useState([]);
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Set mobile state based on window width
|
||||
};
|
||||
|
||||
handleResize(); // Initial check on component mount
|
||||
window.addEventListener('resize', handleResize); // Listen for resize events
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize); // Cleanup on component unmount
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApplicationIds = async () => {
|
||||
try {
|
||||
const url = `${BASE_URL}/application/list`;
|
||||
console.log('Fetching URL:', url); // Log the URL
|
||||
|
||||
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) {
|
||||
const ids = data.details.data.map(app => app.id);
|
||||
console.log('Application Id: ' + ids); // Log the IDs
|
||||
setApplicationIds(data.details.data); // Update state with the fetched data
|
||||
} else {
|
||||
console.error('Failed to fetch data:', data.details.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching application IDs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchApplicationIds();
|
||||
}, []);
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsSelectOpen(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsSelectOpen(false);
|
||||
};
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file && (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg')) {
|
||||
setSelectedImageName(file.name);
|
||||
setErrorMessage('');
|
||||
} else {
|
||||
alert('Please upload a valid image file (JPG, JPEG, PNG).');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={isMobile ? styles.containerMobile : styles.container}>
|
||||
<div style={{ ...styles.statusBox, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img src={CloudDownload} alt="Download Icon" style={styles.iconStyle} />
|
||||
<div style={{ marginLeft: '10px' }}>
|
||||
<h3 style={styles.statusTitle}>Check Our Service Status</h3>
|
||||
<p style={{ margin: '0' }}>
|
||||
Access pre-approved message templates designed for various purposes, including customer support, appointment reminders, order updates, and more.
|
||||
Simplify your communication by using these ready-to-go formats.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" style={{ ...styles.buttonStyle, backgroundColor: '#fff', color: '#0542cc', display: 'flex', alignItems: 'center', padding: '10px 15px', borderRadius: '5px' }}>
|
||||
<i className="fas fa-download" style={{ marginRight: '5px' }}></i>
|
||||
Download Template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={styles.formContainer}>
|
||||
<div
|
||||
style={{
|
||||
...styles.formRow,
|
||||
flexDirection: isMobile ? 'column' : 'row', // Set to column in mobile mode
|
||||
}}
|
||||
>
|
||||
<div style={{ ...styles.formColumn, marginRight: isMobile ? '0' : '20px' }}>
|
||||
<label htmlFor="appId" style={styles.label}>Application ID</label>
|
||||
<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>
|
||||
|
||||
<label htmlFor="messageInfo" style={styles.label}>Message Info</label>
|
||||
<textarea
|
||||
id="messageInfo"
|
||||
rows="5"
|
||||
placeholder="Type your message here..."
|
||||
style={styles.textarea}
|
||||
/>
|
||||
<p style={styles.charCounter}>Character: 0 (Max: 459), SMS Count: 0</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
...styles.uploadBox,
|
||||
marginBottom: isMobile ? '20px' : '0', // Add margin for spacing in mobile
|
||||
}}
|
||||
>
|
||||
<p style={{ fontWeight: 'bold', color: '#666' }}>Upload Template</p>
|
||||
<div
|
||||
style={styles.uploadArea}
|
||||
onClick={() => document.getElementById('fileUpload').click()}
|
||||
>
|
||||
<i className="fas fa-cloud-upload-alt" style={styles.uploadIcon}></i>
|
||||
<p style={styles.uploadText}>Drag and Drop Here</p>
|
||||
<p>Or</p>
|
||||
<button style={styles.browseButton}>Browse</button>
|
||||
<p style={styles.charCounter}>Accepted file: .csv<br />Maximum file size: 5MB</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
id="fileUpload"
|
||||
style={{ display: 'none' }}
|
||||
accept=".csv"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button style={styles.sendButton}>Send Bulk SMS</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bulk;
|
||||
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '20px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
containerMobile: {
|
||||
padding: '10px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
statusBox: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
},
|
||||
buttonStyle: {
|
||||
textDecoration: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.3s',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
statusTitle: {
|
||||
color: '#0066cc',
|
||||
fontWeight: '600',
|
||||
},
|
||||
formContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
formRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap', // Allow wrapping for smaller screens
|
||||
},
|
||||
formColumn: {
|
||||
flex: 1,
|
||||
marginRight: '20px',
|
||||
marginBottom: '20px', // Add spacing between rows in mobile
|
||||
},
|
||||
uploadBox: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bold',
|
||||
display: 'block',
|
||||
margin: '0.5rem 0',
|
||||
},
|
||||
select: {
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
},
|
||||
textarea: {
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
marginTop: '10px',
|
||||
height: '150px', // Adjust height for mobile
|
||||
},
|
||||
uploadArea: {
|
||||
backgroundColor: '#e6f2ff',
|
||||
cursor: 'pointer',
|
||||
padding: '15px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '1px solid #ced4da',
|
||||
borderRadius: '0.25rem',
|
||||
height: 'auto', // Let content dictate height
|
||||
minHeight: '200px',
|
||||
},
|
||||
uploadIcon: {
|
||||
fontSize: '36px',
|
||||
color: '#0542cc',
|
||||
marginBottom: '7px',
|
||||
},
|
||||
uploadText: {
|
||||
color: '#1f2d3d',
|
||||
fontWeight: '400',
|
||||
fontSize: '14px',
|
||||
lineHeight: '13px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
browseButton: {
|
||||
color: '#0066cc',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
marginTop: '5px',
|
||||
},
|
||||
charCounter: {
|
||||
fontSize: '12px',
|
||||
color: '#888',
|
||||
marginTop: '5px',
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: '#0066cc',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '20px',
|
||||
width: '100%', // Full width for mobile
|
||||
maxWidth: '200px', // Limited width for larger screens
|
||||
alignSelf: 'center',
|
||||
},
|
||||
};
|
||||
|
265
src/screens/Sms/Announcements/Section/Announcement.jsx
Normal file
265
src/screens/Sms/Announcements/Section/Announcement.jsx
Normal file
@ -0,0 +1,265 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Extract,
|
||||
Transaction as TransactionIcon,
|
||||
} from '../../../../assets/icon';
|
||||
|
||||
const Announcement = () => {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [application, setApplication] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const menuData = [
|
||||
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
|
||||
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
|
||||
];
|
||||
|
||||
const handleApply = () => {
|
||||
console.log({ startDate, endDate, application });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setApplication('');
|
||||
};
|
||||
|
||||
// Detect if the device is mobile based on window width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
|
||||
};
|
||||
|
||||
// Initialize on component mount
|
||||
handleResize();
|
||||
|
||||
// Add event listener to update state on resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Welcome Message */}
|
||||
<div
|
||||
className="row-card border-left border-primary shadow mb-4"
|
||||
style={{ backgroundColor: '#E2FBEA' }}
|
||||
>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.summaryContainer}>
|
||||
<div style={isMobile ? styles.filtersMobile : styles.filters}>
|
||||
{/* Row 1: Start Date and End Date */}
|
||||
<div style={styles.filterRow}>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Application Select */}
|
||||
<div style={styles.filterRow}>
|
||||
<select
|
||||
value={application}
|
||||
onChange={(e) => setApplication(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
>
|
||||
<option value="">Application</option>
|
||||
<option value="App1">App1</option>
|
||||
<option value="App2">App2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Apply and Cancel Buttons */}
|
||||
<div style={styles.filterRow}>
|
||||
<button style={styles.applyButton} onClick={handleApply}>
|
||||
Apply
|
||||
</button>
|
||||
<button style={styles.cancelButton} onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
|
||||
{menuData.map((item) => (
|
||||
<div key={item.id} style={styles.card}>
|
||||
<div style={styles.textContainer}>
|
||||
<h2 style={styles.cardTitle}>{item.value}</h2>
|
||||
<p style={styles.cardText}>{item.label}</p>
|
||||
</div>
|
||||
<div style={styles.iconContainer}>
|
||||
<img src={item.icon} alt={item.label} style={styles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
margin: '3rem',
|
||||
},
|
||||
filters: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
flexDirection: 'row', // Horizontal layout on desktop
|
||||
gap: '10px',
|
||||
},
|
||||
filtersMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column', // Vertical layout on mobile
|
||||
gap: '15px',
|
||||
},
|
||||
filterRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
filterInput: {
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '5px',
|
||||
fontSize: '16px',
|
||||
width: '48%', // Adjust width to take up 48% of the row for input elements
|
||||
},
|
||||
applyButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#000',
|
||||
padding: '10px 20px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
summaryContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
summaryCards: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
|
||||
gap: '20px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
summaryCardsMobile: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
|
||||
gap: '15px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '15px', // Reduced padding for a more compact card
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
textAlign: 'left',
|
||||
minWidth: '200px', // Ensure minimum width for mobile responsiveness
|
||||
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
|
||||
width: '100%',
|
||||
transition: 'transform 0.3s ease', // Smooth transition for hover effects
|
||||
},
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
|
||||
},
|
||||
iconContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
icon: {
|
||||
width: '36px', // Smaller icon size for a more compact card
|
||||
height: '36px', // Matching height for the icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px', // Smaller font size for title to prevent overflow
|
||||
margin: '0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '12px', // Smaller text size for description to keep it balanced
|
||||
color: '#555',
|
||||
},
|
||||
|
||||
// Optional: Hover effect for the cards to enhance interactivity
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
|
||||
},
|
||||
|
||||
// Media query for smaller screens (e.g., phones in portrait mode)
|
||||
"@media (max-width: 768px)": {
|
||||
summaryCards: {
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile screens
|
||||
},
|
||||
card: {
|
||||
padding: '10px', // Smaller padding for smaller screens
|
||||
minWidth: '180px', // Adjust min width for mobile devices
|
||||
maxWidth: '250px', // Adjust max width for mobile devices
|
||||
},
|
||||
icon: {
|
||||
width: '30px', // Slightly smaller icon size for mobile
|
||||
height: '30px', // Matching height for mobile icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '16px', // Smaller font size for the card title
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '10px', // Smaller font size for card description
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default Announcement;
|
271
src/screens/Sms/Announcements/Section/Transaction.jsx
Normal file
271
src/screens/Sms/Announcements/Section/Transaction.jsx
Normal file
@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Extract,
|
||||
Quality,
|
||||
AsyncIcon,
|
||||
QualityAsync,
|
||||
Transaction as TransactionIcon,
|
||||
} from '../../../../assets/icon';
|
||||
|
||||
const Transaction = () => {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [application, setApplication] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const menuData = [
|
||||
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
|
||||
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
|
||||
{ id: 3, value: '65', label: 'Total Quality', icon: Quality },
|
||||
{ id: 6, value: '900', label: 'Total Extract Async', icon: AsyncIcon },
|
||||
{ id: 4, value: '22', label: 'Total Quality Async', icon: QualityAsync },
|
||||
];
|
||||
|
||||
const handleApply = () => {
|
||||
console.log({ startDate, endDate, application });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setApplication('');
|
||||
};
|
||||
|
||||
// Detect if the device is mobile based on window width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
|
||||
};
|
||||
|
||||
// Initialize on component mount
|
||||
handleResize();
|
||||
|
||||
// Add event listener to update state on resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Welcome Message */}
|
||||
<div
|
||||
className="row-card border-left border-primary shadow mb-4"
|
||||
style={{ backgroundColor: '#E2FBEA' }}
|
||||
>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.summaryContainer}>
|
||||
<div style={isMobile ? styles.filtersMobile : styles.filters}>
|
||||
{/* Row 1: Start Date and End Date */}
|
||||
<div style={styles.filterRow}>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Application Select */}
|
||||
<div style={styles.filterRow}>
|
||||
<select
|
||||
value={application}
|
||||
onChange={(e) => setApplication(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
>
|
||||
<option value="">Application</option>
|
||||
<option value="App1">App1</option>
|
||||
<option value="App2">App2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Apply and Cancel Buttons */}
|
||||
<div style={styles.filterRow}>
|
||||
<button style={styles.applyButton} onClick={handleApply}>
|
||||
Apply
|
||||
</button>
|
||||
<button style={styles.cancelButton} onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
|
||||
{menuData.map((item) => (
|
||||
<div key={item.id} style={styles.card}>
|
||||
<div style={styles.textContainer}>
|
||||
<h2 style={styles.cardTitle}>{item.value}</h2>
|
||||
<p style={styles.cardText}>{item.label}</p>
|
||||
</div>
|
||||
<div style={styles.iconContainer}>
|
||||
<img src={item.icon} alt={item.label} style={styles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
margin: '3rem',
|
||||
},
|
||||
filters: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
flexDirection: 'row', // Horizontal layout on desktop
|
||||
gap: '10px',
|
||||
},
|
||||
filtersMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column', // Vertical layout on mobile
|
||||
gap: '15px',
|
||||
},
|
||||
filterRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
filterInput: {
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '5px',
|
||||
fontSize: '16px',
|
||||
width: '48%', // Adjust width to take up 48% of the row for input elements
|
||||
},
|
||||
applyButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#000',
|
||||
padding: '10px 20px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
summaryContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
summaryCards: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
|
||||
gap: '20px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
summaryCardsMobile: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
|
||||
gap: '15px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '15px', // Reduced padding for a more compact card
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
textAlign: 'left',
|
||||
minWidth: '200px', // Ensure minimum width for mobile responsiveness
|
||||
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
|
||||
width: '100%',
|
||||
transition: 'transform 0.3s ease', // Smooth transition for hover effects
|
||||
},
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
|
||||
},
|
||||
iconContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
icon: {
|
||||
width: '36px', // Smaller icon size for a more compact card
|
||||
height: '36px', // Matching height for the icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px', // Smaller font size for title to prevent overflow
|
||||
margin: '0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '12px', // Smaller text size for description to keep it balanced
|
||||
color: '#555',
|
||||
},
|
||||
|
||||
// Optional: Hover effect for the cards to enhance interactivity
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
|
||||
},
|
||||
|
||||
// Media query for smaller screens (e.g., phones in portrait mode)
|
||||
"@media (max-width: 768px)": {
|
||||
summaryCards: {
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile screens
|
||||
},
|
||||
card: {
|
||||
padding: '10px', // Smaller padding for smaller screens
|
||||
minWidth: '180px', // Adjust min width for mobile devices
|
||||
maxWidth: '250px', // Adjust max width for mobile devices
|
||||
},
|
||||
icon: {
|
||||
width: '30px', // Slightly smaller icon size for mobile
|
||||
height: '30px', // Matching height for mobile icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '16px', // Smaller font size for the card title
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '10px', // Smaller font size for card description
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default Transaction;
|
7
src/screens/Sms/Announcements/Section/index.js
Normal file
7
src/screens/Sms/Announcements/Section/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Transaction from "./Transaction";
|
||||
import Announcement from "./Announcement";
|
||||
|
||||
export {
|
||||
Transaction,
|
||||
Announcement
|
||||
}
|
114
src/screens/Sms/Announcements/Summary.jsx
Normal file
114
src/screens/Sms/Announcements/Summary.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Transaction as SmsAnnouncementSummaryTransaction,
|
||||
Announcement as SmsAnnouncementSummaryAnnouncement
|
||||
} from './Section';
|
||||
|
||||
const Summary = () => {
|
||||
const verifyTabs = [
|
||||
{ name: 'Transaction', link: 'sms-announcement-transaction' },
|
||||
{ name: 'Announcement', link: 'sms-announcement-announcement' },
|
||||
];
|
||||
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Redirect otomatis ke rute default saat akses ke /sms-announcement-transaction
|
||||
useEffect(() => {
|
||||
if (window.location.pathname === '/sms-announcement-summary') {
|
||||
navigate('sms-announcement-transaction', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
// Update state isMobile berdasarkan ukuran layar
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container" style={styles.container}>
|
||||
{/* Static Content */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
<div className="d-flex flex-row mt-3">
|
||||
<Link to="/createApps" style={{ textDecoration: 'none' }}>
|
||||
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
|
||||
<i className="fas fa-plus text-white me-2"></i>
|
||||
<p className="text-white mb-0">Create New App ID</p>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div style={styles.section}>
|
||||
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
|
||||
<div className="d-flex flex-wrap">
|
||||
{verifyTabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.link}
|
||||
to={tab.link}
|
||||
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
|
||||
style={styles.tabLink}
|
||||
>
|
||||
{tab.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Tab Content */}
|
||||
<div className="tab-content">
|
||||
<Routes>
|
||||
<Route path="sms-announcement-transaction" element={<SmsAnnouncementSummaryTransaction />} />
|
||||
<Route path="sms-announcement-announcement" element={<SmsAnnouncementSummaryAnnouncement />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
marginTop: '3%',
|
||||
padding: '0 15px',
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: '#E2FBEA',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '5px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
createButton: {
|
||||
backgroundColor: '#0542CC',
|
||||
},
|
||||
section: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tabLink: {
|
||||
padding: '10px 20px',
|
||||
},
|
||||
};
|
454
src/screens/Sms/Announcements/Transaction.jsx
Normal file
454
src/screens/Sms/Announcements/Transaction.jsx
Normal file
@ -0,0 +1,454 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Transaction = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
transactionId: `TX${String(i).padStart(3, '0')}`, // Transaction ID
|
||||
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
|
||||
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
|
||||
operator: ['Telkomsel', 'Indosat', 'XL', 'Tri', 'Smartfren'][Math.floor(Math.random() * 5)], // Random operator
|
||||
senderId: `SENDER${String(i).padStart(3, '0')}`, // Sender ID
|
||||
segment: ['Retail', 'Finance', 'Health', 'Education', 'Technology'][Math.floor(Math.random() * 5)], // Random segment
|
||||
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
|
||||
mode: Math.random() > 0.5 ? 'Online' : 'Offline', // Random mode
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(31)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row">
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Start Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>End Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Application</label>
|
||||
<select className="form-control">
|
||||
<option>Select Application</option>
|
||||
<option>App 1</option>
|
||||
<option>App 2</option>
|
||||
<option>App 3</option>
|
||||
<option>App 4</option>
|
||||
<option>App 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Operator</label>
|
||||
<select className="form-control">
|
||||
<option>Select Operator</option>
|
||||
<option>Telkomsel</option>
|
||||
<option>Indosat</option>
|
||||
<option>XL</option>
|
||||
<option>Tri</option>
|
||||
<option>Smartfren</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Status</label>
|
||||
<select className="form-control">
|
||||
<option>Select Status</option>
|
||||
<option>Completed</option>
|
||||
<option>Pending</option>
|
||||
<option>Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('transactionId')}>
|
||||
Transaction ID
|
||||
{sortConfig.key === 'transactionId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('applicationName')}>
|
||||
Application Name
|
||||
{sortConfig.key === 'applicationName' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('phoneNumber')}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('operator')}>
|
||||
Operator
|
||||
{sortConfig.key === 'operator' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'operator' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('senderId')}>
|
||||
Sender ID
|
||||
{sortConfig.key === 'senderId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'senderId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('segment')}>
|
||||
Segment
|
||||
{sortConfig.key === 'segment' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'segment' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('dateSent')}>
|
||||
Date Sent
|
||||
{sortConfig.key === 'dateSent' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('status')}>
|
||||
Status
|
||||
{sortConfig.key === 'status' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('mode')}>
|
||||
Mode
|
||||
{sortConfig.key === 'mode' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'mode' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
|
||||
<td>{transaction.transactionId}</td>
|
||||
<td>{transaction.applicationName}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.operator}</td>
|
||||
<td>{transaction.senderId}</td>
|
||||
<td>{transaction.segment}</td>
|
||||
<td>{transaction.dateSent}</td>
|
||||
<td>{transaction.status}</td>
|
||||
<td>{transaction.mode}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginRight: '-1rem', // Adjust as needed
|
||||
},
|
||||
};
|
||||
|
9
src/screens/Sms/Announcements/index.js
Normal file
9
src/screens/Sms/Announcements/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import BulkMessage from './Bulk';
|
||||
import Summary from './Summary';
|
||||
import Transaction from './Transaction';
|
||||
|
||||
export {
|
||||
BulkMessage,
|
||||
Summary,
|
||||
Transaction
|
||||
}
|
565
src/screens/Sms/AnomalyReport/Anomaly.jsx
Normal file
565
src/screens/Sms/AnomalyReport/Anomaly.jsx
Normal file
@ -0,0 +1,565 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
import Select from 'react-select'
|
||||
import { ServerDownAnimation } from '../../../assets/images';
|
||||
|
||||
|
||||
const BASE_URL = process.env.REACT_APP_BASE_URL;
|
||||
const API_KEY = process.env.REACT_APP_API_KEY;
|
||||
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Anomaly = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
const [applicationId, setApplicationId] = useState('');
|
||||
const [applicationIds, setApplicationIds] = useState([]);
|
||||
const [inputValueApplication, setInputValueApplication] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isServer, setIsServer] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const applicationOptions = applicationIds.map(app => ({
|
||||
value: app.id, // This is what will be sent when an option is selected
|
||||
label: app.name // This is what will be displayed in the dropdown
|
||||
}));
|
||||
|
||||
// Validation state
|
||||
const [validationErrors, setValidationErrors] = useState({
|
||||
applicationId: '',
|
||||
file: ''
|
||||
});
|
||||
|
||||
|
||||
const handleApplicationChange = (selectedOption) => {
|
||||
const selectedId = selectedOption.value;
|
||||
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
|
||||
|
||||
setApplicationId(selectedOption ? selectedOption.value : '');
|
||||
};
|
||||
|
||||
const handleInputChangeApplication = (inputValue) => {
|
||||
setInputValueApplication(inputValue);
|
||||
};
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
date: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
applicationId: `APP${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`, // Random Application ID
|
||||
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
|
||||
smsType: ['Promotional', 'Transactional'][Math.floor(Math.random() * 2)], // Random SMS Type
|
||||
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
|
||||
tryCount: Math.floor(Math.random() * 5) + 1, // Random try count between 1 and 5
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApplicationIds = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`${BASE_URL}/application/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch application IDs');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response Data:', data);
|
||||
|
||||
if (data.status_code === 200) {
|
||||
setApplicationIds(data.details.data);
|
||||
setIsServer(true);
|
||||
} else {
|
||||
setIsServer(false);
|
||||
throw new Error(data.details.message || 'Failed to fetch application IDs');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Error fetching application IDs');
|
||||
setIsServer(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchApplicationIds();
|
||||
|
||||
setTransactionData(generateDummyData(31)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
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 className="container mt-5">
|
||||
{/* 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>
|
||||
<p style={styles.loadingText}>Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
<strong>Please input the phone number first to show data</strong>
|
||||
<div className="col-md-8 mt-2">
|
||||
<div className="d-flex align-items-center">
|
||||
<div style={styles.selectWrapper}>
|
||||
<Select
|
||||
id="applicationId"
|
||||
value={applicationOptions.find(option => option.value === applicationId)}
|
||||
onChange={handleApplicationChange}
|
||||
options={applicationOptions}
|
||||
placeholder="Select Application ID"
|
||||
isSearchable
|
||||
menuPortalTarget={document.body}
|
||||
menuPlacement="auto"
|
||||
inputValue={inputValueApplication}
|
||||
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
|
||||
/>
|
||||
</div>
|
||||
{validationErrors.applicationId && <p style={styles.errorText}>{validationErrors.applicationId}</p>}
|
||||
<button className="btn text-white" style={{ backgroundColor: '#0542cc', marginLeft: '2vh', width: '20%' }}>Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row">
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Start Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>End Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Services</label>
|
||||
<select className="form-control">
|
||||
<option>Select Services</option>
|
||||
<option>Service 1</option>
|
||||
<option>Service 2</option>
|
||||
<option>Service 3</option>
|
||||
<option>Service 4</option>
|
||||
<option>Service 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Feature</label>
|
||||
<select className="form-control">
|
||||
<option>Select Feature</option>
|
||||
<option>Feature 1</option>
|
||||
<option>Feature 2</option>
|
||||
<option>Feature 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('applicationId')}>
|
||||
Application Id
|
||||
{sortConfig.key === 'applicationId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('applicationName')}>
|
||||
Application Name
|
||||
{sortConfig.key === 'applicationName' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('smsType')}>
|
||||
SMS Type
|
||||
{sortConfig.key === 'smsType' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'smsType' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('phoneNumber')}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('tryCount')}>
|
||||
Try Count
|
||||
{sortConfig.key === 'tryCount' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'tryCount' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
|
||||
<td>{transaction.applicationId}</td>
|
||||
<td>{transaction.applicationName}</td>
|
||||
<td>{transaction.smsType}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.tryCount}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Anomaly;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
selectWrapper: {
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.7rem', // Adjust as needed
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'fixed', // Gunakan fixed untuk overlay penuh layar
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, // Pastikan menutupi seluruh layar
|
||||
display: 'flex',
|
||||
justifyContent: 'center', // Posisikan spinner di tengah secara horizontal
|
||||
alignItems: 'center', // Posisikan spinner di tengah secara vertikal
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semitransparan di background
|
||||
color: 'white',
|
||||
fontSize: '24px',
|
||||
zIndex: 1000, // Pastikan overlay muncul di atas konten lainnya
|
||||
},
|
||||
spinner: {
|
||||
border: '4px solid #f3f3f3', // Border gray untuk spinner
|
||||
borderTop: '4px solid #3498db', // Border biru untuk spinner
|
||||
borderRadius: '50%', // Membuat spinner bulat
|
||||
width: '50px', // Ukuran spinner
|
||||
height: '50px', // Ukuran spinner
|
||||
animation: 'spin 2s linear infinite', // Menambahkan animasi spin
|
||||
},
|
||||
loadingText: {
|
||||
marginLeft: '20px',
|
||||
},
|
||||
};
|
||||
|
5
src/screens/Sms/AnomalyReport/index.js
Normal file
5
src/screens/Sms/AnomalyReport/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Anomaly from "./Anomaly";
|
||||
|
||||
export {
|
||||
Anomaly
|
||||
}
|
130
src/screens/Sms/BlockedNumbers/BlockNumber.jsx
Normal file
130
src/screens/Sms/BlockedNumbers/BlockNumber.jsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const BlockNumber = () => {
|
||||
|
||||
const [applicationId, setApplicationId] = useState('');
|
||||
const [applicationIds, setApplicationIds] = useState([]);
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [phoneId, setPhoneId] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsSelectOpen(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsSelectOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const handleClick = async () => {
|
||||
console.log('Make OTP Demo')
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='container mt-5'>
|
||||
<div style={styles.contentContainer}>
|
||||
<div className="form-group col align-items-center">
|
||||
{/* Application ID Selection */}
|
||||
<div className="col-md-6 my-2">
|
||||
<div style={styles.selectWrapper}>
|
||||
<select
|
||||
id="applicationId"
|
||||
className="form-control"
|
||||
style={{
|
||||
...styles.select,
|
||||
border: '1px solid lightgray'
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Phone Number Input */}
|
||||
<div className="col-md-6 my-2">
|
||||
<div className="input-group" style={{ marginTop: '1rem' }}>
|
||||
<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) => setPhoneId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description Text Area */}
|
||||
<div className="col-md-6 my-3">
|
||||
<textarea
|
||||
id="description"
|
||||
className="form-control"
|
||||
rows="4"
|
||||
placeholder="Enter description here..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div>
|
||||
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
|
||||
<p className="text-white mb-0">Submit Now</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlockNumber
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
selectWrapper: {
|
||||
position: 'relative',
|
||||
marginTop: '0',
|
||||
},
|
||||
select: {
|
||||
width: '100%',
|
||||
paddingRight: '30px',
|
||||
flex: 1,
|
||||
fontSize: '16px',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
},
|
||||
chevronIcon: {
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
}
|
476
src/screens/Sms/BlockedNumbers/BlockedSms.jsx
Normal file
476
src/screens/Sms/BlockedNumbers/BlockedSms.jsx
Normal file
@ -0,0 +1,476 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
FaChevronLeft,
|
||||
FaChevronRight,
|
||||
FaFastBackward,
|
||||
FaFastForward,
|
||||
FaSort,
|
||||
FaSortUp,
|
||||
FaSortDown,
|
||||
FaEdit,
|
||||
FaEye,
|
||||
FaTrash
|
||||
} from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockedSms = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
transactionId: `TX${String(i).padStart(3, '0')}`,
|
||||
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`,
|
||||
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
|
||||
description: [
|
||||
"Transaction processed successfully.",
|
||||
"Payment is pending confirmation.",
|
||||
"Failed to process transaction due to network error.",
|
||||
"Sender ID verification in progress.",
|
||||
"Operator approval required for completion.",
|
||||
"Transaction marked for manual review.",
|
||||
"Processed via offline mode.",
|
||||
"Transaction successfully completed online."
|
||||
][Math.floor(Math.random() * 8)] // Random description
|
||||
});
|
||||
}
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(3113)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Define your handle functions
|
||||
const handleEdit = (transactionId) => {
|
||||
console.log(`Edit transaction with ID: ${transactionId}`);
|
||||
// Add your edit logic here
|
||||
};
|
||||
|
||||
const handleShow = (transactionId) => {
|
||||
console.log(`Show transaction with ID: ${transactionId}`);
|
||||
// Add your show logic here
|
||||
};
|
||||
|
||||
const handleDelete = (transactionId) => {
|
||||
console.log(`Delete transaction with ID: ${transactionId}`);
|
||||
// Add your delete logic here
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Submit Block Number Button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
||||
<Link to="/block-number" className="btn text-white" style={{ backgroundColor: '#0542cc', textDecoration: 'none' }}>
|
||||
Submit Block Number
|
||||
</Link>
|
||||
</div>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row">
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Start Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>End Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Application</label>
|
||||
<select className="form-control">
|
||||
<option>Select Application</option>
|
||||
<option>App 1</option>
|
||||
<option>App 2</option>
|
||||
<option>App 3</option>
|
||||
<option>App 4</option>
|
||||
<option>App 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Endpoint</label>
|
||||
<select className="form-control">
|
||||
<option>Select Endpoint</option>
|
||||
<option>Endpoint 1</option>
|
||||
<option>Endpoint 2</option>
|
||||
<option>Endpoint 3</option>
|
||||
<option>Endpoint 4</option>
|
||||
<option>Endpoint 5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Service Charge</label>
|
||||
<select className="form-control">
|
||||
<option>Select Service Charge</option>
|
||||
<option>$10</option>
|
||||
<option>$20</option>
|
||||
<option>$30</option>
|
||||
<option>$40</option>
|
||||
<option>$50</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('transactionId')}>
|
||||
Transaction ID
|
||||
{sortConfig.key === 'transactionId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('applicationName')}>
|
||||
Application Name
|
||||
{sortConfig.key === 'applicationName' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('phoneNumber')}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn text-sm" onClick={() => handleSort('description')}>
|
||||
Description
|
||||
{sortConfig.key === 'description' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'description' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th style={{ textAlign: 'center' }}>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
|
||||
<td>{transaction.transactionId}</td>
|
||||
<td>{transaction.applicationName}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.description}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleEdit(transaction.transactionId)}
|
||||
>
|
||||
<FaEdit style={{ marginRight: '5px' }} />
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-success"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleShow(transaction.transactionId)}
|
||||
>
|
||||
<FaEye style={{ marginRight: '5px' }} />
|
||||
Show
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleDelete(transaction.transactionId)}
|
||||
>
|
||||
<FaTrash style={{ marginRight: '5px' }} />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockedSms;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.7rem', // Adjust as needed
|
||||
},
|
||||
};
|
||||
|
7
src/screens/Sms/BlockedNumbers/index.js
Normal file
7
src/screens/Sms/BlockedNumbers/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Blocked from './BlockedSms';
|
||||
import BlockNumber from './BlockNumber';
|
||||
|
||||
export {
|
||||
Blocked,
|
||||
BlockNumber
|
||||
}
|
419
src/screens/Sms/OtpManagement/DetailView.jsx
Normal file
419
src/screens/Sms/OtpManagement/DetailView.jsx
Normal file
@ -0,0 +1,419 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailView = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
|
||||
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
|
||||
otp: Math.floor(100000 + Math.random() * 900000),
|
||||
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(31)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
<strong>Please input the phone number first to show data</strong>
|
||||
<div className="col-md-8 mt-2">
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="input-group-prepend">
|
||||
<span className="input-group-text">+62</span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="phoneNumber"
|
||||
className="form-control"
|
||||
placeholder="Phone Number"
|
||||
value={phoneNumber}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
/>
|
||||
<button className="btn text-white" style={{ backgroundColor: '#0542cc', marginLeft: '2vh', width: '20%' }}>Show Data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row">
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Start Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>End Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Application</label>
|
||||
<select className="form-control">
|
||||
<option>Select Application</option>
|
||||
<option>App 1</option>
|
||||
<option>App 2</option>
|
||||
<option>App 3</option>
|
||||
<option>App 4</option>
|
||||
<option>App 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Status</label>
|
||||
<select className="form-control">
|
||||
<option>Select Status</option>
|
||||
<option>Status 1</option>
|
||||
<option>Status 2</option>
|
||||
<option>Status 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('applicationName')}>
|
||||
Application Name
|
||||
{sortConfig.key === 'applicationName' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('phoneNumber')}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('otp')}>
|
||||
OTP
|
||||
{sortConfig.key === 'otp' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'otp' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('dateSent')}>
|
||||
Date Sent
|
||||
{sortConfig.key === 'dateSent' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('status')}>
|
||||
Status
|
||||
{sortConfig.key === 'status' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
|
||||
<td>{transaction.applicationName}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.otp}</td>
|
||||
<td>{transaction.dateSent}</td>
|
||||
<td>{transaction.status}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailView;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.7rem', // Adjust as needed
|
||||
},
|
||||
};
|
||||
|
265
src/screens/Sms/OtpManagement/Section/Announcement.jsx
Normal file
265
src/screens/Sms/OtpManagement/Section/Announcement.jsx
Normal file
@ -0,0 +1,265 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Extract,
|
||||
Transaction as TransactionIcon,
|
||||
} from '../../../../assets/icon';
|
||||
|
||||
const Announcement = () => {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [application, setApplication] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const menuData = [
|
||||
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
|
||||
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
|
||||
];
|
||||
|
||||
const handleApply = () => {
|
||||
console.log({ startDate, endDate, application });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setApplication('');
|
||||
};
|
||||
|
||||
// Detect if the device is mobile based on window width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
|
||||
};
|
||||
|
||||
// Initialize on component mount
|
||||
handleResize();
|
||||
|
||||
// Add event listener to update state on resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Welcome Message */}
|
||||
<div
|
||||
className="row-card border-left border-primary shadow mb-4"
|
||||
style={{ backgroundColor: '#E2FBEA' }}
|
||||
>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.summaryContainer}>
|
||||
<div style={isMobile ? styles.filtersMobile : styles.filters}>
|
||||
{/* Row 1: Start Date and End Date */}
|
||||
<div style={styles.filterRow}>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Application Select */}
|
||||
<div style={styles.filterRow}>
|
||||
<select
|
||||
value={application}
|
||||
onChange={(e) => setApplication(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
>
|
||||
<option value="">Application</option>
|
||||
<option value="App1">App1</option>
|
||||
<option value="App2">App2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Apply and Cancel Buttons */}
|
||||
<div style={styles.filterRow}>
|
||||
<button style={styles.applyButton} onClick={handleApply}>
|
||||
Apply
|
||||
</button>
|
||||
<button style={styles.cancelButton} onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
|
||||
{menuData.map((item) => (
|
||||
<div key={item.id} style={styles.card}>
|
||||
<div style={styles.textContainer}>
|
||||
<h2 style={styles.cardTitle}>{item.value}</h2>
|
||||
<p style={styles.cardText}>{item.label}</p>
|
||||
</div>
|
||||
<div style={styles.iconContainer}>
|
||||
<img src={item.icon} alt={item.label} style={styles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
margin: '3rem',
|
||||
},
|
||||
filters: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
flexDirection: 'row', // Horizontal layout on desktop
|
||||
gap: '10px',
|
||||
},
|
||||
filtersMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column', // Vertical layout on mobile
|
||||
gap: '15px',
|
||||
},
|
||||
filterRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
filterInput: {
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '5px',
|
||||
fontSize: '16px',
|
||||
width: '48%', // Adjust width to take up 48% of the row for input elements
|
||||
},
|
||||
applyButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#000',
|
||||
padding: '10px 20px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
summaryContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
summaryCards: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
|
||||
gap: '20px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
summaryCardsMobile: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
|
||||
gap: '15px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '15px', // Reduced padding for a more compact card
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
textAlign: 'left',
|
||||
minWidth: '200px', // Ensure minimum width for mobile responsiveness
|
||||
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
|
||||
width: '100%',
|
||||
transition: 'transform 0.3s ease', // Smooth transition for hover effects
|
||||
},
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
|
||||
},
|
||||
iconContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
icon: {
|
||||
width: '36px', // Smaller icon size for a more compact card
|
||||
height: '36px', // Matching height for the icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px', // Smaller font size for title to prevent overflow
|
||||
margin: '0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '12px', // Smaller text size for description to keep it balanced
|
||||
color: '#555',
|
||||
},
|
||||
|
||||
// Optional: Hover effect for the cards to enhance interactivity
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
|
||||
},
|
||||
|
||||
// Media query for smaller screens (e.g., phones in portrait mode)
|
||||
"@media (max-width: 768px)": {
|
||||
summaryCards: {
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile screens
|
||||
},
|
||||
card: {
|
||||
padding: '10px', // Smaller padding for smaller screens
|
||||
minWidth: '180px', // Adjust min width for mobile devices
|
||||
maxWidth: '250px', // Adjust max width for mobile devices
|
||||
},
|
||||
icon: {
|
||||
width: '30px', // Slightly smaller icon size for mobile
|
||||
height: '30px', // Matching height for mobile icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '16px', // Smaller font size for the card title
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '10px', // Smaller font size for card description
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default Announcement;
|
271
src/screens/Sms/OtpManagement/Section/Transaction.jsx
Normal file
271
src/screens/Sms/OtpManagement/Section/Transaction.jsx
Normal file
@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Extract,
|
||||
Quality,
|
||||
AsyncIcon,
|
||||
QualityAsync,
|
||||
Transaction as TransactionIcon,
|
||||
} from '../../../../assets/icon';
|
||||
|
||||
const Transaction = () => {
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [application, setApplication] = useState('');
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const menuData = [
|
||||
{ id: 1, value: '150', label: 'Total Transaction', icon: TransactionIcon },
|
||||
{ id: 2, value: '4', label: 'Total Extract', icon: Extract },
|
||||
{ id: 3, value: '65', label: 'Total Quality', icon: Quality },
|
||||
{ id: 6, value: '900', label: 'Total Extract Async', icon: AsyncIcon },
|
||||
{ id: 4, value: '22', label: 'Total Quality Async', icon: QualityAsync },
|
||||
];
|
||||
|
||||
const handleApply = () => {
|
||||
console.log({ startDate, endDate, application });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setStartDate('');
|
||||
setEndDate('');
|
||||
setApplication('');
|
||||
};
|
||||
|
||||
// Detect if the device is mobile based on window width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Define mobile screen as width <= 768px
|
||||
};
|
||||
|
||||
// Initialize on component mount
|
||||
handleResize();
|
||||
|
||||
// Add event listener to update state on resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Welcome Message */}
|
||||
<div
|
||||
className="row-card border-left border-primary shadow mb-4"
|
||||
style={{ backgroundColor: '#E2FBEA' }}
|
||||
>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.summaryContainer}>
|
||||
<div style={isMobile ? styles.filtersMobile : styles.filters}>
|
||||
{/* Row 1: Start Date and End Date */}
|
||||
<div style={styles.filterRow}>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Application Select */}
|
||||
<div style={styles.filterRow}>
|
||||
<select
|
||||
value={application}
|
||||
onChange={(e) => setApplication(e.target.value)}
|
||||
style={styles.filterInput}
|
||||
>
|
||||
<option value="">Application</option>
|
||||
<option value="App1">App1</option>
|
||||
<option value="App2">App2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Apply and Cancel Buttons */}
|
||||
<div style={styles.filterRow}>
|
||||
<button style={styles.applyButton} onClick={handleApply}>
|
||||
Apply
|
||||
</button>
|
||||
<button style={styles.cancelButton} onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={isMobile ? styles.summaryCardsMobile : styles.summaryCards}>
|
||||
{menuData.map((item) => (
|
||||
<div key={item.id} style={styles.card}>
|
||||
<div style={styles.textContainer}>
|
||||
<h2 style={styles.cardTitle}>{item.value}</h2>
|
||||
<p style={styles.cardText}>{item.label}</p>
|
||||
</div>
|
||||
<div style={styles.iconContainer}>
|
||||
<img src={item.icon} alt={item.label} style={styles.icon} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
margin: '3rem',
|
||||
},
|
||||
filters: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '20px',
|
||||
flexDirection: 'row', // Horizontal layout on desktop
|
||||
gap: '10px',
|
||||
},
|
||||
filtersMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column', // Vertical layout on mobile
|
||||
gap: '15px',
|
||||
},
|
||||
filterRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
filterInput: {
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '5px',
|
||||
fontSize: '16px',
|
||||
width: '48%', // Adjust width to take up 48% of the row for input elements
|
||||
},
|
||||
applyButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: '#fff',
|
||||
color: '#000',
|
||||
padding: '10px 20px',
|
||||
border: '1px solid gray',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
width: '48%', // Adjust button width to take half of the row
|
||||
},
|
||||
summaryContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
summaryCards: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)', // 4 columns on desktop
|
||||
gap: '20px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
summaryCardsMobile: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile (full width)
|
||||
gap: '15px',
|
||||
marginTop: '20px',
|
||||
},
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '15px', // Reduced padding for a more compact card
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px', // Slightly smaller border-radius for a sharper look
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
textAlign: 'left',
|
||||
minWidth: '200px', // Ensure minimum width for mobile responsiveness
|
||||
maxWidth: '300px', // Limit the maximum width to prevent cards from becoming too wide
|
||||
width: '100%',
|
||||
transition: 'transform 0.3s ease', // Smooth transition for hover effects
|
||||
},
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom on hover for interactivity
|
||||
},
|
||||
iconContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
textContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
icon: {
|
||||
width: '36px', // Smaller icon size for a more compact card
|
||||
height: '36px', // Matching height for the icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px', // Smaller font size for title to prevent overflow
|
||||
margin: '0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '12px', // Smaller text size for description to keep it balanced
|
||||
color: '#555',
|
||||
},
|
||||
|
||||
// Optional: Hover effect for the cards to enhance interactivity
|
||||
cardHover: {
|
||||
transform: 'scale(1.05)', // Slight zoom effect when hovering over card
|
||||
},
|
||||
|
||||
// Media query for smaller screens (e.g., phones in portrait mode)
|
||||
"@media (max-width: 768px)": {
|
||||
summaryCards: {
|
||||
gridTemplateColumns: '1fr', // 1 column on mobile screens
|
||||
},
|
||||
card: {
|
||||
padding: '10px', // Smaller padding for smaller screens
|
||||
minWidth: '180px', // Adjust min width for mobile devices
|
||||
maxWidth: '250px', // Adjust max width for mobile devices
|
||||
},
|
||||
icon: {
|
||||
width: '30px', // Slightly smaller icon size for mobile
|
||||
height: '30px', // Matching height for mobile icon
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '16px', // Smaller font size for the card title
|
||||
},
|
||||
cardText: {
|
||||
fontSize: '10px', // Smaller font size for card description
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default Transaction;
|
7
src/screens/Sms/OtpManagement/Section/index.js
Normal file
7
src/screens/Sms/OtpManagement/Section/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Transaction from "./Transaction";
|
||||
import Announcement from "./Announcement";
|
||||
|
||||
export {
|
||||
Transaction,
|
||||
Announcement
|
||||
}
|
146
src/screens/Sms/OtpManagement/Settings.jsx
Normal file
146
src/screens/Sms/OtpManagement/Settings.jsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const Settings = () => {
|
||||
const [spamCheckEnabled, setSpamCheckEnabled] = useState(false);
|
||||
const [sequentialPhoneEnabled, setSequentialPhoneEnabled] = useState(false);
|
||||
const [rateLimit, setRateLimit] = useState('');
|
||||
|
||||
return (
|
||||
<div className='container' style={{ marginTop: '3%' }}>
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
}}>
|
||||
{/* Application ID, OTP Length, Expiry Time, and Message Info */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
|
||||
<div style={{ flex: '1', marginRight: '10px' }}>
|
||||
<label>Application ID</label>
|
||||
<input type="text" style={inputStyle} placeholder="Application ID" />
|
||||
</div>
|
||||
<div style={{ flex: '1', marginLeft: '10px' }}>
|
||||
<label>OTP Length</label>
|
||||
<input type="text" style={inputStyle} placeholder="OTP Length" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
|
||||
<div style={{ flex: '1', marginRight: '10px' }}>
|
||||
<label>Expiry Time</label>
|
||||
<input type="text" style={inputStyle} placeholder="Expiry Time" />
|
||||
</div>
|
||||
<div style={{ flex: '1', marginLeft: '10px' }}>
|
||||
<label>Message Info</label>
|
||||
<textarea
|
||||
style={{ ...inputStyle, height: '80px', resize: 'none' }} // Add height and prevent resizing if desired
|
||||
placeholder="Message Info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column'>
|
||||
{/* Spam Check & Rate Limit Section */}
|
||||
<h3>Spam Check</h3>
|
||||
<p>
|
||||
When this feature is enabled, any requests that exceed the limit within the specified time period will return error code 429 and a status of 6 (Rejected). These requests will not incur any charges.
|
||||
</p>
|
||||
{/* Spam Check Section */}
|
||||
<div className="col-md-6" style={{ justifyContent: 'flex-start', padding: '20px', marginBottom: '20px', marginLeft: '-1.2rem' }}>
|
||||
<div className="input-group align-items-center">
|
||||
<span className="input-group-prepend">
|
||||
<span className="input-group-text">Enable</span>
|
||||
</span>
|
||||
{/* Tampilkan Rate Limit jika spamCheckEnabled adalah true */}
|
||||
{spamCheckEnabled && (
|
||||
<input
|
||||
type="number"
|
||||
value={rateLimit}
|
||||
onChange={(e) => setRateLimit(e.target.value)}
|
||||
className="form-control text-center"
|
||||
placeholder="Rate Limit"
|
||||
style={{ marginRight: '10px' }}
|
||||
/>
|
||||
)}
|
||||
{/* Enable Switch */}
|
||||
<div className="form-check form-switch" style={{ display: 'flex', alignItems: 'center', marginLeft: '1rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={spamCheckEnabled}
|
||||
onChange={() => {
|
||||
setSpamCheckEnabled(!spamCheckEnabled);
|
||||
console.log(`Spam Check is now ${!spamCheckEnabled ? 'Enabled' : 'Disabled'}`);
|
||||
}}
|
||||
id="spamCheck"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="spamCheck" style={{ marginLeft: '5px' }}>
|
||||
Enable Spam Check
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sequential Phone Number Section */}
|
||||
<h3>Sequential Phone Number</h3>
|
||||
<p>
|
||||
When activated, all incoming request logs will be analyzed using the Verihubs algorithm. If any anomalies are detected, the
|
||||
request will be rejected and will not be forwarded to the operator. The rejected request will receive a status of 6 (Rejected).
|
||||
</p>
|
||||
{/* Spam Check Section */}
|
||||
<div className="col-md-6" style={{ justifyContent: 'flex-start', padding: '20px', marginBottom: '20px', marginLeft: '-1.2rem' }}>
|
||||
<div className="input-group align-items-center">
|
||||
<span className="input-group-prepend">
|
||||
<span className="input-group-text">Enable</span>
|
||||
</span>
|
||||
{/* Enable Switch */}
|
||||
<div className="form-check form-switch" style={{ display: 'flex', alignItems: 'center', marginLeft: '1rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={sequentialPhoneEnabled}
|
||||
onChange={() => {
|
||||
setSequentialPhoneEnabled(!sequentialPhoneEnabled);
|
||||
console.log(`Phone Number is now ${!sequentialPhoneEnabled ? 'Enabled' : 'Disabled'}`);
|
||||
}}
|
||||
id="spamCheck"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="spamCheck" style={{ marginLeft: '5px' }}>
|
||||
Enable Spam Check
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div>
|
||||
<button style={buttonStyle}>Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Styles
|
||||
const inputStyle = {
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
marginBottom: '10px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
fontSize: '16px',
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
backgroundColor: '#007BFF',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
fontSize: '16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
export default Settings;
|
114
src/screens/Sms/OtpManagement/Summary.jsx
Normal file
114
src/screens/Sms/OtpManagement/Summary.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Transaction as SmsOtpSummaryTransaction,
|
||||
Announcement as SmsOtpSummaryAnnouncement
|
||||
} from './Section';
|
||||
|
||||
const Summary = () => {
|
||||
const verifyTabs = [
|
||||
{ name: 'Transaction', link: 'sms-otp-transaction' },
|
||||
{ name: 'Announcement', link: 'sms-otp-announcement' },
|
||||
];
|
||||
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Redirect otomatis ke rute default saat akses ke /sms-otp-transaction
|
||||
useEffect(() => {
|
||||
if (window.location.pathname === '/sms-otp-summary') {
|
||||
navigate('sms-otp-transaction', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
// Update state isMobile berdasarkan ukuran layar
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container" style={styles.container}>
|
||||
{/* Static Content */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
<div className="d-flex flex-row mt-3">
|
||||
<Link to="/createApps" style={{ textDecoration: 'none' }}>
|
||||
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
|
||||
<i className="fas fa-plus text-white me-2"></i>
|
||||
<p className="text-white mb-0">Create New App ID</p>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div style={styles.section}>
|
||||
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
|
||||
<div className="d-flex flex-wrap">
|
||||
{verifyTabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.link}
|
||||
to={tab.link}
|
||||
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
|
||||
style={styles.tabLink}
|
||||
>
|
||||
{tab.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Tab Content */}
|
||||
<div className="tab-content">
|
||||
<Routes>
|
||||
<Route path="sms-otp-transaction" element={<SmsOtpSummaryTransaction />} />
|
||||
<Route path="sms-otp-announcement" element={<SmsOtpSummaryAnnouncement />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Summary;
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
marginTop: '3%',
|
||||
padding: '0 15px',
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: '#E2FBEA',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '5px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
createButton: {
|
||||
backgroundColor: '#0542CC',
|
||||
},
|
||||
section: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tabLink: {
|
||||
padding: '10px 20px',
|
||||
},
|
||||
};
|
422
src/screens/Sms/OtpManagement/Transaction.jsx
Normal file
422
src/screens/Sms/OtpManagement/Transaction.jsx
Normal file
@ -0,0 +1,422 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight, FaFastBackward, FaFastForward, FaSort, FaSortUp, FaSortDown } from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Transaction = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
transactionId: `TX${String(i).padStart(3, '0')}`, // Transaction ID
|
||||
applicationName: `App ${Math.floor(Math.random() * 5) + 1}`, // Application Name
|
||||
phoneNumber: `+62${Math.floor(Math.random() * 900000000) + 800000000}`, // Random phone number
|
||||
operator: ['Telkomsel', 'Indosat', 'XL', 'Tri', 'Smartfren'][Math.floor(Math.random() * 5)], // Random operator
|
||||
senderId: `SENDER${String(i).padStart(3, '0')}`, // Sender ID
|
||||
dateSent: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
status: ['Completed', 'Pending', 'Failed'][Math.floor(Math.random() * 3)], // Random status
|
||||
mode: Math.random() > 0.5 ? 'Online' : 'Offline', // Random mode
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(31)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row">
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Start Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>End Date</label>
|
||||
<input type="date" className="form-control" />
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<label>Application</label>
|
||||
<select className="form-control">
|
||||
<option>Select Application</option>
|
||||
<option>App 1</option>
|
||||
<option>App 2</option>
|
||||
<option>App 3</option>
|
||||
<option>App 4</option>
|
||||
<option>App 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('transactionId')}>
|
||||
Transaction ID
|
||||
{sortConfig.key === 'transactionId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'transactionId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('applicationName')}>
|
||||
Application Name
|
||||
{sortConfig.key === 'applicationName' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'applicationName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('phoneNumber')}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('operator')}>
|
||||
Operator
|
||||
{sortConfig.key === 'operator' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'operator' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('senderId')}>
|
||||
Sender ID
|
||||
{sortConfig.key === 'senderId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'senderId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('dateSent')}>
|
||||
Date Sent
|
||||
{sortConfig.key === 'dateSent' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'dateSent' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('status')}>
|
||||
Status
|
||||
{sortConfig.key === 'status' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('mode')}>
|
||||
Mode
|
||||
{sortConfig.key === 'mode' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'mode' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td>
|
||||
<td>{transaction.transactionId}</td>
|
||||
<td>{transaction.applicationName}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.operator}</td>
|
||||
<td>{transaction.senderId}</td>
|
||||
<td>{transaction.dateSent}</td>
|
||||
<td>{transaction.status}</td>
|
||||
<td>{transaction.mode}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.7rem', // Adjust as needed
|
||||
},
|
||||
};
|
||||
|
11
src/screens/Sms/OtpManagement/index.js
Normal file
11
src/screens/Sms/OtpManagement/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Settings from './Settings';
|
||||
import Summary from './Summary';
|
||||
import Transaction from './Transaction';
|
||||
import Detail from './DetailView';
|
||||
|
||||
export {
|
||||
Settings,
|
||||
Summary,
|
||||
Transaction,
|
||||
Detail
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import VerificationAnnoncement from "./Announcement";
|
||||
import VerificationOtp from "./Otp";
|
||||
import Annoncement from "./Announcement";
|
||||
import Otp from "./Otp";
|
||||
|
||||
export {
|
||||
VerificationAnnoncement,
|
||||
VerificationOtp
|
||||
Annoncement,
|
||||
Otp
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
VerificationOtp,
|
||||
VerificationAnnoncement
|
||||
Otp as VerificationOtp,
|
||||
Annoncement as VerificationAnnoncement
|
||||
} from './Section';
|
||||
|
||||
const Verify = () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import SmsVerify from "./Verify"
|
||||
import Verify from "./Verify"
|
||||
|
||||
export {
|
||||
SmsVerify
|
||||
Verify
|
||||
}
|
212
src/screens/Wa/Manage/Content/CreateSettings.jsx
Normal file
212
src/screens/Wa/Manage/Content/CreateSettings.jsx
Normal file
@ -0,0 +1,212 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const CreateSettings = () => {
|
||||
const location = useLocation();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const Breadcrumb = ({ path }) => {
|
||||
return (
|
||||
<nav style={styles.breadcrumb}>
|
||||
{path.map((item, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ' > '}
|
||||
<a
|
||||
href={item.link}
|
||||
style={{
|
||||
...styles.breadcrumbLink,
|
||||
fontWeight: location.pathname === item.link ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const breadcrumbPath = [
|
||||
{ name: 'Wa Integration', link: '/wa-integration' },
|
||||
{ name: 'Add Setting', link: '/wa-createSettings' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={isMobile ? styles.wrapperMobile : styles.wrapper}>
|
||||
<Breadcrumb path={breadcrumbPath} />
|
||||
<h2 style={styles.header}>Create Settings</h2>
|
||||
|
||||
<div style={isMobile ? styles.formWrapperMobile : styles.formWrapper}>
|
||||
{/* Left Form Section */}
|
||||
<div style={styles.formSection}>
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>Application ID</label>
|
||||
<select style={styles.input}>
|
||||
<option value="">Select Application</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>Inbound Configuration</label>
|
||||
<select style={styles.input}>
|
||||
<option value="">Select Configuration</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>Webhook URL</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Webhook URL"
|
||||
style={styles.input}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>Event</label>
|
||||
<div style={styles.checkboxGroup}>
|
||||
<label>
|
||||
<input type="checkbox" />
|
||||
Message Created
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" />
|
||||
Message Updated
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button style={styles.button}>Add Now</button>
|
||||
</div>
|
||||
|
||||
{/* Right WhatsApp Details Section */}
|
||||
<div style={styles.detailSection}>
|
||||
<h3 style={styles.detailHeader}>WhatsApp Channel Detail</h3>
|
||||
<div style={styles.detailItem}>
|
||||
<strong>Business Account Name:</strong> RekanVeri
|
||||
</div>
|
||||
<div style={styles.detailItem}>
|
||||
<strong>WABA ID:</strong> 112917284928438
|
||||
</div>
|
||||
<div style={styles.detailItem}>
|
||||
<strong>WhatsApp Number:</strong> 6287736766208
|
||||
</div>
|
||||
<div style={styles.detailItem}>
|
||||
<strong>Channel Name:</strong> RekanVeri
|
||||
</div>
|
||||
<div style={styles.detailItem}>
|
||||
<strong>Eligible for Inbound:</strong> YES
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSettings;
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '1rem',
|
||||
width: '100%',
|
||||
margin: '0 auto',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
padding: '1.5rem',
|
||||
boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
},
|
||||
wrapperMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
padding: '1rem',
|
||||
boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
breadcrumb: {
|
||||
marginBottom: '1rem',
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
breadcrumbLink: {
|
||||
textDecoration: 'none',
|
||||
color: '#0542cc',
|
||||
},
|
||||
header: {
|
||||
marginBottom: '1rem',
|
||||
fontSize: '1.5rem',
|
||||
},
|
||||
formWrapper: {
|
||||
display: 'flex',
|
||||
gap: '2rem',
|
||||
marginTop: '1rem',
|
||||
},
|
||||
formWrapperMobile: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
marginTop: '1rem',
|
||||
},
|
||||
formSection: {
|
||||
flex: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
},
|
||||
inputGroup: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
input: {
|
||||
padding: '0.5rem',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
width: '100%',
|
||||
},
|
||||
checkboxGroup: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
},
|
||||
button: {
|
||||
padding: '0.8rem 1.5rem',
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
detailSection: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f9ff',
|
||||
padding: '1rem',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
detailHeader: {
|
||||
fontSize: '1.2rem',
|
||||
marginBottom: '1rem',
|
||||
color: '#0542cc',
|
||||
},
|
||||
detailItem: {
|
||||
marginBottom: '0.5rem',
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
};
|
256
src/screens/Wa/Manage/Content/RegisterContent.jsx
Normal file
256
src/screens/Wa/Manage/Content/RegisterContent.jsx
Normal file
@ -0,0 +1,256 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { WAB_1, WAB_2 } from '../../../../assets/images';
|
||||
import { RegisterStep } from '../Content';
|
||||
|
||||
// Header component
|
||||
const HeaderComponent = ({ isMobile }) => {
|
||||
return (
|
||||
<div style={isMobile ? styles.headerContainerMobile : styles.headerContainer}>
|
||||
<img src={WAB_1} alt="WAB_1" style={styles.image(isMobile ? 200 : 350, isMobile ? 100 : 170)} />
|
||||
<div style={styles.textContainer}>
|
||||
<h2 style={styles.textTitle}>API For Your WhatsApp Business</h2>
|
||||
<p style={styles.textBody}>
|
||||
WhatsApp is one of the most popular communication platforms globally. As a Facebook-owned company,
|
||||
setting up WhatsApp Business API requires a Facebook account and Facebook Business Manager.
|
||||
Start leveraging this powerful tool for seamless communication with your customers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Step Component (untuk menampilkan 4 langkah yang ada)
|
||||
const StepComponent = ({ isMigration, isMobile }) => {
|
||||
const stepsData = isMigration
|
||||
? [
|
||||
{ number: '01', description: 'A Viable Phone Number for WhatsApp' },
|
||||
{ number: '02', description: 'Your Business Legal address and details' },
|
||||
]
|
||||
: [
|
||||
{ number: '01', description: 'A Viable Phone Number for WhatsApp' },
|
||||
{ number: '02', description: 'Your Business Legal address and details' },
|
||||
{ number: '03', description: 'Select Number you want to connect' },
|
||||
{ number: '04', description: 'Verify your Facebook Business' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={isMobile ? styles.stepsContainerMobile : styles.stepsContainer}>
|
||||
{stepsData.map((step, index) => (
|
||||
<div key={index} style={styles.step}>
|
||||
<div style={styles.stepNumberContainer}>
|
||||
<span style={styles.stepNumber}>{step.number}</span>
|
||||
</div>
|
||||
<p style={styles.stepDescription}>{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Info Box Component untuk setiap informasi
|
||||
const InfoComponent = ({ title, description, onStartNow, isMobile }) => {
|
||||
return (
|
||||
<div style={isMobile ? styles.infoBoxMobile : styles.infoBox}>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
<button style={styles.startButton} onClick={onStartNow}>Start Now</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Need Component (Menggabungkan Step dan Info Pertama)
|
||||
const Need = ({ onStartNow, isMobile }) => {
|
||||
return (
|
||||
<div style={styles.regisBusinessContainer}>
|
||||
<div style={styles.rowContainer}>
|
||||
<div style={styles.col6}>
|
||||
<StepComponent isMobile={isMobile} />
|
||||
</div>
|
||||
<div style={styles.col6}>
|
||||
<InfoComponent
|
||||
title="What You Need?"
|
||||
description="To register for WhatsApp Business API, you'll need an active phone number,
|
||||
your business's legal address and details, select the number you want to connect, and
|
||||
verify your Facebook Business account to complete the setup."
|
||||
onStartNow={onStartNow}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Migration Component (Menggabungkan Step dan Info Kedua)
|
||||
const Migration = ({ isMobile }) => {
|
||||
return (
|
||||
<div style={styles.regisBusinessContainer}>
|
||||
<div style={styles.rowContainer}>
|
||||
<div style={styles.col6}>
|
||||
<div style={styles.leftSideContainer}>
|
||||
<img src={WAB_2} alt="WAB_2" style={styles.image(isMobile ? 100 : 180, isMobile ? 100 : 180)} />
|
||||
<StepComponent isMigration={true} isMobile={isMobile} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.col6}>
|
||||
<InfoComponent
|
||||
title="What You Need for Migration?"
|
||||
description="To complete the migration, you'll need a verified Facebook Business account
|
||||
and access to the phone number you wish to migrate."
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Main component
|
||||
const RegisterContent = () => {
|
||||
const [isStarted, setIsStarted] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
// Update isMobile state on window resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const handleStartNow = () => {
|
||||
setIsStarted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.regisBusinessContainer}>
|
||||
{!isStarted && <HeaderComponent isMobile={isMobile} />}
|
||||
{!isStarted && <Need onStartNow={handleStartNow} isMobile={isMobile} />}
|
||||
{!isStarted && <Migration isMobile={isMobile} />}
|
||||
{isStarted && <RegisterStep />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const styles = {
|
||||
regisBusinessContainer: {
|
||||
padding: '1rem',
|
||||
margin: 'auto',
|
||||
fontFamily: 'Arial, sans-serif'
|
||||
},
|
||||
|
||||
// Header Content Style
|
||||
headerContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
leftSideContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
},
|
||||
image: (width, height) => ({
|
||||
width: width,
|
||||
height: height,
|
||||
flex: '1 1 33%',
|
||||
alignSelf: 'flex-start',
|
||||
}),
|
||||
textContainer: {
|
||||
flex: '1 1 66%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
paddingLeft: '20px',
|
||||
},
|
||||
textTitle: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
textBody: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
rowContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginTop: '20px',
|
||||
flexWrap: 'wrap',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
minHeight: '200px',
|
||||
},
|
||||
col6: {
|
||||
flex: '1 1 50%',
|
||||
padding: '10px',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
||||
// Step Style
|
||||
stepsContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px', // Jarak antar langkah
|
||||
position: 'relative', // Posisi relatif agar connectorLine bisa diposisikan dengan benar
|
||||
},
|
||||
step: {
|
||||
flex: '1 1 16.6%', // Menyusun setiap langkah untuk mengambil 1/6 dari lebar container
|
||||
padding: '10px',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
marginBottom: '10px',
|
||||
position: 'relative', // Agar connectorLine bisa diposisikan di dalam step
|
||||
},
|
||||
stepNumberContainer: {
|
||||
width: '40px', // Menentukan lebar oval
|
||||
height: '40px', // Menentukan tinggi oval
|
||||
borderRadius: '50%', // Membuat bentuk oval
|
||||
backgroundColor: '#0542CC', // Warna latar belakang oval
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
stepNumber: {
|
||||
fontSize: '16px',
|
||||
color: '#fff', // Warna teks putih di dalam oval
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
stepDescription: {
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
margin: '10px 0',
|
||||
},
|
||||
|
||||
// Info Style
|
||||
infoBox: {
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
startButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '15px',
|
||||
},
|
||||
};
|
||||
|
||||
export default RegisterContent;
|
455
src/screens/Wa/Manage/Content/RegisterStep.jsx
Normal file
455
src/screens/Wa/Manage/Content/RegisterStep.jsx
Normal file
@ -0,0 +1,455 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { WAB_2 } from '../../../../assets/images';
|
||||
|
||||
const HeaderComponent = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div style={{ ...styles.headerContainer, flexDirection: isMobile ? 'column' : 'row' }}>
|
||||
<div style={styles.textContainer} className='p-3'>
|
||||
<h2 style={styles.textTitle}>Registration Step</h2>
|
||||
<p style={styles.textBody}>
|
||||
WhatsApp is one of the most popular communication platforms globally. As a Facebook-owned company,
|
||||
setting up WhatsApp Business API requires a Facebook account and Facebook Business Manager.
|
||||
Start leveraging this powerful tool for seamless communication with your customers.
|
||||
</p>
|
||||
</div>
|
||||
<img src={WAB_2} alt="WAB_2" style={styles.image(190, 190)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useIsMobile = () => {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
|
||||
|
||||
const InfoComponent = ({ title, description, onStartNow }) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleCheckboxChange = (event) => {
|
||||
setIsChecked(event.target.checked);
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
const handleStartNow = () => {
|
||||
if (!isChecked) {
|
||||
setErrorMessage('Please confirm agreement on above!');
|
||||
} else {
|
||||
onStartNow();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.contentContainer}>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
<ul style={styles.descriptionList}>
|
||||
<li>The number must be owned by your company.</li>
|
||||
<li>Do not use a personal number.</li>
|
||||
<li>It should not be actively registered on WhatsApp Messenger or WhatsApp Business App.</li>
|
||||
<li>You must be able to receive phone calls or SMS on the number during the setup process.</li>
|
||||
</ul>
|
||||
<div style={styles.checkboxContainer}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="agreement"
|
||||
style={styles.checkbox}
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="agreement" style={styles.checkboxLabel}>
|
||||
I have a number with the above requirements
|
||||
</label>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
|
||||
)}
|
||||
<button style={styles.startButton} onClick={handleStartNow}>
|
||||
Start Now
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// StepComponent
|
||||
const StepComponent = ({ activeStep }) => {
|
||||
const stepsData = [
|
||||
{ number: '01', description: 'Phone Number to WhatsApp' },
|
||||
{ number: '02', description: 'WhatsApp Business Channel' },
|
||||
{ number: '03', description: 'Verify Your Business on Facebook' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={styles.stepsContainer}>
|
||||
{stepsData.map((step, index) => {
|
||||
const isCurrent = activeStep === index;
|
||||
|
||||
return (
|
||||
<div key={index} style={styles.step}>
|
||||
<div
|
||||
style={{
|
||||
...styles.stepNumberContainer,
|
||||
backgroundColor: isCurrent ? '#0542cc' : '#d3d3d3',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...styles.stepNumber,
|
||||
color: isCurrent ? '#fff' : '#808080',
|
||||
}}
|
||||
>
|
||||
{step.number}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
...styles.stepDescription,
|
||||
color: isCurrent ? '#000' : 'gray',
|
||||
}}
|
||||
>
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// FirstContentComponent
|
||||
const FirstContentComponent = ({ onStartNow }) => (
|
||||
<div style={styles.rowContainer}>
|
||||
<div style={styles.col6}>
|
||||
<StepComponent activeStep={0} /> {/* Displays step 1 as active */}
|
||||
</div>
|
||||
<div style={styles.col6}>
|
||||
<InfoComponent
|
||||
title="Following Specifications Step"
|
||||
onStartNow={onStartNow} // Handles "Start Now" button click
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// SecondContentComponent
|
||||
const SecondContentComponent = ({ onNextStep, onBack }) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleCheckboxChange = (event) => {
|
||||
setIsChecked(event.target.checked);
|
||||
setErrorMessage(''); // Reset error message if checkbox is changed
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (!isChecked) {
|
||||
setErrorMessage('Please confirm agreement on above!');
|
||||
} else {
|
||||
onNextStep(); // Proceed to next step if checkbox is checked
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.rowContainer}>
|
||||
<div style={styles.col6}>
|
||||
<StepComponent activeStep={1} /> {/* Displays step 2 as active */}
|
||||
</div>
|
||||
<div style={styles.col6}>
|
||||
<p>You'll need the following information for your WhatsApp Business Channel, Be sure to have them on hand.</p>
|
||||
<ul style={styles.descriptionList}>
|
||||
<li>Your brand's Display Name, shown to customers you chat with.</li>
|
||||
<li>Your company's Legal business name.</li>
|
||||
<li>Your company's official address.</li>
|
||||
</ul>
|
||||
<p>Create your WhatsApp Business Channel</p>
|
||||
<p>You'll be directed to Facebook Business Manager. Complete all the steps in the pop-up in order to create your WhatsApp Business Channel</p>
|
||||
|
||||
<div style={styles.checkboxContainer}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="agreement"
|
||||
style={styles.checkbox}
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="agreement" style={styles.checkboxLabel}>
|
||||
Create Your Own Channel
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<button onClick={onBack} style={styles.backButton}>
|
||||
Back
|
||||
</button>
|
||||
<button onClick={handleNextStep} style={styles.startButton}>
|
||||
Next Step
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// LastContentComponent (ThirdComponent)
|
||||
const LastContentComponent = ({ onBack }) => {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleCheckboxChange = (event) => {
|
||||
setIsChecked(event.target.checked);
|
||||
setErrorMessage(''); // Reset error message if checkbox is changed
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
if (!isChecked) {
|
||||
setErrorMessage('Please confirm agreement on above!');
|
||||
} else {
|
||||
// Perform the final action here (e.g., submit form, complete registration)
|
||||
alert('Registration Complete');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.rowContainer}>
|
||||
<div style={styles.col6}>
|
||||
<StepComponent activeStep={2} /> {/* Displays step 3 as active */}
|
||||
</div>
|
||||
<div style={styles.col6}>
|
||||
<p>Ensure your business is fully verified by completing the required steps on Facebook. This process is essential to activate and manage your WhatsApp Business API effectively.</p>
|
||||
|
||||
<div style={styles.checkboxContainer}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="agreement"
|
||||
style={styles.checkbox}
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="agreement" style={styles.checkboxLabel}>
|
||||
Verify Your Business on Facebook
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<p style={{ color: 'red', fontSize: '0.875rem' }}>{errorMessage}</p>
|
||||
)}
|
||||
|
||||
<button onClick={onBack} style={styles.backButton}>
|
||||
Back
|
||||
</button>
|
||||
<button onClick={handleFinish} style={styles.startButton}>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Component
|
||||
const RegisterStep = () => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleStartNow = () => {
|
||||
setActiveStep(1); // Move to the second step
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
setActiveStep(activeStep + 1); // Move to the next step
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep(activeStep - 1); // Move to the previous step
|
||||
};
|
||||
|
||||
const responsiveStyles = isMobile
|
||||
? { flexDirection: 'column', alignItems: 'flex-start' }
|
||||
: { flexDirection: 'row' };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderComponent />
|
||||
<div style={{ ...styles.rowContainer, ...responsiveStyles }}>
|
||||
{activeStep === 0 && (
|
||||
<FirstContentComponent onStartNow={handleStartNow} />
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<SecondContentComponent onNextStep={handleNextStep} onBack={handleBack} />
|
||||
)}
|
||||
{activeStep === 2 && (
|
||||
<LastContentComponent onBack={handleBack} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default RegisterStep
|
||||
|
||||
const styles = {
|
||||
rowContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginTop: '20px',
|
||||
flexWrap: 'wrap',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
minHeight: '200px',
|
||||
},
|
||||
col6: {
|
||||
flex: '1 1 48%', // Membagi ruang menjadi 2 kolom
|
||||
padding: '10px',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
headerContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ddd',
|
||||
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
minHeight: '250px',
|
||||
},
|
||||
leftSideContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
},
|
||||
image: (width, height) => ({
|
||||
width: width,
|
||||
height: height,
|
||||
flex: '1 1 33%',
|
||||
}),
|
||||
textContainer: {
|
||||
flex: '1 1 66%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
paddingLeft: '20px',
|
||||
},
|
||||
textTitle: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
textBody: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
// Step Styles
|
||||
stepsContainer: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px', // Jarak antar langkah
|
||||
position: 'relative',
|
||||
},
|
||||
step: {
|
||||
flex: '1 1 16.6%', // Membagi langkah dalam 1/6 bagian dari lebar container
|
||||
padding: '10px',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
marginBottom: '10px',
|
||||
position: 'relative',
|
||||
},
|
||||
stepNumberContainer: {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '8px',
|
||||
transition: 'background-color 0.3s ease',
|
||||
},
|
||||
stepNumber: {
|
||||
fontSize: '16px',
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
stepDescription: {
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: '#333',
|
||||
textAlign: 'center',
|
||||
margin: '10px 0',
|
||||
},
|
||||
// Content Styles
|
||||
contentContainer: {
|
||||
marginTop: '20px',
|
||||
padding: '20px',
|
||||
},
|
||||
infoBox: {
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
descriptionList: {
|
||||
listStyleType: 'disc',
|
||||
paddingLeft: '20px',
|
||||
color: '#333',
|
||||
fontSize: '14px',
|
||||
},
|
||||
descriptionText: {
|
||||
marginLeft: '0',
|
||||
paddingLeft: '0',
|
||||
fontSize: '14px',
|
||||
color: '#333',
|
||||
},
|
||||
startButton: {
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '15px',
|
||||
},
|
||||
checkboxContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
checkbox: {
|
||||
marginRight: '10px',
|
||||
},
|
||||
checkboxLabel: {
|
||||
color: '#0542cc',
|
||||
fontSize: '14px',
|
||||
},
|
||||
backButton: {
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#fff', // Background color updated to white
|
||||
color: '#000', // Text color set to black
|
||||
border: '2px solid #0542cc', // Border color set to #0542cc
|
||||
borderRadius: '5px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '10px',
|
||||
},
|
||||
|
||||
};
|
9
src/screens/Wa/Manage/Content/index.js
Normal file
9
src/screens/Wa/Manage/Content/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import RegisterContent from "./RegisterContent";
|
||||
import RegisterStep from "./RegisterStep";
|
||||
import CreateSettings from "./CreateSettings";
|
||||
|
||||
export {
|
||||
RegisterContent,
|
||||
RegisterStep,
|
||||
CreateSettings
|
||||
}
|
437
src/screens/Wa/Manage/Integration.jsx
Normal file
437
src/screens/Wa/Manage/Integration.jsx
Normal file
@ -0,0 +1,437 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
FaChevronLeft,
|
||||
FaChevronRight,
|
||||
FaFastBackward,
|
||||
FaFastForward,
|
||||
FaSort,
|
||||
FaSortUp,
|
||||
FaSortDown,
|
||||
FaEdit,
|
||||
FaEye,
|
||||
FaTrash
|
||||
} from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Integration = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
channelName: `Channel ${Math.floor(Math.random() * 5) + 1}`, // Random channel name (e.g., Channel 1, Channel 2, ...)
|
||||
channelId: `CH${String(i).padStart(3, '0')}`, // Random channel ID (e.g., CH001, CH002, ...)
|
||||
phoneNumber: `+1${Math.floor(Math.random() * 1000000000)}`, // Random phone number (e.g., +1234567890)
|
||||
status: ['Active', 'Inactive', 'Suspended'][Math.floor(Math.random() * 3)], // Random status
|
||||
creationDate: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
tier: ['Tier 1', 'Tier 2', 'Tier 3', 'Tier 4', 'Tier 5'][Math.floor(Math.random() * 5)], // Random tier
|
||||
officialBusinessAccount: `Business ${Math.floor(Math.random() * 5) + 1}`, // Random business account (e.g., Business 1, Business 2, ...)
|
||||
});
|
||||
}
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(52)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Define your handle functions
|
||||
const handleEdit = (transactionId) => {
|
||||
console.log(`Edit transaction with ID: ${transactionId}`);
|
||||
// Add your edit logic here
|
||||
};
|
||||
|
||||
const handleShow = (transactionId) => {
|
||||
console.log(`Show transaction with ID: ${transactionId}`);
|
||||
// Add your show logic here
|
||||
};
|
||||
|
||||
const handleDelete = (transactionId) => {
|
||||
console.log(`Delete transaction with ID: ${transactionId}`);
|
||||
// Add your delete logic here
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row d-flex justify-content-around"> {/* Using Flexbox */}
|
||||
|
||||
{/* Dropdown: Business Account */}
|
||||
<div className={`col-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<select className="form-control">
|
||||
<option>Select Application</option>
|
||||
<option>App 1</option>
|
||||
<option>App 2</option>
|
||||
<option>App 3</option>
|
||||
<option>App 4</option>
|
||||
<option>App 5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Dropdown: Status */}
|
||||
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<select className="form-control">
|
||||
<option>Select Status</option>
|
||||
<option>Status 1</option>
|
||||
<option>Status 2</option>
|
||||
<option>Status 3</option>
|
||||
<option>Status 4</option>
|
||||
<option>Status 5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Apply and Cancel Buttons */}
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-between' : 'col-md-2 d-flex align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-48">Apply</button>
|
||||
<button className="btn btn-secondary w-48">Cancel</button>
|
||||
</div>
|
||||
|
||||
{/* Create Settings Button with Icon */}
|
||||
<div className={`col-12 ${isMobile ? 'd-flex justify-content-end' : 'col-md-2 d-flex justify-content-end align-items-end'}`} style={{ gap: '10px' }}>
|
||||
<Link to="/wa-createSettings" style={{ textDecoration: 'none' }}>
|
||||
<button className="btn btn-primary">
|
||||
<i className="fas fa-cogs me-2"></i> {/* Font Awesome icon */}
|
||||
Create Settings
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('channelName')} style={styles.buttonStyle}>
|
||||
<span>Channel Name</span>
|
||||
{sortConfig.key === 'channelName' &&
|
||||
(sortConfig.direction === 'asc' ?
|
||||
<FaSortUp style={styles.iconMarginLeft} /> :
|
||||
<FaSortDown style={styles.iconMarginLeft} />
|
||||
)
|
||||
}
|
||||
{sortConfig.key !== 'channelName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('channelId')} style={styles.buttonStyle}>
|
||||
Channel ID
|
||||
{sortConfig.key === 'channelId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'channelId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('phoneNumber')} style={styles.buttonStyle}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('status')} style={styles.buttonStyle}>
|
||||
Status
|
||||
{sortConfig.key === 'status' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('creationDate')} style={styles.buttonStyle}>
|
||||
Creation Date
|
||||
{sortConfig.key === 'creationDate' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'creationDate' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('tier')} style={styles.buttonStyle}>
|
||||
Tier
|
||||
{sortConfig.key === 'tier' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'tier' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('officialBusinessAccount')} style={styles.buttonStyle}>
|
||||
Official Business Account
|
||||
{sortConfig.key === 'officialBusinessAccount' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'officialBusinessAccount' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
|
||||
<td>{transaction.channelName}</td>
|
||||
<td>{transaction.channelId}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.status}</td>
|
||||
<td>{transaction.creationDate}</td>
|
||||
<td>{transaction.tier}</td>
|
||||
<td>{transaction.officialBusinessAccount}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.5rem',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
buttonStyle: {
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 1rem',
|
||||
},
|
||||
};
|
||||
|
486
src/screens/Wa/Manage/Profile.jsx
Normal file
486
src/screens/Wa/Manage/Profile.jsx
Normal file
@ -0,0 +1,486 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
FaChevronLeft,
|
||||
FaChevronRight,
|
||||
FaFastBackward,
|
||||
FaFastForward,
|
||||
FaSort,
|
||||
FaSortUp,
|
||||
FaSortDown,
|
||||
FaEdit,
|
||||
FaEye,
|
||||
FaTrash
|
||||
} from 'react-icons/fa'; // Icons for sorting
|
||||
import { NoAvailable } from '../../../assets/icon';
|
||||
|
||||
// Pagination Component
|
||||
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
||||
const handlePrev = () => {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFirst = () => {
|
||||
onPageChange(1); // Go to first page
|
||||
};
|
||||
|
||||
const handleLast = () => {
|
||||
onPageChange(totalPages); // Go to last page
|
||||
};
|
||||
|
||||
// Logic to display only 3 pages in pagination
|
||||
const getPaginationRange = () => {
|
||||
const range = [];
|
||||
const totalPagesCount = totalPages;
|
||||
|
||||
let start = currentPage - 1;
|
||||
let end = currentPage + 1;
|
||||
|
||||
// Adjust start and end if near the boundaries
|
||||
if (currentPage === 1) {
|
||||
start = 1;
|
||||
end = Math.min(3, totalPagesCount);
|
||||
} else if (currentPage === totalPages) {
|
||||
start = Math.max(totalPagesCount - 2, 1);
|
||||
end = totalPagesCount;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const pageRange = getPaginationRange();
|
||||
|
||||
return (
|
||||
<div className="pagination-container d-flex justify-content-end mt-4">
|
||||
{/* First Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleFirst}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaFastBackward /> {/* Double Arrow Left */}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handlePrev}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<FaChevronLeft /> {/* Single Arrow Left */}
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pageRange.map((pageNum) => (
|
||||
<button
|
||||
key={pageNum}
|
||||
className={`btn ${pageNum === currentPage ? 'btn-primary' : ''}`}
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleNext}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaChevronRight /> {/* Single Arrow Right */}
|
||||
</button>
|
||||
|
||||
{/* Last Page Button */}
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleLast}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<FaFastForward /> {/* Double Arrow Right */}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Profile = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isMobile, setIsMobile] = useState(false); // State to detect mobile view
|
||||
const [transactionData, setTransactionData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); // Sorting state
|
||||
const dataPerPage = 10; // Data per page (10 data per page)
|
||||
|
||||
const buttonData = [
|
||||
{ label: 'Copy', enabled: true },
|
||||
{ label: 'CSV', enabled: true },
|
||||
{ label: 'Excel', enabled: true },
|
||||
{ label: 'PDF', enabled: true },
|
||||
{ label: 'Print', enabled: true },
|
||||
{ label: 'Column Visibility', enabled: true },
|
||||
];
|
||||
|
||||
|
||||
// Generate 691 dummy transactions
|
||||
const generateDummyData = (numOfItems) => {
|
||||
const transactionData = [];
|
||||
|
||||
for (let i = 1; i <= numOfItems; i++) {
|
||||
transactionData.push({
|
||||
channelName: `Channel ${Math.floor(Math.random() * 5) + 1}`, // Random channel name (e.g., Channel 1, Channel 2, ...)
|
||||
channelId: `CH${String(i).padStart(3, '0')}`, // Random channel ID (e.g., CH001, CH002, ...)
|
||||
phoneNumber: `+1${Math.floor(Math.random() * 1000000000)}`, // Random phone number (e.g., +1234567890)
|
||||
status: ['Active', 'Inactive', 'Suspended'][Math.floor(Math.random() * 3)], // Random status
|
||||
creationDate: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toLocaleDateString(), // Random date
|
||||
officialBusinessAccount: `Business ${Math.floor(Math.random() * 5) + 1}`, // Random business account (e.g., Business 1, Business 2, ...)
|
||||
});
|
||||
}
|
||||
|
||||
return transactionData;
|
||||
};
|
||||
|
||||
|
||||
// Set the generated transaction data
|
||||
useEffect(() => {
|
||||
setTransactionData(generateDummyData(52)); // count data dummy transactions
|
||||
}, []);
|
||||
|
||||
// Sorting function
|
||||
const sortData = (data, config) => {
|
||||
const { key, direction } = config;
|
||||
return [...data].sort((a, b) => {
|
||||
if (a[key] < b[key]) {
|
||||
return direction === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (a[key] > b[key]) {
|
||||
return direction === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
|
||||
// Handle column header sort click
|
||||
const handleSort = (key) => {
|
||||
let direction = 'asc';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
||||
direction = 'desc'; // Toggle direction if the same column is clicked
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
// Get the paginated data
|
||||
const getPaginatedData = (data, page, perPage) => {
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
const startIndex = (page - 1) * perPage;
|
||||
const endIndex = startIndex + perPage;
|
||||
return sortedData.slice(startIndex, endIndex);
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate total pages based on the data and data per page
|
||||
const totalPages = Math.ceil(transactionData.length / dataPerPage);
|
||||
|
||||
// Paginated data
|
||||
const paginatedData = getPaginatedData(transactionData, currentPage, dataPerPage);
|
||||
|
||||
// Detect screen size and update isMobile state
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768); // Change 768 to your breakpoint
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Define your handle functions
|
||||
const handleEdit = (transactionId) => {
|
||||
console.log(`Edit transaction with ID: ${transactionId}`);
|
||||
// Add your edit logic here
|
||||
};
|
||||
|
||||
const handleShow = (transactionId) => {
|
||||
console.log(`Show transaction with ID: ${transactionId}`);
|
||||
// Add your show logic here
|
||||
};
|
||||
|
||||
const handleDelete = (transactionId) => {
|
||||
console.log(`Delete transaction with ID: ${transactionId}`);
|
||||
// Add your delete logic here
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={{ backgroundColor: '#E2FBEA' }}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.contentContainer}>
|
||||
{/* Filter Form */}
|
||||
<div className="card p-3 mb-4">
|
||||
<div className="row d-flex justify-content-arround"> {/* Menggunakan Flexbox */}
|
||||
{/* Dropdown: Business Account */}
|
||||
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<select className="form-control">
|
||||
<option>Select Business Account</option>
|
||||
<option>Account 1</option>
|
||||
<option>Account 2</option>
|
||||
<option>Account 3</option>
|
||||
<option>Account 4</option>
|
||||
<option>Account 5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Input: Business Account Name */}
|
||||
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<input type="text" className="form-control" placeholder="Enter Business Account Name" />
|
||||
</div>
|
||||
|
||||
{/* Dropdown: Meta Manager ID */}
|
||||
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<select className="form-control">
|
||||
<option>Select Meta Manager</option>
|
||||
<option>Manager 1</option>
|
||||
<option>Manager 2</option>
|
||||
<option>Manager 3</option>
|
||||
<option>Manager 4</option>
|
||||
<option>Manager 5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Dropdown: WABA ID */}
|
||||
<div className={`col-sm-12 ${isMobile ? 'mb-2' : 'col-md-2'}`}>
|
||||
<select className="form-control">
|
||||
<option>Select WABA ID</option>
|
||||
<option>WABA 1</option>
|
||||
<option>WABA 2</option>
|
||||
<option>WABA 3</option>
|
||||
<option>WABA 4</option>
|
||||
<option>WABA 5</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
{buttonData.map((button, index) =>
|
||||
button.enabled ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`btn btn-light ${isMobile ? 'mb-2' : ''}`} // Add margin on mobile
|
||||
style={styles.actionButton}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Bar with Icon */}
|
||||
<div className="input-group" style={{ width: '250px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="form-control"
|
||||
/>
|
||||
<span className="input-group-text">
|
||||
<i className="fas fa-search"></i> {/* FontAwesome search icon */}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Table */}
|
||||
<div className="table-responsive">
|
||||
<table className="table table-bordered" style={styles.tableContainer}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th> {/* Kolom untuk Nomor Urut */}
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('channelName')} style={styles.buttonStyle}>
|
||||
<span>Channel Name</span>
|
||||
{sortConfig.key === 'channelName' &&
|
||||
(sortConfig.direction === 'asc' ?
|
||||
<FaSortUp style={styles.iconMarginLeft} /> :
|
||||
<FaSortDown style={styles.iconMarginLeft} />
|
||||
)
|
||||
}
|
||||
{sortConfig.key !== 'channelName' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('channelId')} style={styles.buttonStyle}>
|
||||
Channel ID
|
||||
{sortConfig.key === 'channelId' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'channelId' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('phoneNumber')} style={styles.buttonStyle}>
|
||||
Phone Number
|
||||
{sortConfig.key === 'phoneNumber' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'phoneNumber' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('status')} style={styles.buttonStyle}>
|
||||
Status
|
||||
{sortConfig.key === 'status' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'status' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('creationDate')} style={styles.buttonStyle}>
|
||||
Creation Date
|
||||
{sortConfig.key === 'creationDate' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'creationDate' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th>
|
||||
<button className="btn" onClick={() => handleSort('officialBusinessAccount')} style={styles.buttonStyle}>
|
||||
Official Business Account
|
||||
{sortConfig.key === 'officialBusinessAccount' &&
|
||||
(sortConfig.direction === 'asc' ? <FaSortUp style={styles.iconMarginLeft} /> : <FaSortDown style={styles.iconMarginLeft} />)
|
||||
}
|
||||
{sortConfig.key !== 'officialBusinessAccount' && <FaSort style={styles.iconMarginLeft} />}
|
||||
</button>
|
||||
</th>
|
||||
<th style={{ textAlign: 'center', verticalAlign: 'middle' }}>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((transaction, index) => (
|
||||
<tr key={index}>
|
||||
{/* Kolom Nomor Urut */}
|
||||
<td>{(currentPage - 1) * dataPerPage + index + 1}</td> {/* Nomor urut berdasarkan halaman dan index */}
|
||||
<td>{transaction.channelName}</td>
|
||||
<td>{transaction.channelId}</td>
|
||||
<td>{transaction.phoneNumber}</td>
|
||||
<td>{transaction.status}</td>
|
||||
<td>{transaction.creationDate}</td>
|
||||
<td>{transaction.officialBusinessAccount}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleEdit(transaction.transactionId)}
|
||||
>
|
||||
<FaEdit style={{ marginRight: '5px' }} />
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-success"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleShow(transaction.transactionId)}
|
||||
>
|
||||
<FaEye style={{ marginRight: '5px' }} />
|
||||
Show
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
onClick={() => handleDelete(transaction.transactionId)}
|
||||
>
|
||||
<FaTrash style={{ marginRight: '5px' }} />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="7" className="text-center">
|
||||
<div className="d-flex flex-column align-items-center mt-5">
|
||||
<img src={NoAvailable} alt="No Data Available" className="mb-3" style={styles.iconStyle} />
|
||||
<p>Data not available</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
const styles = {
|
||||
contentContainer: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tableContainer: {
|
||||
minHeight: '300px',
|
||||
maxHeight: '1500px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
iconStyle: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
},
|
||||
// Add margin-left style for icons
|
||||
iconMarginLeft: {
|
||||
marginLeft: '0.5rem',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
buttonStyle: {
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 1rem',
|
||||
},
|
||||
};
|
||||
|
29
src/screens/Wa/Manage/Registration.jsx
Normal file
29
src/screens/Wa/Manage/Registration.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { RegisterContent } from './Content'
|
||||
|
||||
const Registration = () => {
|
||||
return (
|
||||
<div className='container' style={styles.container}>
|
||||
<div style={styles.section}>
|
||||
<h1>WA Manage</h1>
|
||||
<RegisterContent />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Registration
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
marginTop: '3%',
|
||||
padding: '0 15px',
|
||||
},
|
||||
section: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
}
|
346
src/screens/Wa/Manage/Template.jsx
Normal file
346
src/screens/Wa/Manage/Template.jsx
Normal file
@ -0,0 +1,346 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
|
||||
const Template = () => {
|
||||
const BASE_URL = process.env.REACT_APP_BASE_URL;
|
||||
const API_KEY = process.env.REACT_APP_API_KEY;
|
||||
|
||||
const [ownership, setOwnership] = useState([]);
|
||||
const [buttonStates, setButtonStates] = useState([]);
|
||||
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState(''); // state for search term
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); // state for debounced search term
|
||||
const [errorMessage, setErrorMessage] = useState(null); // state for error message
|
||||
|
||||
// Debounce search term input
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
}, 500); // Delay of 500ms before search term is sent
|
||||
return () => clearTimeout(timer); // Clear timeout when searchTerm changes
|
||||
}, [searchTerm]);
|
||||
|
||||
// Fetch applications based on the search term
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchApplications = async () => {
|
||||
if (debouncedSearchTerm) {
|
||||
// Fetch by search term
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/application/get-by-name/${debouncedSearchTerm}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (result.details && result.details.data) {
|
||||
if (isMounted) {
|
||||
setOwnership([result.details.data]);
|
||||
setButtonStates([{ isHovered: false, isActive: false }]);
|
||||
setErrorMessage(null); // Reset error if data found
|
||||
}
|
||||
} else {
|
||||
if (isMounted) {
|
||||
setOwnership([]);
|
||||
setErrorMessage('Data is not found.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isMounted) {
|
||||
setOwnership([]);
|
||||
setErrorMessage('Data is not found.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching applications:', error);
|
||||
if (isMounted) {
|
||||
setOwnership([]);
|
||||
setErrorMessage('Data is not found.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fetch all applications when there's no search term
|
||||
const fetchAllApplications = async () => {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/application/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok && result.status_code === 200) {
|
||||
if (isMounted) {
|
||||
setOwnership(result.details.data);
|
||||
setButtonStates(result.details.data.map(() => ({ isHovered: false, isActive: false })));
|
||||
setErrorMessage(null); // Reset error if data found
|
||||
}
|
||||
} else {
|
||||
if (isMounted) {
|
||||
setOwnership([]);
|
||||
setErrorMessage('Error fetching data. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching all applications:', error);
|
||||
if (isMounted) {
|
||||
setOwnership([]);
|
||||
setErrorMessage('Error fetching data. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchAllApplications();
|
||||
}
|
||||
};
|
||||
|
||||
fetchApplications();
|
||||
|
||||
return () => {
|
||||
isMounted = false; // Set the flag to false on cleanup
|
||||
};
|
||||
|
||||
}, [debouncedSearchTerm]); // Trigger the effect when `debouncedSearchTerm` changes
|
||||
|
||||
const handleMouseEnter = (index) => {
|
||||
setButtonStates((prevStates) => {
|
||||
const newStates = [...prevStates];
|
||||
newStates[index].isHovered = true;
|
||||
return newStates;
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = (index) => {
|
||||
setButtonStates((prevStates) => {
|
||||
const newStates = [...prevStates];
|
||||
newStates[index].isHovered = false;
|
||||
return newStates;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Welcome Message */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<div>
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-message fa-bold me-3"></i>Create WhatsApp Business Templates
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Design pre-approved message templates tailored for various business needs, such as customer support, appointment reminders, order updates, and more. Streamline communication and ensure consistency with ready-to-use templates.
|
||||
</p>
|
||||
</div>
|
||||
<div className="d-flex flex-row mt-3">
|
||||
<Link to="/" style={{ textDecoration: 'none' }}>
|
||||
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
|
||||
<i className="fas fa-plus text-white me-2"></i>
|
||||
<p className="text-white mb-0">Create Template</p>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.wrapperCard}>
|
||||
<div className="card mt-3">
|
||||
<div className="row">
|
||||
{/* Filtering Options */}
|
||||
<div className="col-md-2">
|
||||
<select className="form-control">
|
||||
<option value="">Pilih WABA IDI</option>
|
||||
<option value="waba1">WABA 1</option>
|
||||
<option value="waba2">WABA 2</option>
|
||||
<option value="waba3">WABA 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<select className="form-control">
|
||||
<option value="">Pilih Kategori</option>
|
||||
<option value="kategori1">Kategori 1</option>
|
||||
<option value="kategori2">Kategori 2</option>
|
||||
<option value="kategori3">Kategori 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2">
|
||||
<select className="form-control">
|
||||
<option value="">Pilih Status</option>
|
||||
<option value="aktif">Aktif</option>
|
||||
<option value="non-aktif">Non-Aktif</option>
|
||||
<option value="pending">Pending</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="col-md-2 d-flex align-items-end" style={{ gap: '10px' }}>
|
||||
<button className="btn btn-primary w-100">Apply</button>
|
||||
<button className="btn btn-secondary w-100">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div style={styles.searchInputWrapper}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type your keywords here"
|
||||
style={styles.searchInput}
|
||||
value={searchTerm}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<button style={styles.searchButton}>
|
||||
<i className="fas fa-search" style={styles.searchIcon}></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Application Cards */}
|
||||
<div style={styles.cardContainer}>
|
||||
{/* Show message if no data or search result */}
|
||||
{ownership.length === 0 && !errorMessage && <p style={styles.noDataMessage}>Data is not found.</p>}
|
||||
|
||||
{ownership.map((item, index) => (
|
||||
<div key={item.id} style={styles.card}>
|
||||
<div style={styles.cardHeader}>
|
||||
<span style={styles.cardTitle}>{item.name}</span>
|
||||
</div>
|
||||
<div style={styles.cardContent}>
|
||||
<div style={styles.applicationIdWrapper}>
|
||||
<p><strong>WABA Ownership:</strong></p>
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
|
||||
{/* Copy to Clipboard Button */}
|
||||
<CopyToClipboard
|
||||
key={index}
|
||||
text={item.name}
|
||||
onCopy={() => {
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(null), 2000); // Hide "Copied" status after 2 seconds
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: copiedIndex === index ? '#4BA5E7' : '#0542cc',
|
||||
padding: '10px 20px',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onMouseEnter={() => handleMouseEnter(index)}
|
||||
onMouseLeave={() => handleMouseLeave(index)}
|
||||
>
|
||||
{copiedIndex === index ? (
|
||||
<i className="fas fa-check"></i> // Show check icon if copied
|
||||
) : (
|
||||
<i className="fas fa-copy"></i> // Show copy icon
|
||||
)}
|
||||
{copiedIndex === index ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Show error message */}
|
||||
{errorMessage && <p style={styles.errorMessage}>{errorMessage}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
margin: '20px auto',
|
||||
maxWidth: '1200px',
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: '#F1F3F6',
|
||||
borderColor: '#1e2b34',
|
||||
borderRadius: '5px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
createButton: {
|
||||
backgroundColor: '#1d3f68',
|
||||
color: '#fff',
|
||||
borderRadius: '5px',
|
||||
padding: '12px 20px',
|
||||
width: '150px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
wrapperCard: {
|
||||
padding: '20px',
|
||||
},
|
||||
searchInputWrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
searchInput: {
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '5px',
|
||||
width: '300px',
|
||||
marginRight: '10px',
|
||||
},
|
||||
searchButton: {
|
||||
backgroundColor: '#1d3f68',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
padding: '10px 15px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
searchIcon: {
|
||||
fontSize: '16px',
|
||||
},
|
||||
cardContainer: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '20px',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '10px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
cardHeader: {
|
||||
marginBottom: '10px',
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cardContent: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
applicationIdWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
noDataMessage: {
|
||||
color: 'gray',
|
||||
},
|
||||
errorMessage: {
|
||||
color: 'red',
|
||||
}
|
||||
};
|
||||
|
||||
export default Template;
|
11
src/screens/Wa/Manage/index.js
Normal file
11
src/screens/Wa/Manage/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Registration from './Registration';
|
||||
import Profile from './Profile';
|
||||
import Template from './Template';
|
||||
import Integration from './Integration';
|
||||
|
||||
export {
|
||||
Registration,
|
||||
Profile,
|
||||
Template,
|
||||
Integration
|
||||
}
|
542
src/screens/Wa/Verify/Section/Auth.jsx
Normal file
542
src/screens/Wa/Verify/Section/Auth.jsx
Normal file
@ -0,0 +1,542 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import Select from 'react-select'
|
||||
|
||||
const BASE_URL = process.env.REACT_APP_BASE_URL
|
||||
const API_KEY = process.env.REACT_APP_API_KEY
|
||||
|
||||
const Verify = ({ phoneId, onVerify }) => {
|
||||
const [otpCode, setOtpCode] = useState('');
|
||||
const [generateCode, setGenerateCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleClick = async () => {
|
||||
console.log('Verify Click');
|
||||
console.log('otp code: ', otpCode);
|
||||
console.log('generate code: ', generateCode);
|
||||
console.log('phone number: ', phoneId);
|
||||
|
||||
// Validation logic
|
||||
if (!otpCode) {
|
||||
setError("OTP Code is required."); // Set error message directly
|
||||
return; // Exit the function if there are errors
|
||||
} else if (otpCode.length !== 6) {
|
||||
setError("OTP Code must be exactly 6 characters long."); // Set error message directly
|
||||
return; // Exit the function if there are errors
|
||||
}
|
||||
|
||||
setError(''); // Clear error if validation passes
|
||||
onVerify(); // Call the onVerify function passed from Auth
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div style={styles.section}>
|
||||
{/* Results Data */}
|
||||
<div className="form-group row align-items-center mt-4">
|
||||
<div className="col-md-6">
|
||||
<div style={styles.selectWrapper}>
|
||||
<input
|
||||
type="text"
|
||||
id="generateCode"
|
||||
className="form-control"
|
||||
placeholder="e133d538-dc40-4c43-b042-06aad864d634"
|
||||
value={generateCode}
|
||||
onChange={(e) => setGenerateCode(e.target.value)}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OTP Code */}
|
||||
<div className="form-group row align-items-center mt-3">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
id="otpCode"
|
||||
className="form-control"
|
||||
placeholder="OTP Code"
|
||||
value={otpCode}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce exactly 6 digits
|
||||
if (/^\d{0,6}$/.test(value)) {
|
||||
setOtpCode(value);
|
||||
}
|
||||
}}
|
||||
maxLength={6} // Prevents entering more than 6 digits
|
||||
/>
|
||||
|
||||
{error && <small className="text-danger">{error}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verify Button */}
|
||||
<div style={styles.submitButton}>
|
||||
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
|
||||
<p className="text-white mb-0">Verify</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const Preview = () => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(inputValue);
|
||||
console.log('Copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-group row align-items-center mt-4">
|
||||
<div className="col-10">
|
||||
<div style={styles.selectWrapper}>
|
||||
<input
|
||||
type="text"
|
||||
id="inputValue"
|
||||
className="form-control"
|
||||
placeholder="7976 is your verification code. For your security, do not share this code."
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-2 d-flex">
|
||||
<div style={styles.selectWrapper}>
|
||||
<button className="btn btn-primary" onClick={handleCopy}>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Auth = () => {
|
||||
|
||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [applicationId, setApplicationId] = useState('');
|
||||
const [expiryId, setExpiryId] = useState(0);
|
||||
const [otpId, setOtpId] = useState('');
|
||||
const [phoneId, setPhoneId] = useState('');
|
||||
const [templateId, setTemplateId] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
const [applicationIds, setApplicationIds] = useState([]);
|
||||
const [showVerify, setShowVerify] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
|
||||
const [isServer, setIsServer] = useState(true);
|
||||
const [selectedQuota, setSelectedQuota] = useState(0);
|
||||
|
||||
const applicationOptions = applicationIds.map(app => ({
|
||||
value: app.id,
|
||||
label: app.name
|
||||
}));
|
||||
|
||||
const handleApplicationChange = (selectedOption) => {
|
||||
const selectedId = selectedOption.value;
|
||||
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
|
||||
|
||||
setApplicationId(selectedOption ? selectedOption.value : '');
|
||||
|
||||
if (selectedApp) {
|
||||
setSelectedQuota(selectedApp.quota);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChangeApplication = (newInputValue) => {
|
||||
// Limit input to 15 characters for Application ID
|
||||
if (newInputValue.length <= 15) {
|
||||
setInputValueApplication(newInputValue);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApplicationIds = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`${BASE_URL}/application/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch application IDs');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response Data:', data);
|
||||
|
||||
if (data.status_code === 200) {
|
||||
setApplicationIds(data.details.data);
|
||||
setIsServer(true);
|
||||
} else {
|
||||
setIsServer(false);
|
||||
throw new Error(data.details.message || 'Failed to fetch application IDs');
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors(error.message || 'Error fetching application IDs');
|
||||
setIsServer(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchApplicationIds();
|
||||
}, []);
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsSelectOpen(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsSelectOpen(false);
|
||||
};
|
||||
|
||||
const handleVerify = () => {
|
||||
setShowPreview(true); // Show the preview when verified
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
if (!applicationId) newErrors.applicationId = "Application ID is required.";
|
||||
if (expiryId <= 0) newErrors.expiryId = "Expiry Time must be greater than 0.";
|
||||
if (!otpId) newErrors.otpId = "OTP Length is required.";
|
||||
if (!phoneId) newErrors.phoneId = "Phone Number is required.";
|
||||
if (!templateId) newErrors.templateId = "Template Name is required.";
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
if (validate()) {
|
||||
// Log the input data
|
||||
console.log('Application ID:', applicationId);
|
||||
console.log('Expiry ID:', expiryId);
|
||||
console.log('OTP Length:', otpId);
|
||||
console.log('Phone Number:', '08' + phoneId); // Prepend "08" to the phone number
|
||||
console.log('Template ID:', templateId);
|
||||
|
||||
console.log('Make Auth Demo');
|
||||
setShowVerify(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={styles.section}>
|
||||
{/* 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>
|
||||
)}
|
||||
{/* Application ID Selection */}
|
||||
<div className="form-group row align-items-center">
|
||||
<div className="col-md-6">
|
||||
<Select
|
||||
id="applicationId"
|
||||
value={applicationOptions.find(option => option.value === applicationId)}
|
||||
onChange={handleApplicationChange}
|
||||
options={applicationOptions}
|
||||
placeholder="Select Application ID"
|
||||
isSearchable
|
||||
menuPortalTarget={document.body}
|
||||
menuPlacement="auto"
|
||||
inputValue={inputValueApplication}
|
||||
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
|
||||
/>
|
||||
</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}>{selectedQuota}</span>
|
||||
<span style={styles.timesText}>(times)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expiry and OTP */}
|
||||
<div className="form-group row align-items-center">
|
||||
<div className="col-md-6">
|
||||
<div style={styles.selectWrapper}>
|
||||
<input
|
||||
type="number"
|
||||
id="expiryId"
|
||||
className="form-control"
|
||||
style={styles.select}
|
||||
value={expiryId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Check if the value is empty or a valid number
|
||||
if (value === '' || /^[0-9]+$/.test(value)) {
|
||||
setExpiryId(value === '' ? '' : parseInt(value, 10)); // Update state as number
|
||||
}
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Expiry Time"
|
||||
/>
|
||||
{errors.expiryId && <small className="text-danger">{errors.expiryId}</small>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
id="otpId"
|
||||
className="form-control"
|
||||
placeholder="OTP Length"
|
||||
value={otpId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce length constraints
|
||||
if (/^\d{0,6}$/.test(value)) {
|
||||
setOtpId(value);
|
||||
}
|
||||
}}
|
||||
minLength={6} // Enforce minimum length (for validation)
|
||||
maxLength={6} // Enforce maximum length
|
||||
/>
|
||||
{errors.otpId && <small className="text-danger">{errors.otpId}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message and Phone */}
|
||||
<div className="form-group row align-items-center mt-4">
|
||||
<div className="col-md-6">
|
||||
<div style={styles.selectWrapper}>
|
||||
<input
|
||||
type="text"
|
||||
id="templateId"
|
||||
className="form-control"
|
||||
placeholder="Template Name"
|
||||
value={templateId}
|
||||
onChange={(e) => setTemplateId(e.target.value)}
|
||||
/>
|
||||
{errors.templateId && <small className="text-danger">{errors.templateId}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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} // Enforce minimum length
|
||||
maxLength={14} // Enforce maximum length
|
||||
/>
|
||||
|
||||
</div>
|
||||
{errors.phoneId && <small className="text-danger">{errors.phoneId}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div style={styles.submitButton}>
|
||||
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
|
||||
<p className="text-white mb-0">Make Authentication</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showVerify && <Verify phoneId={phoneId} onVerify={handleVerify} />}
|
||||
{showPreview && <Preview />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
|
||||
|
||||
const styles = {
|
||||
section: {
|
||||
margin: '1rem 0 1rem 0'
|
||||
},
|
||||
formGroup: {
|
||||
marginTop: '-45px',
|
||||
},
|
||||
selectWrapper: {
|
||||
position: 'relative',
|
||||
marginTop: '0',
|
||||
},
|
||||
select: {
|
||||
width: '100%',
|
||||
paddingRight: '30px',
|
||||
flex: 1,
|
||||
fontSize: '16px',
|
||||
border: 'none',
|
||||
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',
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center', // Center vertically within container
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
height: '47px',
|
||||
margin: 'auto 0',
|
||||
},
|
||||
button: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0', // Kurangi padding tombol
|
||||
margin: '0', // Hilangkan margin antar tombol
|
||||
fontSize: '12px',
|
||||
},
|
||||
icon: {
|
||||
fontSize: '14px',
|
||||
},
|
||||
submitButton: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: '1.2rem',
|
||||
textAlign: 'start',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
uploadError: {
|
||||
color: 'red',
|
||||
fontSize: '12px',
|
||||
marginTop: '5px',
|
||||
},
|
||||
|
||||
containerResultStyle: {
|
||||
padding: '20px',
|
||||
border: '1px solid #0053b3',
|
||||
borderRadius: '5px',
|
||||
width: '100%',
|
||||
margin: '20px auto',
|
||||
},
|
||||
resultContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
tableStyle: {
|
||||
width: '60%',
|
||||
borderCollapse: 'collapse',
|
||||
},
|
||||
imageContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
width: '30%',
|
||||
marginLeft: '10px'
|
||||
},
|
||||
imageStyle: {
|
||||
width: '193px',
|
||||
height: '242px',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000,
|
||||
},
|
||||
spinner: {
|
||||
border: '4px solid rgba(0, 0, 0, 0.1)',
|
||||
borderLeftColor: '#0542cc',
|
||||
borderRadius: '50%',
|
||||
width: '90px',
|
||||
height: '90px',
|
||||
animation: 'spin 1s ease-in-out infinite',
|
||||
},
|
||||
preview: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542cc',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
marginTop: '1rem',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
};
|
691
src/screens/Wa/Verify/Section/Message.jsx
Normal file
691
src/screens/Wa/Verify/Section/Message.jsx
Normal file
@ -0,0 +1,691 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronDown, faChevronLeft, faCloudUploadAlt, faTimes, faImage } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FileUploader } from 'react-drag-drop-files';
|
||||
|
||||
|
||||
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 [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('single');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsSelectOpen(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsSelectOpen(false);
|
||||
};
|
||||
|
||||
const handleClick = async () => {
|
||||
console.log('Make SMS Demo');
|
||||
};
|
||||
|
||||
const handleImageUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file && (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg')) {
|
||||
console.log('File uploaded:', file.name); // Handle the file upload
|
||||
} else {
|
||||
alert('Please upload a valid image file (JPG, JPEG).');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Tab buttons */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setActiveTab('single')}
|
||||
style={activeTab === 'single' ? styles.tabButton : styles.tabInactiveButton}
|
||||
>
|
||||
Single Message
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('bulk')}
|
||||
style={activeTab === 'bulk' ? styles.tabButton : styles.tabInactiveButton}
|
||||
>
|
||||
Bulk Message
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Conditional content based on active tab */}
|
||||
{activeTab === 'single' && <SingleMessage
|
||||
isSelectOpen={isSelectOpen}
|
||||
handleFocus={handleFocus}
|
||||
handleBlur={handleBlur}
|
||||
handleClick={handleClick}
|
||||
/>}
|
||||
{activeTab === 'bulk' && <BulkMessage
|
||||
isSelectOpen={isSelectOpen}
|
||||
handleFocus={handleFocus}
|
||||
handleBlur={handleBlur}
|
||||
handleClick={handleClick}
|
||||
handleImageUpload={handleImageUpload}
|
||||
fileInputRef={fileInputRef}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Message;
|
||||
|
||||
const styles = {
|
||||
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',
|
||||
},
|
||||
tabButton: {
|
||||
display: 'inline-block',
|
||||
padding: '10px 20px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#0542cc',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
marginRight: '10px',
|
||||
borderRadius: '7px'
|
||||
},
|
||||
tabInactiveButton: {
|
||||
display: 'inline-block',
|
||||
padding: '10px 20px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#fff',
|
||||
color: '#0542cc',
|
||||
border: '1px solid #0542cc',
|
||||
marginRight: '10px',
|
||||
borderRadius: '7px'
|
||||
},
|
||||
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',
|
||||
},
|
||||
waMessagePreview: {
|
||||
backgroundColor: '#F8F9FA',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #dcdcdc',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
preview: {
|
||||
marginTop: '20px',
|
||||
},
|
||||
// Bulk Message
|
||||
uploadArea: {
|
||||
backgroundColor: '#e6f2ff',
|
||||
height: '250px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '1rem',
|
||||
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',
|
||||
},
|
||||
fileWrapper: {
|
||||
backgroundColor: '#fff',
|
||||
border: '0.2px solid gray',
|
||||
padding: '15px 0 0 17px',
|
||||
borderRadius: '5px',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
imageIcon: {
|
||||
color: '#0542cc',
|
||||
fontSize: '24px',
|
||||
marginBottom: '1rem'
|
||||
},
|
||||
closeIcon: {
|
||||
color: 'red',
|
||||
cursor: 'pointer',
|
||||
fontSize: '26px',
|
||||
marginRight: '1rem',
|
||||
marginBottom: '1rem'
|
||||
},
|
||||
customLabel: {
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
color: '#212529'
|
||||
},
|
||||
submitButton: {
|
||||
marginTop: '20px',
|
||||
},
|
||||
};
|
7
src/screens/Wa/Verify/Section/index.js
Normal file
7
src/screens/Wa/Verify/Section/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Auth from "./Auth";
|
||||
import Message from "./Message";
|
||||
|
||||
export {
|
||||
Auth,
|
||||
Message
|
||||
}
|
114
src/screens/Wa/Verify/Verify.jsx
Normal file
114
src/screens/Wa/Verify/Verify.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Auth as AuthWa,
|
||||
Message as MessageWa
|
||||
} from './Section';
|
||||
|
||||
const Verify = () => {
|
||||
const verifyTabs = [
|
||||
{ name: 'Auth', link: 'wa-auth' },
|
||||
{ name: 'Message', link: 'wa-message' }
|
||||
];
|
||||
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Redirect otomatis ke rute default saat akses ke /wa-verify
|
||||
useEffect(() => {
|
||||
if (window.location.pathname === '/wa-verify') {
|
||||
navigate('wa-auth', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
// Update state isMobile berdasarkan ukuran layar
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container" style={styles.container}>
|
||||
{/* Static Content */}
|
||||
<div className="row-card border-left border-primary shadow mb-4" style={styles.welcomeCard}>
|
||||
<div className="d-flex flex-column justify-content-start align-items-start p-4">
|
||||
<h4 className="mb-3 text-start">
|
||||
<i className="fas fa-warning fa-bold me-3"></i>Alert
|
||||
</h4>
|
||||
<p className="mb-0 text-start">
|
||||
Get started now by creating an Application ID and explore all the demo services available on the dashboard.
|
||||
Experience the ease and flexibility of trying out all our features firsthand.
|
||||
</p>
|
||||
<div className="d-flex flex-row mt-3">
|
||||
<Link to="/createApps" style={{ textDecoration: 'none' }}>
|
||||
<button className="btn d-flex justify-content-center align-items-center me-2" style={styles.createButton}>
|
||||
<i className="fas fa-plus text-white me-2"></i>
|
||||
<p className="text-white mb-0">Create New App ID</p>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div style={styles.section}>
|
||||
<div className={`d-flex ${isMobile ? 'flex-column' : 'flex-row'} justify-content-between align-items-center mb-3`}>
|
||||
<div className="d-flex flex-wrap">
|
||||
{verifyTabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.link}
|
||||
to={tab.link}
|
||||
className={`btn ${window.location.pathname.includes(tab.link) ? 'btn-primary' : 'btn-light'} me-2 mb-2`}
|
||||
style={styles.tabLink}
|
||||
>
|
||||
{tab.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Tab Content */}
|
||||
<div className="tab-content">
|
||||
<Routes>
|
||||
<Route path="wa-auth" element={<AuthWa />} />
|
||||
<Route path="wa-message" element={<MessageWa />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Verify;
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
marginTop: '3%',
|
||||
padding: '0 15px',
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: '#E2FBEA',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '5px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
createButton: {
|
||||
backgroundColor: '#0542CC',
|
||||
},
|
||||
section: {
|
||||
padding: '20px',
|
||||
border: '0.1px solid rgba(0, 0, 0, 0.2)',
|
||||
borderLeft: '4px solid #0542CC',
|
||||
borderRadius: '10px',
|
||||
width: '100%',
|
||||
},
|
||||
tabLink: {
|
||||
padding: '10px 20px',
|
||||
},
|
||||
};
|
5
src/screens/Wa/Verify/index.js
Normal file
5
src/screens/Wa/Verify/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Verify from "./Verify";
|
||||
|
||||
export {
|
||||
Verify
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user