From 9d08533a4079012a259ea533a9efb109f38ed2ba Mon Sep 17 00:00:00 2001 From: Nishit Suwal <81785002+NSUWAL123@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:46:13 +0545 Subject: [PATCH] feat(frontend): working submission comments to submission page (#1709) * fix(submissionDetails): display start, end, today date, deviceid seperately above * fix(accordion): add seperator props * fix(submissionDetails): use accordion to display nested values, code refactor * fix(submissionTable): change submission taskId key * fix(submissionsTable): redirect to submission instance page with taskUId * fix(submission): pass taskUId while reviewStateModal dispatch * feat(assetModules): calendar icon add * fix(submissionComments): submission comments component add * fix(submissionDetails): fix UI, submissionComments component add * feat(updateReviewStatusModal): add comments api integration * fix(projectInfo): fix index access * fix(submissionService): remove clear reviewStatusState & loading state remove from service * fix(comments): filter task comments only * fix(submissionComments): display msg if no comments * fix(submissionDetails): text size change * fix(submissionDetails): style fixes --- src/frontend/src/api/SubmissionService.ts | 10 - .../components/ProjectDetailsV2/Comments.tsx | 7 +- .../ProjectSubmissions/ProjectInfo.tsx | 2 +- .../ProjectSubmissions/SubmissionsTable.tsx | 58 ++-- .../UpdateReviewStatusModal.tsx | 43 ++- .../SubmissionInstance/SubmissionComments.tsx | 55 ++++ .../src/components/common/Accordion.tsx | 4 +- src/frontend/src/shared/AssetModules.js | 2 + .../src/store/slices/SubmissionSlice.ts | 1 + src/frontend/src/store/types/ISubmissions.ts | 1 + src/frontend/src/views/SubmissionDetails.tsx | 262 +++++++++++------- 11 files changed, 299 insertions(+), 146 deletions(-) create mode 100644 src/frontend/src/components/SubmissionInstance/SubmissionComments.tsx diff --git a/src/frontend/src/api/SubmissionService.ts b/src/frontend/src/api/SubmissionService.ts index ed070feea..89193a693 100644 --- a/src/frontend/src/api/SubmissionService.ts +++ b/src/frontend/src/api/SubmissionService.ts @@ -89,16 +89,6 @@ export const UpdateReviewStateService: Function = (url: string) => { dispatch(SubmissionActions.UpdateReviewStateLoading(true)); const response = await CoreModules.axios.post(url); dispatch(SubmissionActions.UpdateSubmissionTableDataReview(response.data)); - dispatch( - SubmissionActions.SetUpdateReviewStatusModal({ - toggleModalStatus: false, - projectId: null, - instanceId: null, - taskId: null, - reviewState: '', - }), - ); - dispatch(SubmissionActions.UpdateReviewStateLoading(false)); } catch (error) { dispatch( CommonActions.SetSnackBar({ diff --git a/src/frontend/src/components/ProjectDetailsV2/Comments.tsx b/src/frontend/src/components/ProjectDetailsV2/Comments.tsx index ba3aa0915..7cccb4f4d 100644 --- a/src/frontend/src/components/ProjectDetailsV2/Comments.tsx +++ b/src/frontend/src/components/ProjectDetailsV2/Comments.tsx @@ -29,6 +29,9 @@ const Comments = () => { return task?.index == selectedTask; })?.[0], }; + const filteredProjectCommentsList = projectCommentsList?.filter( + (comment) => !comment?.action_text?.includes('-SUBMISSION_INST-'), + ); useEffect(() => { dispatch(GetProjectComments(`${import.meta.env.VITE_API_URL}/tasks/${currentStatus?.id}/history/?comment=true`)); @@ -72,9 +75,9 @@ const Comments = () => { ) : (
- {projectCommentsList?.length > 0 ? ( + {filteredProjectCommentsList?.length > 0 ? (
- {projectCommentsList?.map((projectComment, i) => ( + {filteredProjectCommentsList?.map((projectComment, i) => (
{
{projectDetailsLoading || submissionContributorsLoading || entityOsmMapLoading ? (
- {Array.from({ length: 3 }).map((i) => ( + {Array.from({ length: 3 }).map((_, i) => ( ))}
diff --git a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx index ab768a1bc..90637ee40 100644 --- a/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx +++ b/src/frontend/src/components/ProjectSubmissions/SubmissionsTable.tsx @@ -57,6 +57,10 @@ const SubmissionsTable = ({ toggleView }) => { const projectInfo = useAppSelector((state) => state.project.projectInfo); const josmEditorError = useAppSelector((state) => state.task.josmEditorError); const downloadSubmissionLoading = useAppSelector((state) => state.task.downloadSubmissionLoading); + const projectTaskBoundries = useAppSelector((state) => state.project.projectTaskBoundries); + const projectIndex = projectTaskBoundries.findIndex((project) => project.id == +projectId); + const taskList = projectTaskBoundries[projectIndex]?.taskBoundries; + const [numberOfFilters, setNumberOfFilters] = useState(0); const [paginationPage, setPaginationPage] = useState(1); const [submittedBy, setSubmittedBy] = useState(null); @@ -430,31 +434,35 @@ const SubmissionsTable = ({ toggleView }) => { dataField="Actions" headerClassName="updatedHeader !fmtm-sticky fmtm-right-0 fmtm-shadow-[-10px_0px_20px_0px_rgba(0,0,0,0.1)] fmtm-text-center" rowClassName="updatedRow !fmtm-sticky fmtm-right-0 fmtm-bg-white fmtm-shadow-[-10px_0px_20px_0px_rgba(0,0,0,0.1)]" - dataFormat={(row) => ( -
- { - navigate(`/project/${projectId}/tasks/${row?.phonenumber}/submission/${row?.meta?.instanceID}`); - }} - />{' '} - {' '} - { - dispatch( - SubmissionActions.SetUpdateReviewStatusModal({ - toggleModalStatus: true, - instanceId: row?.meta?.instanceID, - taskId: row?.task_id, - projectId: projectId, - reviewState: row?.__system?.reviewState, - }), - ); - }} - /> -
- )} + dataFormat={(row) => { + const taskUId = taskList?.find((task) => task?.index == row?.task_id)?.id; + return ( +
+ { + navigate(`/project/${projectId}/tasks/${taskUId}/submission/${row?.meta?.instanceID}`); + }} + />{' '} + {' '} + { + dispatch( + SubmissionActions.SetUpdateReviewStatusModal({ + toggleModalStatus: true, + instanceId: row?.meta?.instanceID, + taskId: row?.task_id, + projectId: projectId, + reviewState: row?.__system?.reviewState, + taskUId: taskUId, + }), + ); + }} + /> +
+ ); + }} /> )} diff --git a/src/frontend/src/components/ProjectSubmissions/UpdateReviewStatusModal.tsx b/src/frontend/src/components/ProjectSubmissions/UpdateReviewStatusModal.tsx index 770ed800c..ef000f380 100644 --- a/src/frontend/src/components/ProjectSubmissions/UpdateReviewStatusModal.tsx +++ b/src/frontend/src/components/ProjectSubmissions/UpdateReviewStatusModal.tsx @@ -7,6 +7,7 @@ import { UpdateReviewStateService } from '@/api/SubmissionService'; import TextArea from '../common/TextArea'; import Button from '../common/Button'; import { useAppSelector } from '@/types/reduxTypes'; +import { PostProjectComments } from '@/api/Project'; const reviewList: reviewListType[] = [ { @@ -41,22 +42,48 @@ const UpdateReviewStatusModal = () => { setReviewStatus(updateReviewStatusModal.reviewState); }, [updateReviewStatusModal.reviewState]); - const handleStatusUpdate = () => { + const handleStatusUpdate = async () => { if (!updateReviewStatusModal.instanceId || !updateReviewStatusModal.projectId || !updateReviewStatusModal.taskId) { return; } if (!reviewStatus) { setError('Review state needs to be selected.'); - return; + } else if (updateReviewStatusModal.reviewState !== reviewStatus) { + await dispatch( + UpdateReviewStateService( + `${import.meta.env.VITE_API_URL}/submission/update_review_state?project_id=${ + updateReviewStatusModal.projectId + }&task_id=${parseInt(updateReviewStatusModal.taskId)}&instance_id=${ + updateReviewStatusModal.instanceId + }&review_state=${reviewStatus}`, + ), + ); + } + if (noteComments.trim().length > 0) { + await dispatch( + PostProjectComments( + `${import.meta.env.VITE_API_URL}/tasks/task-comments/?project_id=${updateReviewStatusModal?.projectId}`, + { + task_id: updateReviewStatusModal?.taskUId, + project_id: updateReviewStatusModal?.projectId, + comment: `${updateReviewStatusModal?.instanceId}-SUBMISSION_INST-${noteComments}`, + }, + ), + ); + setNoteComments(''); } dispatch( - UpdateReviewStateService( - `${import.meta.env.VITE_API_URL}/submission/update_review_state?project_id=${updateReviewStatusModal.projectId}&task_id=${parseInt( - updateReviewStatusModal.taskId, - )}&instance_id=${updateReviewStatusModal.instanceId}&review_state=${reviewStatus}`, - ), + SubmissionActions.SetUpdateReviewStatusModal({ + toggleModalStatus: false, + projectId: null, + instanceId: null, + taskId: null, + reviewState: '', + taskUId: null, + }), ); + dispatch(SubmissionActions.UpdateReviewStateLoading(false)); }; return ( @@ -106,6 +133,7 @@ const UpdateReviewStatusModal = () => { instanceId: null, taskId: null, reviewState: '', + taskUId: null, }), ); }} @@ -131,6 +159,7 @@ const UpdateReviewStatusModal = () => { instanceId: null, taskId: null, reviewState: '', + taskUId: null, }), ); }} diff --git a/src/frontend/src/components/SubmissionInstance/SubmissionComments.tsx b/src/frontend/src/components/SubmissionInstance/SubmissionComments.tsx new file mode 100644 index 000000000..1ce9c1faa --- /dev/null +++ b/src/frontend/src/components/SubmissionInstance/SubmissionComments.tsx @@ -0,0 +1,55 @@ +import { useAppSelector } from '@/types/reduxTypes'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import AssetModules from '@/shared/AssetModules'; +import CoreModules from '@/shared/CoreModules'; + +const SubmissionComments = () => { + const params = useParams(); + const submissionInstanceId = params.instanceId; + + const taskCommentsList = useAppSelector((state) => state?.project?.projectCommentsList); + const filteredTaskCommentsList = taskCommentsList + ?.filter((comment) => comment?.action_text.includes('-SUBMISSION_INST-')) + .filter((comment) => comment.action_text.split('-SUBMISSION_INST-')[0] === submissionInstanceId); + const taskGetCommentsLoading = useAppSelector((state) => state?.project?.projectGetCommentsLoading); + + return ( +
+

Comments

+ {taskGetCommentsLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+ + +
+ +
+ ))} +
+ ) : filteredTaskCommentsList?.length > 0 ? ( + filteredTaskCommentsList?.map((comment) => ( +
+
+

{comment?.username}

+
+ +

{comment?.action_date?.split('T')[0]}

+
+
+

{comment?.action_text?.split('-SUBMISSION_INST-')[1]}

+
+ )) + ) : ( +

No Comments!

+ )} +
+ ); +}; + +export default SubmissionComments; diff --git a/src/frontend/src/components/common/Accordion.tsx b/src/frontend/src/components/common/Accordion.tsx index 976cccaec..00036ad75 100644 --- a/src/frontend/src/components/common/Accordion.tsx +++ b/src/frontend/src/components/common/Accordion.tsx @@ -10,6 +10,7 @@ interface IAccordion { onToggle: any; description?: string; disableHeaderClickToggle?: boolean; + hasSeperator?: boolean; } export default function Accordion({ @@ -21,6 +22,7 @@ export default function Accordion({ onToggle, description, disableHeaderClickToggle, + hasSeperator = true, }: IAccordion) { const [collapsed, setCollapsed] = useState(isCollapsed); @@ -34,7 +36,7 @@ export default function Accordion({
{ if (disableHeaderClickToggle) return; diff --git a/src/frontend/src/shared/AssetModules.js b/src/frontend/src/shared/AssetModules.js index 646309bc6..d82dcd7b2 100755 --- a/src/frontend/src/shared/AssetModules.js +++ b/src/frontend/src/shared/AssetModules.js @@ -85,6 +85,7 @@ import { IntegrationInstructions as IntegrationInstructionsIcon, QrCode2Outlined as QrCode2OutlinedIcon, BarChart as BarChartIcon, + CalendarTodayOutlined as CalendarTodayOutlinedIcon, } from '@mui/icons-material'; import LockPng from '@/assets/images/lock.png'; import RedLockPng from '@/assets/images/red-lock.png'; @@ -180,4 +181,5 @@ export default { IntegrationInstructionsIcon, QrCode2OutlinedIcon, BarChartIcon, + CalendarTodayOutlinedIcon, }; diff --git a/src/frontend/src/store/slices/SubmissionSlice.ts b/src/frontend/src/store/slices/SubmissionSlice.ts index a38726106..46e46e2a2 100644 --- a/src/frontend/src/store/slices/SubmissionSlice.ts +++ b/src/frontend/src/store/slices/SubmissionSlice.ts @@ -27,6 +27,7 @@ const initialState: SubmissionStateTypes = { taskId: null, projectId: null, reviewState: '', + taskUId: null, }, updateReviewStateLoading: false, }; diff --git a/src/frontend/src/store/types/ISubmissions.ts b/src/frontend/src/store/types/ISubmissions.ts index 30b74a43a..6a26defc3 100644 --- a/src/frontend/src/store/types/ISubmissions.ts +++ b/src/frontend/src/store/types/ISubmissions.ts @@ -24,4 +24,5 @@ type updateReviewStatusModal = { taskId: string | null; projectId: number | null; reviewState: string; + taskUId: string | null; }; diff --git a/src/frontend/src/views/SubmissionDetails.tsx b/src/frontend/src/views/SubmissionDetails.tsx index b4df1f404..058054f51 100644 --- a/src/frontend/src/views/SubmissionDetails.tsx +++ b/src/frontend/src/views/SubmissionDetails.tsx @@ -9,6 +9,77 @@ import UpdateReviewStatusModal from '@/components/ProjectSubmissions/UpdateRevie import { useAppSelector } from '@/types/reduxTypes'; import { useNavigate } from 'react-router-dom'; import useDocumentTitle from '@/utilfunctions/useDocumentTitle'; +import Accordion from '@/components/common/Accordion'; +import { GetProjectComments } from '@/api/Project'; +import SubmissionComments from '@/components/SubmissionInstance/SubmissionComments'; + +const renderValue = (value: any, key: string = '') => { + if (key === 'start' || key === 'end') { + return ( + <> +
{key}
+

+ {value?.split('T')[0]}, {value?.split('T')[1]} +

+ + ); + } else if (typeof value === 'object' && Object.values(value).includes('Point')) { + return ( + <> + {renderValue( + `${value?.type} (${value?.coordinates?.[0]},${value?.coordinates?.[1]},${value?.coordinates?.[2]}`, + key, + )} +
+ {renderValue(value?.properties?.accuracy, 'accuracy')} + + ); + } else if (typeof value === 'object') { + return ( +
    + {}} + hasSeperator={false} + header={

    {key}

    } + body={ +
    + {Object.entries(value).map(([key, nestedValue]) => { + return
    {renderValue(nestedValue, key)}
    ; + })} +
    + } + /> +
+ ); + } else { + return ( + <> +
+ {key} +
+ {value} + + ); + } +}; + +function removeNullValues(obj: Record) { + const newObj = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== null) { + if (typeof value === 'object') { + const nestedObj = removeNullValues(value); + if (Object.keys(nestedObj).length > 0) { + newObj[key] = nestedObj; + } + } else { + newObj[key] = value; + } + } + } + return newObj; +} const SubmissionDetails = () => { useDocumentTitle('Submission Instance'); @@ -18,6 +89,7 @@ const SubmissionDetails = () => { const projectId = params.projectId; const paramsInstanceId = params.instanceId; + const taskUId = params.taskId; const projectDashboardDetail = useAppSelector((state) => state.project.projectDashboardDetail); const projectDashboardLoading = useAppSelector((state) => state.project.projectDashboardLoading); const submissionDetails = useAppSelector((state) => state.submission.submissionDetails); @@ -28,6 +100,9 @@ const SubmissionDetails = () => { ? submissionDetails?.task_filter : '-'; + const { start, end, today, deviceid, ...restSubmissionDetails } = submissionDetails || {}; + const dateDeviceDetails = { start, end, today, deviceid }; + useEffect(() => { dispatch(GetProjectDashboard(`${import.meta.env.VITE_API_URL}/projects/project_dashboard/${projectId}`)); }, []); @@ -35,30 +110,23 @@ const SubmissionDetails = () => { useEffect(() => { dispatch( SubmissionService( - `${import.meta.env.VITE_API_URL}/submission/submission-detail?submission_id=${paramsInstanceId}&project_id=${projectId}`, + `${ + import.meta.env.VITE_API_URL + }/submission/submission-detail?submission_id=${paramsInstanceId}&project_id=${projectId}`, ), ); }, [projectId, paramsInstanceId]); - function removeNullValues(obj: Record) { - const newObj = {}; - for (const [key, value] of Object.entries(obj)) { - if (value !== null) { - if (typeof value === 'object') { - const nestedObj = removeNullValues(value); - if (Object.keys(nestedObj).length > 0) { - newObj[key] = nestedObj; - } - } else { - newObj[key] = value; - } - } - } - return newObj; - } - const filteredData = submissionDetails ? removeNullValues(submissionDetails) : {}; + useEffect(() => { + if (!taskUId) return; + dispatch(GetProjectComments(`${import.meta.env.VITE_API_URL}/tasks/${parseInt(taskUId)}/history/?comment=true`)); + }, [taskUId]); + + const filteredData = restSubmissionDetails ? removeNullValues(restSubmissionDetails) : {}; - const coordinatesArray: [number, number][] = submissionDetails?.xlocation?.split(';').map(function (coord: string) { + const coordinatesArray: [number, number][] = restSubmissionDetails?.xlocation?.split(';').map(function ( + coord: string, + ) { let coordinate = coord .trim() .split(' ') @@ -86,48 +154,16 @@ const SubmissionDetails = () => { const pointFeature = { type: 'Feature', geometry: { - ...submissionDetails?.point, + ...restSubmissionDetails?.point, }, properties: {}, }; - const renderValue = (value: any, key: string = '') => { - if (key === 'start' || key === 'end') { - return ( -

- {value?.split('T')[0]}, {value?.split('T')[1]} -

- ); - } else if (typeof value === 'object' && Object.values(value).includes('Point')) { - return ( -
-

{value?.type} ({value?.coordinates?.[0]},{value?.coordinates?.[1]}, - {value?.coordinates?.[2]}){renderValue(value?.properties)} -
- ); - } else if (typeof value === 'object') { - return ( -
    - {Object.entries(value).map(([key, nestedValue]) => ( - - - {key}:{' '} - - {renderValue(nestedValue, key)} - - ))} -
- ); - } else { - return {value}; - } - }; - return ( -
+
{projectDashboardLoading ? ( - + ) : (

@@ -149,64 +185,90 @@ const SubmissionDetails = () => {

)} -
+
- {projectDashboardLoading ? ( - +
+ {projectDashboardLoading ? ( + + ) : ( +
+

+ {projectDashboardDetail?.project_name_prefix} +

+

Task: {taskId}

+

+ Submission Id: {paramsInstanceId} +

+
+ )} +
+ {/* start, end, today, deviceid values */} + {submissionDetailsLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ ))} +
) : ( -
-

- {projectDashboardDetail?.project_name_prefix} -

-

Task: {taskId}

-

Submission Id: {paramsInstanceId}

+
+ {Object.entries(dateDeviceDetails).map(([key, value]) => ( +
+
{renderValue(value, key)}
+
+ ))}
)} -
-
-
+
+
- {submissionDetailsLoading ? ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- -
- ))} -
- ) : ( +
+ {submissionDetailsLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ +
+ ))} +
+ ) : ( +
+ {Object.entries(filteredData).map(([key, value]) => ( +
+
{renderValue(value, key)}
+
+ ))} +
+ )}
- {Object.entries(filteredData).map(([key, value]) => ( -
- -
{key}
- {renderValue(value, key)} -
-
- ))}{' '} +
- )} +
); };