Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1f92f55
LF-5182 Commit phase 1 agent completion
kathyavini Mar 2, 2026
ceba077
Merge branch 'integration' into LF-5182-experiment-tape-plan-phase-1-…
SayakaOno Mar 2, 2026
69e9dce
LF-5182 Adjust getSurveyVersion
SayakaOno Mar 2, 2026
73aae23
LF-5182 Add useTapeSurveyJsonForFarmCountry hook
SayakaOno Mar 3, 2026
ccf2d7b
LF-5182 Use useTapeSurveyJsonForFarmCountry hook in TAPESurvey
SayakaOno Mar 3, 2026
8ae4dc9
LF-5182 Replace tapeQuestions.json with useTapeSurveyJsonForFarmCount…
SayakaOno Mar 3, 2026
8176e0d
LF-5182 Update survey json path, add useGetTapeSurveyQuery in tapeSur…
SayakaOno Mar 3, 2026
cbfa4f2
LF-5182 Get isTapeSurveyCompleted status from via the API
SayakaOno Mar 3, 2026
dc22279
LF-5182 Export prefetch from tapeSurveyApi
SayakaOno Mar 3, 2026
aa8808b
LF-5182 Adjust handleComplete
SayakaOno Mar 3, 2026
a0ad9b2
LF-5182 Adjust tapeSurveyStatusSelector
SayakaOno Mar 3, 2026
de015c3
LF-5182 Update tapeSurveyStatusSelector
SayakaOno Mar 3, 2026
a89b028
LF-5182 Fix initialData in TAPESurvey
SayakaOno Mar 3, 2026
9c63f5b
LF-5182 Fix tape's insightData
SayakaOno Mar 4, 2026
301adda
LF-5182 Pass isCompactSideMenu prop to TapeSurvey in routes
SayakaOno Mar 4, 2026
3a58c8d
LF-5182 Adjust the survey.js error position
SayakaOno Mar 4, 2026
759f726
LF-5182 Handle survey json loading error with snackbar
SayakaOno Mar 5, 2026
cc9fb45
LF-5182 Refactor tape
SayakaOno Mar 5, 2026
412f230
LF-5182 Remove useTapeSurveyJsonForFarmCountry hook
SayakaOno Mar 5, 2026
273c164
LF-5182 Conditionally show TAPE in insights
SayakaOno Mar 5, 2026
1dfb67f
LF-5182 Update TapeResults to use surveyData in DB
SayakaOno Mar 6, 2026
52dec6d
LF-5182 Update tapeSurveySlice
SayakaOno Mar 6, 2026
cd441fe
LF-5182 Update and add function to reopen survey
SayakaOno Mar 6, 2026
44ad39d
LF-5182 Update submitTapeSurvey endpoint in tapeSurveyApi, update mut…
SayakaOno Mar 6, 2026
502720e
LF-5182 Adjust TapeSurveyRecord interface
SayakaOno Mar 9, 2026
4442369
LF-5182 Replace submission_type with id
SayakaOno Mar 9, 2026
dd99b55
LF-5182 Handle update tape survey
SayakaOno Mar 9, 2026
08b13c6
LF-5182 Rename submitTapeSurvey endpoint
SayakaOno Mar 9, 2026
c2344aa
LF-5182 Fix isTapeSurveyCompleted condition in Insights
SayakaOno Mar 9, 2026
4f40909
LF-5182 Update comment
SayakaOno Mar 9, 2026
8ee6bfe
Merge branch 'integration' into LF-5182-experiment-tape-plan-phase-1-…
SayakaOno Mar 10, 2026
17595e0
LF-5182 Delete tapeQuestions.json
SayakaOno Mar 10, 2026
1f11427
LF-5182 Correct types for tapeSurveyApi
SayakaOno Mar 10, 2026
447b4dc
LF-5182 Fix API args in TapeSurvey
SayakaOno Mar 10, 2026
1176ae1
Comment out TapeSurvey Step 2
SayakaOno Mar 10, 2026
172e8e2
LF-5182 Fix isTapeSurveyCompleted condition
SayakaOno Mar 10, 2026
de7b624
LF-5182 Remove code for TAPE survey update
SayakaOno Mar 10, 2026
f6c4421
LF-5182 Restore comment
SayakaOno Mar 10, 2026
319686a
LF-5182 Redirect user to survey when surveyData doesn't exist
SayakaOno Mar 10, 2026
575498a
LF-5182 Handle TAPE results errors
SayakaOno Mar 10, 2026
c37a76f
Merge branch 'integration' into LF-5182-experiment-tape-plan-phase-1-…
SayakaOno Mar 10, 2026
e9d1fff
LF-5182 Remove Tape survey step 2 code
SayakaOno Mar 11, 2026
3dc6153
LF-5182 Update Tape result calculation
SayakaOno Mar 11, 2026
3243b03
LF-5182 Disable TAPE menu onClick while fetching
SayakaOno Mar 11, 2026
cc9a2f5
LF-5182 Make TapeResults chart responsive
SayakaOno Mar 11, 2026
1a87abb
LF-5182 Adjust TAPE components styles
SayakaOno Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/webapp/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,9 @@
"TAPE": {
"COMPLETED": "Completed - See your results",
"IN_PROGRESS": "In progress",
"LOAD_ERROR": "Failed to load survey. Please try again later.",
"NOT_FILLED": "Not filled",
"RESULTS_LOAD_ERROR": "Failed to load results. Please try again later.",
"RESULTS_TITLE": "Characterization of the Agroecological Transitions (CAET) Scores",
"TITLE": "TAPE Survey (Tool for Agroecology Performance Evaluation)",
"UPDATE_ANSWERS": "Update your answers"
Expand Down
1 change: 1 addition & 0 deletions packages/webapp/src/apiConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const marketProductCategoryUrl = `${URI}/market_product_categories`;
export const marketDirectoryPartnersUrl = `${URI}/market_directory_partners`;
export const supportTicketUrl = `${URI}/support_ticket`;
export const offlineEventLogUrl = `${URI}/offline_event_log`;
export const tapeSurveyUrl = `${URI}/tape_survey`;

export const url = URI;

Expand Down
10 changes: 5 additions & 5 deletions packages/webapp/src/components/SurveyComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
*/

import { useCallback, useEffect, useMemo } from 'react';
import { Model } from 'survey-core';
import { CompleteEvent, Model } from 'survey-core';
import { Survey } from 'survey-react-ui';
import { DefaultLight } from 'survey-core/themes';
import 'survey-core/survey-core.css';

interface SurveyComponentProps {
surveyJson: any; // Survey JSON schema object
onComplete: (currentPageNo: number, surveyData: any) => void;
onComplete: (surveyData: any, options: CompleteEvent) => void;
initialData?: Record<string, any>;
initialPageNo?: number;
onCurrentPageChanged?: (currentPageNo: number, surveyData: Record<string, any>) => void;
Expand Down Expand Up @@ -62,9 +62,9 @@ export default function SurveyComponent({

// https://surveyjs.io/form-library/documentation/get-started-react
const handleComplete = useCallback(
(surveyModel: Model) => {
const { currentPageNo, surveyData } = extractSurveyState(surveyModel);
onComplete(currentPageNo, surveyData);
(surveyModel: Model, options: CompleteEvent) => {
const { surveyData } = extractSurveyState(surveyModel);
onComplete(surveyData, options);
},
[onComplete],
);
Expand Down
172 changes: 67 additions & 105 deletions packages/webapp/src/containers/Insights/TapeSurvey/TapeResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { Radar } from 'react-chartjs-2';
Expand All @@ -25,72 +26,18 @@ import {
Filler,
Tooltip,
} from 'chart.js';
import { tapeSurveySelector, reopenSurvey } from './tapeSurveySlice';
import styles from './styles.module.scss';
import insightStyles from '../styles.module.scss';
import { Semibold } from '../../../components/Typography';
import PageTitle from '../../../components/PageTitle';
import TapeQuestions from './tapeQuestions.json';
import { roundToOne } from '../../../util/rounding';
import Button from '../../../components/Form/Button';
import { ReactComponent as EditIcon } from '../../../assets/images/edit.svg';
import { useGetTapeSurveyQuery } from '../../../store/api/tapeSurveyApi';
import { enqueueErrorSnackbar, snackbarSelector } from '../../Snackbar/snackbarSlice';

const CHART_COLOR = 'rgba(85, 143, 112, 1)'; // --Colors-Secondary-Secondary-green-700
const CHART_FILL_COLOR = 'rgba(85, 143, 112, 0.2)'; // reduced opacity
const MAX_SCORE = 100;

const STEP_TWO_SURVEY_NAMES = [
'Qualitative economic indicator',
'Land tenure',
'Food and nutrition',
'Dietary diversity',
'Youth employment and aspiration',
'Soil health',
];

const getChartTitleFromSurveyTitle = (surveyTitle: unknown) => {
if (!surveyTitle || typeof surveyTitle !== 'string') return '';

// Remove the section number from the title
const titleWithoutSectionNumber = surveyTitle.split(/\s+/).slice(1).join(' ');

if (titleWithoutSectionNumber === '') return '';

return (
titleWithoutSectionNumber.charAt(0).toUpperCase() +
titleWithoutSectionNumber.slice(1).toLowerCase()
);
};

const getAnswerKeys = (element: any): string[] => {
if (Array.isArray(element.elements)) {
return element.elements.flatMap(getAnswerKeys);
}
return element.name ? [element.name] : [];
};

const CHOSEN_SECTION_NAMES = [
'diversity',
'synergy',
'recycling',
'efficiency',
'resilience',
'culture_and_food',
'cocreation_and_knowledge',
'human_and_social',
'responsible_governance',
];

const CHART_SECTION_DATA = TapeQuestions.pages.reduce<ChartSection[]>((acc, cv) => {
if (CHOSEN_SECTION_NAMES.includes(cv.name)) {
acc.push({
dimension: getChartTitleFromSurveyTitle(cv.title),
answerKeys: getAnswerKeys(cv),
maxScore: MAX_SCORE,
});
return acc;
}
return acc;
}, []);
const RAW_MAX_SCORE = 4;

ChartJS.register(RadialLinearScale, PointElement, LineElement, Filler, Tooltip);

Expand All @@ -105,9 +52,26 @@ function TAPEResults() {
const history = useHistory();
const dispatch = useDispatch();

const { surveyData } = useSelector(tapeSurveySelector);

const tapeData = analyzeTAPEData(surveyData);
const { data: surveyData, error: surveyDataError } = useGetTapeSurveyQuery();
const { survey_response } = surveyData || {};
const notifications: { message: string }[] = useSelector(snackbarSelector);

useEffect(() => {
// Redirect back to survey page if no saved survey data is found
// (e.g. if user tries to access results page directly without completing survey)
if (surveyDataError && 'status' in surveyDataError && surveyDataError?.status === 404) {
history.replace('/Insights/tape');
} else if (surveyDataError) {
const activeError = notifications.find(
({ message }) => message === t('INSIGHTS.TAPE.RESULTS_LOAD_ERROR'),
);
if (!activeError) {
dispatch(enqueueErrorSnackbar(t('INSIGHTS.TAPE.RESULTS_LOAD_ERROR')));
}
}
}, [surveyDataError]);

const tapeData = survey_response ? analyzeTAPEData(survey_response) : [];

const chartData = {
labels: tapeData.map((d) => d.dimension),
Expand Down Expand Up @@ -140,6 +104,13 @@ function TAPEResults() {
font: {
size: 14,
},
// Splits labels into a maximum of 2 lines (assumes English labels)
callback: (label: any) => {
const words = label.split(' ');
const splitIndex = words.length === 1 ? 1 : Math.floor(words.length / 2);

return [words.slice(0, splitIndex).join(' '), words.slice(splitIndex).join(' ')];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh it takes arrays for the labels! Thank you for figuring that out ❤️

},
},
},
},
Expand All @@ -152,65 +123,56 @@ function TAPEResults() {
},
};

const returnToSurvey = () => {
dispatch(reopenSurvey());
history.push('/insights/tape');
};

return (
<>
<div className={insightStyles.insightContainer}>
<PageTitle title={t('INSIGHTS.TAPE.TITLE')} backUrl="/Insights" />
<div className={styles.resultsContainer}>
<div className={styles.sectionContainer}>
<div className={styles.buttonContainer}>
<Button sm color="secondary-2" onClick={returnToSurvey}>
{t('INSIGHTS.TAPE.UPDATE_ANSWERS')}
<EditIcon className={styles.editIcon} />
</Button>
</div>
<Semibold className={styles.titleText}>{t('INSIGHTS.TAPE.RESULTS_TITLE')}</Semibold>
{tapeData && tapeData.length > 0 && (
<div className={styles.chartContainer}>
<Radar data={chartData} options={options} />
</div>
)}
</div>
<div className={styles.sectionContainer}>
<Semibold className={styles.titleText}>Step 2 - Core Criteria of Performance</Semibold>
<div className={styles.stepTwoButtonContainer}>
{
/* Placeholders for Step 2 content. Ultimately these will link to distinct surveys for each section */
STEP_TWO_SURVEY_NAMES.map((name) => (
<Button key={name} sm color="secondary">
{name}
</Button>
))
}
<div className={styles.chartContainerWrapper}>
{tapeData && tapeData.length > 0 && (
<div className={styles.chartContainer}>
<Radar data={chartData} options={options} />
</div>
)}
</div>
</div>
</div>
</>
</div>
);
}
interface ChartSection {
dimension: string;
answerKeys: string[];
maxScore: number;
}

const DIMENSION_MAPPING = {
Diversity: 'diversity_1',
Synergy: 'synergy_2',
Recycling: 'recycling_3',
Efficiency: 'efficiency_4',
Resilience: 'resilience_5',
'Culture and food traditions': 'culture_6',
'Co-creation and sharing of knowledge': 'knowledge_7',
'Human and social values': 'human_8',
'Circular economy and solidarity': 'circular_9',
'Responsible governance': 'governance_10',
};

const analyzeTAPEData = (data: any): TAPEDimension[] => {
if (!data) return [];

return CHART_SECTION_DATA.map(({ dimension, answerKeys, maxScore }) => {
return Object.entries(DIMENSION_MAPPING).map(([dimension, prefix]) => {
const scores = Object.keys(data)
.filter((key) => key.startsWith(prefix))
.map((key) => Number(data[key]) || 0);

if (!scores.length) {
return { dimension, score: 0, maxScore: MAX_SCORE };
}

const averageRawScore = scores.reduce((sum, value) => sum + value, 0) / scores.length;

return {
dimension,
score:
25 *
(answerKeys.reduce<number>((acc, cv) => {
return data[cv] ? acc + Number(data[cv]) : acc;
}, 0) /
answerKeys.length), // simple average
maxScore,
score: (averageRawScore / RAW_MAX_SCORE) * MAX_SCORE,
maxScore: MAX_SCORE,
};
});
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2026 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

const COUNTRY_VERSIONS = ['AU'];

/**
* Returns the survey version key used to fetch the correct JSON from DO CDN,
* based on the farm's 2-letter ISO country code.
*
* To add a new country-specific version:
* 1. Upload `tape_surveys/<key>.json` to the DO Spaces bucket.
* 2. Add a country_code to `COUNTRY_VERSIONS`.
*/
export const getSurveyVersion = (countryCode: string | undefined): string | undefined => {
return countryCode && COUNTRY_VERSIONS.includes(countryCode)
? countryCode.toLowerCase()
: undefined;
};
Loading
Loading