- {projectDashboardLoading ? (
+ {projectDetailsLoading || submissionContributorsLoading || entityOsmMapLoading ? (
{Array.from({ length: 3 }).map((i) => (
diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsInfographics.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsInfographics.tsx
index 0c7ee65893..ee91040d9a 100644
--- a/src/frontend/src/components/ProjectSubmissions/SubmissionsInfographics.tsx
+++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsInfographics.tsx
@@ -6,139 +6,145 @@ import Table, { TableHeader } from '@/components/common/CustomTable';
import CustomLineChart from '@/components/common/LineChart';
import CoreModules from '@/shared/CoreModules';
import InfographicsCard from '@/components/ProjectSubmissions/InfographicsCard';
-import {
- ProjectContributorsService,
- ProjectSubmissionInfographicsService,
- ValidatedVsMappedInfographicsService,
-} from '@/api/SubmissionService';
-import {
- submissionContributorsTypes,
- submissionInfographicsTypes,
- validatedVsMappedInfographicsTypes,
-} from '@/models/submission/submissionModel';
-import { taskSubmissionInfoType } from '@/models/task/taskModel';
-
import useDocumentTitle from '@/utilfunctions/useDocumentTitle';
+import { useAppSelector } from '@/types/reduxTypes';
+import { taskHistoryTypes } from '@/models/project/projectModel';
+import { formSubmissionType, validatedMappedType } from '@/models/submission/submissionModel';
+import { dateNDaysAgo, generateLast30Days, getMonthDate } from '@/utilfunctions/commonUtils';
-const lineKeyData = [
- {
- name: '11/25',
- Actual: 4000,
- Planned: 2400,
- amt: 2400,
- },
- {
- name: '11/26',
- Actual: 3000,
- Planned: 1398,
- amt: 2210,
- },
- {
- name: '11/27',
- Actual: 2000,
- Planned: 9800,
- amt: 2290,
- },
- {
- name: '11/28',
- Actual: 2780,
- Planned: 3908,
- amt: 2000,
- },
- {
- name: '11/29',
- Actual: 1890,
- Planned: 4800,
- amt: 2181,
- },
- {
- name: '11/30',
- Actual: 2390,
- Planned: 3800,
- amt: 2500,
- },
- {
- name: '12/01',
- Actual: 3490,
- Planned: 4300,
- amt: 2100,
- },
- {
- name: '12/02',
- Actual: 2780,
- Planned: 3908,
- amt: 2000,
- },
- {
- name: '12/03',
- Actual: 1890,
- Planned: 4800,
- amt: 2181,
- },
- {
- name: '12/04',
- Actual: 2390,
- Planned: 3800,
- amt: 2500,
- },
- {
- name: '12/05',
- Actual: 3490,
- Planned: 4300,
- amt: 2100,
- },
-];
-
-const SubmissionsInfographics = ({ toggleView }) => {
+const SubmissionsInfographics = ({ toggleView, entities }) => {
useDocumentTitle('Submission Infographics');
const formSubmissionRef = useRef(null);
const projectProgressRef = useRef(null);
const totalContributorsRef = useRef(null);
const plannedVsActualRef = useRef(null);
- const dispatch = CoreModules.useAppDispatch();
const params = CoreModules.useParams();
const projectId = params.projectId;
- const submissionInfographicsData: submissionInfographicsTypes[] = CoreModules.useAppSelector(
- (state) => state.submission.submissionInfographics,
- );
- const submissionInfographicsLoading: boolean = CoreModules.useAppSelector(
- (state) => state.submission.submissionInfographicsLoading,
- );
- const submissionContributorsData: submissionContributorsTypes[] = CoreModules.useAppSelector(
- (state) => state.submission.submissionContributors,
- );
- const submissionContributorsLoading: boolean = CoreModules.useAppSelector(
- (state) => state.submission.submissionContributorsLoading,
- );
+ const submissionContributorsData = useAppSelector((state) => state.submission.submissionContributors);
+ const submissionContributorsLoading = useAppSelector((state) => state.submission.submissionContributorsLoading);
const [submissionProjection, setSubmissionProjection] = useState<10 | 30>(10);
- const validatedVsMappedInfographics: validatedVsMappedInfographicsTypes[] = CoreModules.useAppSelector(
- (state) => state.submission.validatedVsMappedInfographics,
- );
- const validatedVsMappedLoading: boolean = CoreModules.useAppSelector(
- (state) => state.submission.validatedVsMappedLoading,
- );
- const taskInfo: taskSubmissionInfoType = CoreModules.useAppSelector((state) => state.task.taskInfo);
- const taskLoading: boolean = CoreModules.useAppSelector((state) => state.task.taskLoading);
+ const taskInfo = useAppSelector((state) => state.task.taskInfo);
+ const taskLoading = useAppSelector((state) => state.task.taskLoading);
+ const entityOsmMapLoading = useAppSelector((state) => state.project.entityOsmMapLoading);
+ const projectTaskList = useAppSelector((state) => state.project.projectTaskBoundries);
+ const projectDetailsLoading = useAppSelector((state) => state.project.projectDetailsLoading);
- useEffect(() => {
- dispatch(
- ProjectSubmissionInfographicsService(
- `${import.meta.env.VITE_API_URL}/submission/submission_page/${projectId}?days=${submissionProjection}`,
- ),
- );
- }, [submissionProjection]);
+ const today = new Date().toISOString();
+ const [formSubmissionsData, setFormSubmissionsData] = useState
([]);
+ const [validatedVsMappedInfographics, setValidatedVsMappedInfographics] = useState([]);
useEffect(() => {
- dispatch(
- ValidatedVsMappedInfographicsService(`${import.meta.env.VITE_API_URL}/tasks/activity/?project_id=${projectId}`),
- );
- }, []);
+ if (!projectTaskList || (projectTaskList && projectTaskList?.length === 0)) return;
+
+ const projectIndex = projectTaskList.findIndex((project) => project.id == +projectId);
+ // task activities history list
+ const taskActivities = projectTaskList?.[projectIndex]?.taskBoundries?.reduce((acc: taskHistoryTypes[], task) => {
+ return [...acc, ...task?.task_history];
+ }, []);
+
+ // filter activities for last 30 days
+ const taskActivities30Days = taskActivities?.filter((activity) => {
+ const actionDate = new Date(activity?.action_date).toISOString();
+ return actionDate >= dateNDaysAgo(30) && actionDate <= today;
+ });
+
+ // only filter MAPPED & VALIDATED activities
+ const groupedData: validatedMappedType[] = taskActivities30Days?.reduce((acc: validatedMappedType[], activity) => {
+ const date = activity?.action_date.split('T')[0];
+ const index = acc.findIndex((submission) => submission.date === date);
+ if (acc?.find((submission) => submission.date === date)) {
+ if (activity?.action_text?.includes('LOCKED_FOR_MAPPING to MAPPED')) {
+ acc[index].Mapped += 1;
+ }
+ if (activity?.action_text?.includes('LOCKED_FOR_VALIDATION to VALIDATED')) {
+ acc[index].Validated += 1;
+ }
+ } else {
+ const splittedDate = date?.split('-');
+ const label = `${splittedDate[1]}/${splittedDate[2]}`;
+ if (activity?.action_text?.includes('LOCKED_FOR_MAPPING to MAPPED')) {
+ acc.push({ date: date, Validated: 0, Mapped: 1, label });
+ }
+ if (activity?.action_text?.includes('LOCKED_FOR_VALIDATION to VALIDATED')) {
+ acc.push({ date: date, Validated: 1, Mapped: 0, label });
+ }
+ }
+ return acc;
+ }, []);
+ // generate validatedMapped data for last 30 days
+ const last30Days = generateLast30Days().map((datex) => {
+ const mappedVsValidatedValue = groupedData?.find((group) => {
+ return group?.date === datex;
+ });
+
+ if (mappedVsValidatedValue) {
+ return mappedVsValidatedValue;
+ } else {
+ // if no validated-mapped date - return count of 0 for both
+ const splittedDate = datex?.split('-');
+ const label = `${splittedDate[1]}/${splittedDate[2]}`;
+ return { date: datex, Validated: 0, Mapped: 0, label: label };
+ }
+ });
+
+ // sort by ascending date
+ const sortedValidatedMapped = last30Days?.sort((a, b) => {
+ const date1: any = new Date(a.date);
+ const date2: any = new Date(b.date);
+ return date1 - date2;
+ });
+
+ const cumulativeCount = {
+ validated: 0,
+ mapped: 0,
+ };
+
+ // generate cumulative count data
+ const finalData = sortedValidatedMapped?.map((submission) => {
+ cumulativeCount.validated += submission.Validated;
+ cumulativeCount.mapped += submission.Mapped;
+ return { ...submission, Validated: cumulativeCount.validated, Mapped: cumulativeCount.mapped };
+ });
+
+ setValidatedVsMappedInfographics(finalData);
+ }, [projectTaskList]);
+
+ // data for validated vs mapped graph
useEffect(() => {
- dispatch(ProjectContributorsService(`${import.meta.env.VITE_API_URL}/projects/contributors/${projectId}`));
- }, []);
+ if (entities?.length === 0) return;
+
+ // get entities updated within the last 10 or 30 days
+ const updatedEntityLastNDays = entities?.filter((entity) => {
+ const updatedDate = new Date(entity?.updated_at).toISOString();
+ return updatedDate >= dateNDaysAgo(submissionProjection) && updatedDate <= today;
+ });
+
+ // group entity submission according to date
+ const submissions: formSubmissionType[] = [];
+ updatedEntityLastNDays?.map((entity) => {
+ if (submissions?.find((submission) => submission.label === getMonthDate(entity.updated_at))) {
+ const index = submissions.findIndex((submission) => submission.label === getMonthDate(entity.updated_at));
+ submissions[index].count += 1;
+ } else {
+ submissions.push({
+ date: entity.updated_at?.split('T')[0],
+ label: getMonthDate(entity.updated_at),
+ count: 1,
+ });
+ }
+ });
+
+ // sort submissions by ascending date
+ const sortedEntitySubmissions = submissions?.sort((a, b) => {
+ const date1: any = new Date(a.date);
+ const date2: any = new Date(b.date);
+ return date1 - date2;
+ });
+ setFormSubmissionsData(sortedEntitySubmissions);
+ }, [entities, submissionProjection]);
const FormSubmissionSubHeader = () => (
@@ -163,7 +169,6 @@ const SubmissionsInfographics = ({ toggleView }) => {
const totalFeatureCount = taskInfo.reduce((total, task) => total + task.feature_count, 0);
const totalSubmissionCount = taskInfo.reduce((total, task) => total + task.submission_count, 0);
- const totalTaskCount = taskInfo.length;
const projectProgressData = [
{
names: 'Current',
@@ -182,21 +187,21 @@ const SubmissionsInfographics = ({ toggleView }) => {
{toggleView}
-
+
}
body={
- submissionInfographicsLoading ? (
+ entityOsmMapLoading ? (
- ) : submissionInfographicsData.length > 0 ? (
+ ) : formSubmissionsData.length > 0 ? (
) : (
@@ -206,7 +211,7 @@ const SubmissionsInfographics = ({ toggleView }) => {
}
/>
-
-
+
) : validatedVsMappedInfographics.length > 0 ? (
) : (
- No tasks validated or mapped yet!
+ No Tasks Validated or Mapped in the Last 30 Days
)
}
/>
-
-
*/}
diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx
index ce30b2d91c..d63c9b2115 100644
--- a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx
+++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx
@@ -54,7 +54,7 @@ const SubmissionsTable = ({ toggleView }) => {
const submissionTableDataLoading = useAppSelector((state) => state.submission.submissionTableDataLoading);
const submissionTableRefreshing = useAppSelector((state) => state.submission.submissionTableRefreshing);
const taskInfo = useAppSelector((state) => state.task.taskInfo);
- const projectInfo: projectInfoType = CoreModules.useAppSelector((state) => state.project.projectInfo);
+ const projectInfo = useAppSelector((state) => state.project.projectInfo);
const josmEditorError = useAppSelector((state) => state.task.josmEditorError);
const downloadSubmissionLoading = useAppSelector((state) => state.task.downloadSubmissionLoading);
const [numberOfFilters, setNumberOfFilters] = useState
(0);
diff --git a/src/frontend/src/components/common/LineChart.tsx b/src/frontend/src/components/common/LineChart.tsx
index 28cddda77f..137a8142f5 100644
--- a/src/frontend/src/components/common/LineChart.tsx
+++ b/src/frontend/src/components/common/LineChart.tsx
@@ -49,8 +49,8 @@ const CustomLineChart = ({ data, xAxisDataKey, lineOneKey, lineTwoKey, xLabel, y
)}
-
-
+
+
);
diff --git a/src/frontend/src/models/project/projectModel.ts b/src/frontend/src/models/project/projectModel.ts
index d9a3422ba2..f668fd087c 100644
--- a/src/frontend/src/models/project/projectModel.ts
+++ b/src/frontend/src/models/project/projectModel.ts
@@ -71,6 +71,7 @@ export type projectInfoType = {
organisation_logo: string;
instructions: string;
custom_tms_url: string;
+ created: string;
};
export type downloadProjectFormLoadingType = { type: 'form' | 'geojson' | 'csv' | 'json'; loading: boolean };
diff --git a/src/frontend/src/models/submission/submissionModel.ts b/src/frontend/src/models/submission/submissionModel.ts
index b972cd89a9..f62ed1ade0 100644
--- a/src/frontend/src/models/submission/submissionModel.ts
+++ b/src/frontend/src/models/submission/submissionModel.ts
@@ -1,8 +1,3 @@
-export type submissionInfographicsTypes = {
- date: string;
- count: 1;
-};
-
export type submissionContributorsTypes = {
user: string;
contributions: number;
@@ -16,12 +11,6 @@ export type submissionFormFieldsTypes = {
selectMultiple: any;
};
-export type validatedVsMappedInfographicsTypes = {
- date: string;
- validated: number;
- mapped: number;
-};
-
export type submissionTableDataTypes = {
results: any[];
pagination: {
@@ -40,3 +29,6 @@ export type reviewListType = {
className: string;
hoverClass: string;
};
+
+export type formSubmissionType = { date: string; count: number; label: string };
+export type validatedMappedType = { date: string; Validated: number; Mapped: number; label: string };
diff --git a/src/frontend/src/store/slices/SubmissionSlice.ts b/src/frontend/src/store/slices/SubmissionSlice.ts
index a4ad76d74e..a387261060 100644
--- a/src/frontend/src/store/slices/SubmissionSlice.ts
+++ b/src/frontend/src/store/slices/SubmissionSlice.ts
@@ -4,8 +4,6 @@ import { SubmissionStateTypes } from '@/store/types/ISubmissions';
const initialState: SubmissionStateTypes = {
submissionDetailsLoading: true,
submissionDetails: null,
- submissionInfographics: [],
- submissionInfographicsLoading: false,
submissionContributors: [],
submissionContributorsLoading: true,
submissionFormFields: [],
@@ -23,8 +21,6 @@ const initialState: SubmissionStateTypes = {
submissionFormFieldsLoading: false,
submissionTableDataLoading: false,
submissionTableRefreshing: false,
- validatedVsMappedInfographics: [],
- validatedVsMappedLoading: false,
updateReviewStatusModal: {
toggleModalStatus: false,
instanceId: null,
@@ -45,18 +41,6 @@ const SubmissionSlice = createSlice({
SetSubmissionDetails(state, action) {
state.submissionDetails = action.payload;
},
- SetSubmissionInfographics(state, action) {
- state.submissionInfographics = action.payload;
- },
- SetSubmissionInfographicsLoading(state, action) {
- state.submissionInfographicsLoading = action.payload;
- },
- SetValidatedVsMappedInfographics(state, action) {
- state.validatedVsMappedInfographics = action.payload;
- },
- SetValidatedVsMappedLoading(state, action) {
- state.validatedVsMappedLoading = action.payload;
- },
SetSubmissionContributors(state, action) {
state.submissionContributors = action.payload;
},
diff --git a/src/frontend/src/store/slices/TaskSlice.ts b/src/frontend/src/store/slices/TaskSlice.ts
index dda28ab5f2..a220c742aa 100644
--- a/src/frontend/src/store/slices/TaskSlice.ts
+++ b/src/frontend/src/store/slices/TaskSlice.ts
@@ -41,7 +41,7 @@ const TaskSlice = createSlice({
},
SetTaskSubmissionStates(state, action) {
- const groupedPayload: Record = action.payload.reduce((acc, item) => {
+ const groupedPayload: Record = action.payload?.reduce((acc, item) => {
if (!acc[item.task_id]) {
acc[item.task_id] = [];
}
@@ -57,7 +57,7 @@ const TaskSlice = createSlice({
let submissionCount = 0;
let lastSubmission: string | null = null;
items.forEach((item) => {
- if (item.status === task_status.MAPPED) {
+ if (item.status > 1) {
submissionCount++;
}
if (item.updated_at && (!lastSubmission || item.updated_at > lastSubmission)) {
diff --git a/src/frontend/src/store/types/ISubmissions.ts b/src/frontend/src/store/types/ISubmissions.ts
index fcb6fdc98d..30b74a43ad 100644
--- a/src/frontend/src/store/types/ISubmissions.ts
+++ b/src/frontend/src/store/types/ISubmissions.ts
@@ -1,16 +1,12 @@
import {
submissionContributorsTypes,
submissionFormFieldsTypes,
- submissionInfographicsTypes,
submissionTableDataTypes,
- validatedVsMappedInfographicsTypes,
} from '@/models/submission/submissionModel';
export type SubmissionStateTypes = {
submissionDetailsLoading: boolean;
submissionDetails: Record | null;
- submissionInfographics: submissionInfographicsTypes[];
- submissionInfographicsLoading: boolean;
submissionContributors: submissionContributorsTypes[];
submissionContributorsLoading: boolean;
submissionFormFields: submissionFormFieldsTypes[];
@@ -18,8 +14,6 @@ export type SubmissionStateTypes = {
submissionFormFieldsLoading: boolean;
submissionTableDataLoading: boolean;
submissionTableRefreshing: boolean;
- validatedVsMappedInfographics: validatedVsMappedInfographicsTypes[];
- validatedVsMappedLoading: boolean;
updateReviewStatusModal: updateReviewStatusModal;
updateReviewStateLoading: boolean;
};
diff --git a/src/frontend/src/utilfunctions/commonUtils.ts b/src/frontend/src/utilfunctions/commonUtils.ts
index cf04d93932..3df243017b 100644
--- a/src/frontend/src/utilfunctions/commonUtils.ts
+++ b/src/frontend/src/utilfunctions/commonUtils.ts
@@ -16,3 +16,28 @@ export const isStatusSuccess = (status: number) => {
}
return false;
};
+
+// get date N days ago
+export const dateNDaysAgo = (NDays: number) => {
+ return new Date(new Date().getTime() - NDays * 24 * 60 * 60 * 1000).toISOString();
+};
+
+// extract month & day in MM/DD format for chart date labels
+export const getMonthDate = (date: string) => {
+ const splittedDate = date?.split('T')[0]?.split('-');
+ return `${splittedDate[1]}/${splittedDate[2]}`;
+};
+
+// generates an array of date strings for last 30 days
+export const generateLast30Days = (): string[] => {
+ const last30Days: string[] = [];
+ const today = new Date();
+
+ for (let i = 0; i < 30; i++) {
+ const date = new Date();
+ date.setDate(today.getDate() - i);
+ last30Days.push(date.toISOString().split('T')[0]);
+ }
+
+ return last30Days;
+};
diff --git a/src/frontend/src/views/ProjectSubmissions.tsx b/src/frontend/src/views/ProjectSubmissions.tsx
index e039044478..56ba67c078 100644
--- a/src/frontend/src/views/ProjectSubmissions.tsx
+++ b/src/frontend/src/views/ProjectSubmissions.tsx
@@ -6,9 +6,9 @@ import SubmissionsTable from '@/components/ProjectSubmissions/SubmissionsTable.j
import CoreModules from '@/shared/CoreModules';
import { ProjectActions } from '@/store/slices/ProjectSlice';
import { ProjectById, GetEntityInfo } from '@/api/Project';
-import { GetProjectDashboard } from '@/api/Project';
import { useSearchParams } from 'react-router-dom';
-import { projectInfoType } from '@/models/project/projectModel';
+import { useAppSelector } from '@/types/reduxTypes';
+import { ProjectContributorsService } from '@/api/SubmissionService';
const ProjectSubmissions = () => {
const dispatch = CoreModules.useAppDispatch();
@@ -17,8 +17,10 @@ const ProjectSubmissions = () => {
const projectId = params.projectId;
- const state = CoreModules.useAppSelector((state) => state.project);
- const projectInfo: projectInfoType = CoreModules.useAppSelector((state) => state.project.projectInfo);
+ const state = useAppSelector((state) => state.project);
+ const projectInfo = useAppSelector((state) => state.project.projectInfo);
+ const entityList = useAppSelector((state) => state.project.entityOsmMap);
+ const updatedEntities = entityList?.filter((entity) => entity?.updated_at && entity?.status > 1);
//Fetch project for the first time
useEffect(() => {
@@ -39,16 +41,16 @@ const ProjectSubmissions = () => {
}
}, [params.id]);
- useEffect(() => {
- dispatch(GetProjectDashboard(`${import.meta.env.VITE_API_URL}/projects/project_dashboard/${projectId}`));
- }, []);
-
// for hot fix to display task-list and show option of task-list for submission table filter
// better solution needs to be researched
useEffect(() => {
dispatch(GetEntityInfo(`${import.meta.env.VITE_API_URL}/projects/${projectId}/entities/statuses`));
}, []);
+ useEffect(() => {
+ dispatch(ProjectContributorsService(`${import.meta.env.VITE_API_URL}/projects/contributors/${projectId}`));
+ }, []);
+
useEffect(() => {
if (!searchParams.get('tab')) {
setSearchParams({ tab: 'infographics' });
@@ -85,11 +87,11 @@ const ProjectSubmissions = () => {
return (
{searchParams.get('tab') === 'infographics' ? (
- } />
+ } entities={updatedEntities} />
) : (
} />
)}