diff --git a/ipi-survey-platform/package-lock.json b/ipi-survey-platform/package-lock.json index 8b9f59e..48d243e 100644 --- a/ipi-survey-platform/package-lock.json +++ b/ipi-survey-platform/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-google-recaptcha": "^3.1.0", "react-router-dom": "^7.9.4" }, "devDependencies": { @@ -2729,6 +2730,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2810,7 +2820,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3159,6 +3168,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3295,6 +3316,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3444,6 +3474,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3469,6 +3510,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-async-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz", + "integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -3481,6 +3535,25 @@ "react": "^19.2.0" } }, + "node_modules/react-google-recaptcha": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz", + "integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.0", + "react-async-script": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/ipi-survey-platform/package.json b/ipi-survey-platform/package.json index 0481683..e03f330 100644 --- a/ipi-survey-platform/package.json +++ b/ipi-survey-platform/package.json @@ -13,6 +13,7 @@ "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-google-recaptcha": "^3.1.0", "react-router-dom": "^7.9.4" }, "devDependencies": { diff --git a/ipi-survey-platform/src/App.jsx b/ipi-survey-platform/src/App.jsx index befd636..b44aa1b 100644 --- a/ipi-survey-platform/src/App.jsx +++ b/ipi-survey-platform/src/App.jsx @@ -11,6 +11,7 @@ import Login from './pages/Login/Login' import History from './pages/History/History' import ChangePassword from './pages/ChangePassword/ChangePassword' import ForgotPassword from './pages/ForgotPassword/ForgotPassword' +import PublicContactForm from './pages/PublicContactForm' const RequireRole = ({ allowedRoles = [], children }) => { let role @@ -102,6 +103,7 @@ function App() { /> } /> } /> + } /> } /> ) diff --git a/ipi-survey-platform/src/components/admin/AdminHeader.jsx b/ipi-survey-platform/src/components/admin/AdminHeader.jsx index 33ffd05..bc3c62a 100644 --- a/ipi-survey-platform/src/components/admin/AdminHeader.jsx +++ b/ipi-survey-platform/src/components/admin/AdminHeader.jsx @@ -110,7 +110,7 @@ const AdminHeader = () => { className="w-full px-4 py-2.5 text-center text-sm text-[#232528] hover:bg-[#F2ECCF]" onClick={() => { setProfileOpen(false); - navigate('/change-password'); + navigate('/reset-password'); }} > Change Password diff --git a/ipi-survey-platform/src/pages/Login/Login.jsx b/ipi-survey-platform/src/pages/Login/Login.jsx index 821590d..abfc25b 100644 --- a/ipi-survey-platform/src/pages/Login/Login.jsx +++ b/ipi-survey-platform/src/pages/Login/Login.jsx @@ -302,7 +302,7 @@ const Login = () => { diff --git a/ipi-survey-platform/src/pages/PublicContactForm.jsx b/ipi-survey-platform/src/pages/PublicContactForm.jsx new file mode 100644 index 0000000..ec64fac --- /dev/null +++ b/ipi-survey-platform/src/pages/PublicContactForm.jsx @@ -0,0 +1,316 @@ +import React, { useState, useEffect } from "react"; +import Logo from "../assets/images/FCSCLogo.svg"; +import { useNavigate } from "react-router-dom"; +import apiClient from "../services/api/apiClient.js"; + +const PublicContactForm = () => { + const navigate = useNavigate(); + + const [formData, setFormData] = useState({ + establishmentName: "", + establishmentId: "", + email: "", + contactName: "", + contactPhone: "", + notes: "", + }); + + const [errors, setErrors] = useState({}); + const [isValid, setIsValid] = useState(false); + const [captcha, setCaptcha] = useState(""); + const [userCaptcha, setUserCaptcha] = useState(""); + const [captchaError, setCaptchaError] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + + // Generate simple CAPTCHA code + const generateCaptcha = () => { + const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let code = ""; + for (let i = 0; i < 6; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + setCaptcha(code); + }; + + useEffect(() => { + generateCaptcha(); + }, []); + + // Validation rules + const validate = (data = formData) => { + const newErrors = {}; + + if (!data.establishmentName.trim()) newErrors.establishmentName = "Establishment Name is required."; + if (!data.email.trim()) newErrors.email = "Registered Email is required."; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) newErrors.email = "Enter a valid email address."; + if (!data.contactName.trim()) newErrors.contactName = "Contact Person Name is required."; + if (!data.contactPhone.trim()) { + newErrors.contactPhone = "Contact Phone is required."; + } else if (!/^\d{10,12}$/.test(data.contactPhone)) { + newErrors.contactPhone = "Enter a valid phone number (10–12 digits)."; + } + setErrors(newErrors); + setIsValid(Object.keys(newErrors).length === 0); + return newErrors; + }; + + // Restrict Contact Name: only letters and spaces +const handleNameChange = (e) => { + const { value } = e.target; + const filtered = value.replace(/[^A-Za-z\s]/g, ""); // remove numbers/special chars + setFormData((prev) => ({ ...prev, contactName: filtered })); + validate({ ...formData, contactName: filtered }); + }; + + const handlePhoneChange = (e) => { + let { value } = e.target; + + // Allow only numbers + value = value.replace(/\D/g, ""); // remove all non-numeric characters + + // Limit to 12 digits + if (value.length > 12) value = value.slice(0, 12); + + setFormData((prev) => ({ ...prev, contactPhone: value })); + validate({ ...formData, contactPhone: value }); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + const updated = { ...formData, [name]: value }; + setFormData(updated); + validate(updated); + }; + + // CAPTCHA validation + const handleCaptchaChange = (e) => { + setUserCaptcha(e.target.value.toUpperCase()); + setCaptchaError(""); + }; + + // Form submit + const handleSubmit = async (e) => { + e.preventDefault(); + setErrorMsg(""); + setSuccess(""); + + const validationErrors = validate(); + if (Object.keys(validationErrors).length > 0) return; + + if (userCaptcha !== captcha) { + setCaptchaError("Incorrect CAPTCHA. Please try again."); + generateCaptcha(); + return; + } + + // Simulate processing + setLoading(true); + setTimeout(() => { + setLoading(false); + + // ✅ Show success toast/message + setSuccess("Reset Password request successful."); + + // ✅ Clear fields and regenerate CAPTCHA + setFormData({ + establishmentName: "", + establishmentId: "", + email: "", + contactName: "", + contactPhone: "", + notes: "", + }); + setUserCaptcha(""); + generateCaptcha(); + + // ✅ After 3 seconds → clear session + route to login + setTimeout(() => { + try { + sessionStorage.removeItem("auth_token"); + sessionStorage.removeItem("user_profile"); + sessionStorage.removeItem("user_role"); + sessionStorage.removeItem("establishment_id"); + } catch { + // ignore storage errors + } + navigate("/login"); + }, 3000); + }, 1000); // small delay to simulate "submitting" + }; + + + return ( +
+
+
+ {/* Header */} +
+ FCSC +

+ Contact Establishment Info +

+

+ Provide details to contact your establishment. +

+
+ +
+ {errorMsg && ( +
+ {errorMsg} +
+ )} + {success && ( +
+ {success} +
+ )} + + {/* Inputs */} + + + + + + + {/* Notes */} +
+ + +
+ + {/* CAPTCHA */} +
+ +
+ + +
+ + {/* Accessible alternative */} +

+ If you cannot read the code, click "Refresh" or type the following:{" "} + {captcha} +

+ + + {captchaError && ( +

{captchaError}

+ )} +
+ + {/* Submit */} + + + + +
+
+
+ ); +}; + +// Input component +const InputField = ({ label, name, value, onChange, placeholder, required, error, type = "text" }) => ( +
+ + + {error &&

{error}

} +
+); + +export default PublicContactForm; \ No newline at end of file