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",
|
"bootstrap": "^5.3.3",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-drop-files": "^2.4.0",
|
"react-drag-drop-files": "^2.4.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
@ -5973,6 +5974,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"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": {
|
"node_modules/core-js": {
|
||||||
"version": "3.39.0",
|
"version": "3.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
"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": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
@ -15541,6 +15564,12 @@
|
|||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-drop-files": "^2.4.0",
|
"react-drag-drop-files": "^2.4.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
|
142
src/App.js
142
src/App.js
@ -13,53 +13,101 @@ import {
|
|||||||
CreateApps
|
CreateApps
|
||||||
} from './screens/Home';
|
} from './screens/Home';
|
||||||
import {
|
import {
|
||||||
FaceVerify,
|
Verify as FaceVerify,
|
||||||
FaceSummary,
|
Summary as FaceSummary,
|
||||||
FaceTransaction
|
Transaction as FaceTransaction
|
||||||
} from './screens/Biometric/FaceRecognition';
|
} from './screens/Biometric/FaceRecognition';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Enroll,
|
Enroll,
|
||||||
VerifySection,
|
Verify as VerifySection,
|
||||||
Liveness,
|
Liveness,
|
||||||
Compare,
|
Compare,
|
||||||
Search
|
Search
|
||||||
} from './screens/Biometric/FaceRecognition/Section';
|
} from './screens/Biometric/FaceRecognition/Section';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManageKtp,
|
Manage as ManageKtp,
|
||||||
SummaryKtp,
|
Summary as SummaryKtp,
|
||||||
VerifyKtp,
|
Verify as VerifyKtp,
|
||||||
TransactionKtp
|
Transaction as TransactionKtp
|
||||||
} from './screens/Biometric/OcrKtp';
|
} from './screens/Biometric/OcrKtp';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SummaryNpwp,
|
Summary as SummaryNpwp,
|
||||||
TransactionNpwp,
|
Transaction as TransactionNpwp,
|
||||||
VerifyNpwp
|
Verify as VerifyNpwp
|
||||||
} from './screens/Biometric/OcrNpwp';
|
} from './screens/Biometric/OcrNpwp';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VerifySim,
|
Verify as VerifySim,
|
||||||
SummarySim,
|
Summary as SummarySim,
|
||||||
TransactionSim
|
Transaction as TransactionSim
|
||||||
} from './screens/Biometric/OcrSim';
|
} from './screens/Biometric/OcrSim';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VerifyDoc,
|
Verify as VerifyDoc,
|
||||||
SummaryDoc,
|
Summary as SummaryDoc,
|
||||||
TransactionDoc
|
Transaction as TransactionDoc
|
||||||
} from './screens/Biometric/OcrDocument';
|
} from './screens/Biometric/OcrDocument';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SmsVerify
|
Verify as SmsVerify
|
||||||
} from './screens/Sms/Verification';
|
} from './screens/Sms/Verification';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VerificationAnnoncement,
|
Annoncement as VerificationAnnoncement,
|
||||||
VerificationOtp
|
Otp as VerificationOtp
|
||||||
} from './screens/Sms/Verification/Section';
|
} 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 = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@ -89,12 +137,6 @@ const App = () => {
|
|||||||
{/* Default route */}
|
{/* Default route */}
|
||||||
<Route index element={<Navigate to="face-enroll" />} />
|
<Route index element={<Navigate to="face-enroll" />} />
|
||||||
</Route>
|
</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) */}
|
{/* Biometric - Face Recognition (Summary) */}
|
||||||
<Route path="/face-summary" element={<FaceSummary />} />
|
<Route path="/face-summary" element={<FaceSummary />} />
|
||||||
@ -130,6 +172,54 @@ const App = () => {
|
|||||||
<Route index element={<Navigate to="sms-otp" />} />
|
<Route index element={<Navigate to="sms-otp" />} />
|
||||||
</Route>
|
</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 />} /> */}
|
{/* <Route path="/sms-otp-settings" element={<SmsOtpSettings />} /> */}
|
||||||
{/* Continue for each link */}
|
{/* Continue for each link */}
|
||||||
|
|
||||||
|
@ -11287,3 +11287,4 @@ body {
|
|||||||
background-color: red;
|
background-color: red;
|
||||||
font-size: 10px;
|
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 DashboardImg from './dashboard-img.png';
|
||||||
import DummyKtp from './Dummy-Ktp.png';
|
import DummyKtp from './Dummy-Ktp.png';
|
||||||
import ServerDownAnimation from './server-down.gif';
|
import ServerDownAnimation from './server-down.gif';
|
||||||
|
import WAB_1 from './WAB-1.png';
|
||||||
|
import WAB_2 from './WAB-2.png';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ProfileImage,
|
ProfileImage,
|
||||||
Logo,
|
Logo,
|
||||||
DashboardImg,
|
DashboardImg,
|
||||||
DummyKtp,
|
DummyKtp,
|
||||||
ServerDownAnimation
|
ServerDownAnimation,
|
||||||
|
WAB_1,
|
||||||
|
WAB_2
|
||||||
}
|
}
|
@ -137,10 +137,10 @@ const dataMenu = [
|
|||||||
name: 'WhatsApp Management', // Changed the name
|
name: 'WhatsApp Management', // Changed the name
|
||||||
target: 'collapseWaManage',
|
target: 'collapseWaManage',
|
||||||
subMenus: [
|
subMenus: [
|
||||||
{ name: 'Register Business Account', link: '/wa-manage-register'}, // Changed the name
|
{ name: 'Register Business Account', link: '/wa-registration'}, // Changed the name
|
||||||
{ name: 'WhatsApp Profile Settings', link: '/wa-manage-profile'}, // Changed the name
|
{ name: 'WhatsApp Profile Settings', link: '/wa-profile'}, // Changed the name
|
||||||
{ name: 'Message Templates', link: '/wa-manage-template'}, // Changed the name
|
{ name: 'Message Templates', link: '/wa-template'}, // Changed the name
|
||||||
{ name: 'Integration Settings', link: '/wa-manage-integration'}, // Changed the name
|
{ name: 'Integration Settings', link: '/wa-integration'}, // Changed the name
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -667,7 +667,7 @@ const Verify = () => {
|
|||||||
menuPlacement="auto"
|
menuPlacement="auto"
|
||||||
inputValue={inputValueApplication}
|
inputValue={inputValueApplication}
|
||||||
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
|
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{applicationError && <small style={{color: 'red'}}>{applicationError}</small>}
|
{applicationError && <small style={{color: 'red'}}>{applicationError}</small>}
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import Enroll from "./Enroll";
|
import Enroll from "./Enroll";
|
||||||
import VerifySection from "./Verify";
|
import Verify from "./Verify";
|
||||||
import Compare from "./Compare"
|
import Compare from "./Compare"
|
||||||
import Liveness from "./Liveness"
|
import Liveness from "./Liveness"
|
||||||
import Search from "./Search"
|
import Search from "./Search"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Enroll,
|
Enroll,
|
||||||
VerifySection,
|
Verify,
|
||||||
Compare,
|
Compare,
|
||||||
Liveness,
|
Liveness,
|
||||||
Search
|
Search
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
Compare,
|
Compare,
|
||||||
Liveness,
|
Liveness,
|
||||||
Search,
|
Search,
|
||||||
VerifySection
|
Verify as VerifySection
|
||||||
} from './Section';
|
} from './Section';
|
||||||
|
|
||||||
const Verify = () => {
|
const Verify = () => {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import FaceVerify from "./Verify";
|
import Verify from "./Verify";
|
||||||
import FaceSummary from "./Summary";
|
import Summary from "./Summary";
|
||||||
import FaceTransaction from "./Transaction";
|
import Transaction from "./Transaction";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FaceVerify,
|
Verify,
|
||||||
FaceSummary,
|
Summary,
|
||||||
FaceTransaction
|
Transaction
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import VerifyDoc from "./Verify";
|
import Verify from "./Verify";
|
||||||
import SummaryDoc from "./Summary";
|
import Summary from "./Summary";
|
||||||
import TransactionDoc from "./Transaction";
|
import Transaction from "./Transaction";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VerifyDoc,
|
Verify,
|
||||||
SummaryDoc,
|
Summary,
|
||||||
TransactionDoc
|
Transaction
|
||||||
}
|
}
|
@ -353,7 +353,8 @@ const Verify = () => {
|
|||||||
color: '#721c24',
|
color: '#721c24',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
margin: '0',
|
margin: '0',
|
||||||
},loadingOverlay: {
|
},
|
||||||
|
loadingOverlay: {
|
||||||
position: 'fixed', // Gunakan fixed untuk overlay penuh layar
|
position: 'fixed', // Gunakan fixed untuk overlay penuh layar
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import VerifyKtp from "./Verify";
|
import Verify from "./Verify";
|
||||||
import ManageKtp from "./Manage";
|
import Manage from "./Manage";
|
||||||
import SummaryKtp from "./Summary";
|
import Summary from "./Summary";
|
||||||
import TransactionKtp from "./Transaction";
|
import Transaction from "./Transaction";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VerifyKtp,
|
Verify,
|
||||||
ManageKtp,
|
Manage,
|
||||||
SummaryKtp,
|
Summary,
|
||||||
TransactionKtp
|
Transaction
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import VerifyNpwp from "./Verify";
|
import Verify from "./Verify";
|
||||||
import SummaryNpwp from "./Summary";
|
import Summary from "./Summary";
|
||||||
import TransactionNpwp from "./Transaction";
|
import Transaction from "./Transaction";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VerifyNpwp,
|
Verify,
|
||||||
SummaryNpwp,
|
Summary,
|
||||||
TransactionNpwp
|
Transaction
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import VerifySim from "./Verify";
|
import Verify from "./Verify";
|
||||||
import SummarySim from "./Summary";
|
import Summary from "./Summary";
|
||||||
import TransactionSim from "./Transaction";
|
import Transaction from "./Transaction";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VerifySim,
|
Verify,
|
||||||
SummarySim,
|
Summary,
|
||||||
TransactionSim
|
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 Annoncement from "./Announcement";
|
||||||
import VerificationOtp from "./Otp";
|
import Otp from "./Otp";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
VerificationAnnoncement,
|
Annoncement,
|
||||||
VerificationOtp
|
Otp
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
import { Link, Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
VerificationOtp,
|
Otp as VerificationOtp,
|
||||||
VerificationAnnoncement
|
Annoncement as VerificationAnnoncement
|
||||||
} from './Section';
|
} from './Section';
|
||||||
|
|
||||||
const Verify = () => {
|
const Verify = () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import SmsVerify from "./Verify"
|
import Verify from "./Verify"
|
||||||
|
|
||||||
export {
|
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