diff --git a/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx b/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx index b7a038e..d540f57 100644 --- a/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx +++ b/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx @@ -143,7 +143,7 @@ const AdminDashboard = () => {
-
+
{ const checked = e.target.checked; setShowPreviousNext(checked); - if (checked) { - try { - setFetchingNextQuarter(true); - - const currentQuarter = quarterPeriods?.current_quarter || "Q4"; - const currentYear = quarterPeriods?.current_year || 2025; - - const establishmentId = submissionData?.establishment?.id; - const productId = submissionData?.products?.[0]?.product?.id; - - if (!establishmentId || !currentQuarter || !currentYear || !productId) { - console.error("Missing required parameters:", { - establishmentId, - currentQuarter, - currentYear, - productId, - }); - return; - } - - // API call - const response = await getBeforePreviousData( - establishmentId, - currentQuarter, - currentYear, - productId -); - - let mappedDataMap = {}; - - if (response?.data) { - submissionData.products.forEach((product) => { - const pid = product?.product?.id; - if (pid) { - mappedDataMap[pid] = { - forecast_quantity_period_one: - response.data.current_quantity_period_one ?? 0, - forecast_quantity_period_two: - response.data.current_quantity_period_two ?? 0, - forecast_quantity_period_three: - response.data.current_quantity_period_three ?? 0, - - forecast_cost_period_one: - response.data.current_cost_period_one ?? 0, - forecast_cost_period_two: - response.data.current_cost_period_two ?? 0, - forecast_cost_period_three: - response.data.current_cost_period_three ?? 0, - }; - } - }); - } else { - // if no backend data - submissionData.products.forEach((product) => { - const pid = product?.product?.id; - if (pid) { - mappedDataMap[pid] = { - forecast_quantity_period_one: 0, - forecast_quantity_period_two: 0, - forecast_quantity_period_three: 0, - forecast_cost_period_one: 0, - forecast_cost_period_two: 0, - forecast_cost_period_three: 0, - }; - } - }); - } - - setNextQuarterData(mappedDataMap); - - // Calculate previous-previous quarter UI labels - const q = Number(currentQuarter.replace("Q", "")); - let historicalQuarter = `Q${q - 2}`; - let historicalYear = currentYear; - - if (q - 2 <= 0) { - historicalQuarter = `Q${4 + (q - 2)}`; - historicalYear = currentYear - 1; - } - - setAdditionalForecastQuarter(historicalQuarter); - setAdditionalForecastYear(historicalYear); - setAdditionalForecastMonths( - getMonthsForQuarter(historicalQuarter, historicalYear) - ); - - } catch (error) { - console.error("Error fetching before-previous data:", error); - } finally { - setFetchingNextQuarter(false); - } - - } else { - // On uncheck, clear values + // Uncheck → reset + if (!checked) { setNextQuarterData({}); setAdditionalForecastQuarter(""); setAdditionalForecastYear(""); setAdditionalForecastMonths(["", "", ""]); + return; + } + + try { + setFetchingNextQuarter(true); + + const establishmentId = submissionData?.establishment?.id; + const productId = submissionData?.products?.[0]?.product?.id; + + if (!establishmentId || !productId) { + console.error("Missing required params"); + return; + } + + // API Call + const response = await getBeforePreviousData( + establishmentId, + quarterPeriods?.current_quarter, + quarterPeriods?.current_year, + productId + ); + + + let mappedDataMap = {}; + + submissionData?.products?.forEach((product) => { + const pid = product?.product?.id; + if (!pid) return; + + mappedDataMap[pid] = { + forecast_quantity_period_one: + response?.data?.forecast_quantity_period_one ?? 0, + forecast_quantity_period_two: + response?.data?.forecast_quantity_period_two ?? 0, + forecast_quantity_period_three: + response?.data?.forecast_quantity_period_three ?? 0, + + forecast_cost_period_one: + response?.data?.forecast_cost_period_one ?? 0, + forecast_cost_period_two: + response?.data?.forecast_cost_period_two ?? 0, + forecast_cost_period_three: + response?.data?.forecast_cost_period_three ?? 0, + }; + }); + + setNextQuarterData(mappedDataMap); + + // DIRECTLY FROM API – NO CALCULATION + const qp = response?.data?.quarter_periods; + + setAdditionalForecastQuarter(qp?.forecast_quarter); + setAdditionalForecastYear(qp?.forecast_year); + setAdditionalForecastMonths([ + qp?.forecast_month?.forecast_period_one, + qp?.forecast_month?.forecast_period_two, + qp?.forecast_month?.forecast_period_three, + ]); + + } catch (error) { + console.error("Error fetching forecast data:", error); + } finally { + setFetchingNextQuarter(false); } }; + + // Helper to get forecast data for a specific product const getCurrentYearForecastData = (productId) => { if (!showPreviousNext || !productId) { @@ -920,7 +893,7 @@ const getQuarterYearLabel = (quarter, year) => { const previousYear = quarterPeriods?.previous_year || 2025; const currentQuarter = quarterPeriods?.current_quarter || 'Q4'; const currentYear = quarterPeriods?.current_year || 2025; - const forecastQuarter = quarterPeriods?.forecast_quarter || 'Q1'; + const forecastQuarter = quarterPeriods?.forecast_quarter || '-'; const forecastYear = quarterPeriods?.forecast_year || 2026; // Get dynamic month information from API response @@ -962,8 +935,6 @@ const additionalForecastLabel = ? `${additionalForecastQuarter} ${additionalForecastYear}` : ''; - - const establishmentDetails = [ { label: 'Establishment Name', @@ -1140,7 +1111,7 @@ const additionalForecastLabel =
)} @@ -1175,11 +1147,14 @@ const additionalForecastLabel =
- {varianceHighlighting && ( -
- Cells with variance > {threshold}% will be highlighted -
- )} + {varianceHighlighting && ( +
+ Cells with variance{" "} + {threshold <= 100 ? `≤ ${threshold}%` : "> 100%"} will be highlighted +
+)} + + diff --git a/ipi-survey-platform/src/pages/Admin/Validations.jsx b/ipi-survey-platform/src/pages/Admin/Validations.jsx index 7795f50..ada6014 100644 --- a/ipi-survey-platform/src/pages/Admin/Validations.jsx +++ b/ipi-survey-platform/src/pages/Admin/Validations.jsx @@ -444,7 +444,7 @@ React.useEffect(() => {

- Manage Submissions ({filtered.length}) + Manage Submissions ({filtered.length})

diff --git a/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx b/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx index ed31f26..7419ae0 100644 --- a/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx +++ b/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx @@ -30,26 +30,27 @@ const ToggleableInput = ({ error ? "border-red-500" : "border-[#92722A]" } focus:border-[#92722A] focus:ring-0 px-4 pr-11 text-sm bg-white`} /> - + {type === 'password' && ( + + )}
{error &&

{error}

}
@@ -220,49 +221,50 @@ if (newPassword !== confirmPassword) {
setOtp(e.target.value)} - show={showOtp} - toggleShow={() => setShowOtp((v) => !v)} - placeholder="Enter 6-digit code" - error={otpError} - autoComplete="one-time-code" -/> + label="Verification Code" + required + value={otp} + onChange={(e) => setOtp(e.target.value)} + placeholder="Enter 6-digit code" + error={otpError} + autoComplete="one-time-code" + type="text" + /> - { - const value = e.target.value; - setNewPassword(value); - const passwordError = validatePassword(value); - setNewPasswordError(passwordError); - }} - autoComplete="new-password" - show={showNewPassword} - toggleShow={() => setShowNewPassword((v) => !v)} - placeholder="Enter new password" - error={newPasswordError} -/> + { + const value = e.target.value; + setNewPassword(value); + const passwordError = validatePassword(value); + setNewPasswordError(passwordError); + }} + type="password" + autoComplete="new-password" + show={showNewPassword} + toggleShow={() => setShowNewPassword((v) => !v)} + placeholder="Enter new password" + error={newPasswordError} + /> - { - const value = e.target.value; - setConfirmPassword(value); - if (newPassword === value) setConfirmPasswordError(""); - }} - autoComplete="new-password" - show={showConfirmPassword} - toggleShow={() => setShowConfirmPassword((v) => !v)} - placeholder="Re-enter new password" - error={confirmPasswordError} -/> + { + const value = e.target.value; + setConfirmPassword(value); + if (newPassword === value) setConfirmPasswordError(""); + }} + type="password" + autoComplete="new-password" + show={showConfirmPassword} + toggleShow={() => setShowConfirmPassword((v) => !v)} + placeholder="Re-enter new password" + error={confirmPasswordError} + /> {newPassword && (
  • diff --git a/ipi-survey-platform/src/services/api/CommonService.js b/ipi-survey-platform/src/services/api/CommonService.js index 138db97..eafa668 100644 --- a/ipi-survey-platform/src/services/api/CommonService.js +++ b/ipi-survey-platform/src/services/api/CommonService.js @@ -1,12 +1,29 @@ -import apiClient from '@/services/api/apiClient'; +import apiClient, { fetchCsrfToken } from './apiClient'; + +const withCsrf = async (requestFn, ...args) => { + try { + await fetchCsrfToken(); + return await requestFn(...args); + } catch (error) { + if (error.response?.status === 403 && error.response?.data?.message?.includes('CSRF')) { + // If we get a CSRF error, try to refresh the token and retry once + await fetchCsrfToken(); + return await requestFn(...args); + } + throw error; + } +}; export const getRequest = (url, config = {}) => apiClient.get(url, config); -export const postRequest = (url, data, config = {}) => apiClient.post(url, data, config); +export const postRequest = (url, data, config = {}) => + withCsrf(apiClient.post.bind(apiClient), url, data, config); -export const putRequest = (url, data, config = {}) => apiClient.put(url, data, config); +export const putRequest = (url, data, config = {}) => + withCsrf(apiClient.put.bind(apiClient), url, data, config); -export const deleteRequest = (url, config = {}) => apiClient.delete(url, config); +export const deleteRequest = (url, config = {}) => + withCsrf(apiClient.delete.bind(apiClient), url, config); export default { getRequest, diff --git a/ipi-survey-platform/src/services/api/apiClient.js b/ipi-survey-platform/src/services/api/apiClient.js index 0d34ef7..b936be4 100644 --- a/ipi-survey-platform/src/services/api/apiClient.js +++ b/ipi-survey-platform/src/services/api/apiClient.js @@ -4,6 +4,9 @@ const baseURL = import.meta.env.VITE_API_BASE_URL; const appSignature = import.meta.env.VITE_API_APP_SIGNATURE; const fallbackToken = import.meta.env.VITE_API_TOKEN; +// Store CSRF token in memory +let csrfToken = null; + const apiClient = axios.create({ baseURL, withCredentials: true, @@ -14,8 +17,34 @@ const apiClient = axios.create({ }, }); +/** + * Fetches and stores a new CSRF token from the server + * @returns {Promise} The CSRF token + */ +export const fetchCsrfToken = async () => { + try { + const response = await apiClient.get('/csrf-token'); + csrfToken = response.data.csrfToken; + return csrfToken; + } catch (error) { + console.error('Failed to fetch CSRF token:', error); + throw error; + } +}; + apiClient.interceptors.request.use((config) => { config.headers = config.headers ?? {}; + + // Skip auth for CSRF token endpoint + if (config.url === '/csrf-token') { + return config; + } + + // Add CSRF token for write operations + if (csrfToken && ['post', 'put', 'patch', 'delete'].includes(config.method?.toLowerCase())) { + config.headers['X-CSRF-Token'] = csrfToken; + } + if (config.skipAuth) { return config; } @@ -34,6 +63,7 @@ apiClient.interceptors.request.use((config) => { // config.headers.Authorization = `Bearer ${token}`; // } // } + return config; });