From dec0425bda116709045b4d058a328088e8b28285 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Sat, 23 Nov 2024 18:21:25 +0200 Subject: [PATCH 1/4] Cleanup and improvements * move form submissions to /responses/form-submissions --- ...MonitoringObserverFormSubmissionsTable.tsx | 2 +- .../CitizenReportDetails.tsx | 5 +- .../CitizenReportsFormAggregatedDetails.tsx | 15 ++---- .../FormSubmissionDetails.tsx | 13 ++++-- .../FormSubmissionsAggregatedByFormTable.tsx | 2 +- .../FormSubmissionsAggregatedDetails.tsx | 15 ++---- .../FormSubmissionsByEntryTable.tsx | 2 +- .../QuickReportDetails/QuickReportDetails.tsx | 11 +---- .../TextAggregateContent.tsx | 2 +- .../hooks/form-submissions-queries.ts | 2 +- .../features/responses/utils/column-defs.tsx | 10 ++-- web/src/routeTree.gen.ts | 46 ++++++++++--------- .../$formId.aggregated.tsx | 4 +- .../{ => form-submissions}/$submissionId.tsx | 2 +- 14 files changed, 59 insertions(+), 72 deletions(-) rename web/src/routes/responses/{ => form-submissions}/$formId.aggregated.tsx (95%) rename web/src/routes/responses/{ => form-submissions}/$submissionId.tsx (94%) diff --git a/web/src/features/monitoring-observers/components/MonitoringObserverFormSubmissionsTable/MonitoringObserverFormSubmissionsTable.tsx b/web/src/features/monitoring-observers/components/MonitoringObserverFormSubmissionsTable/MonitoringObserverFormSubmissionsTable.tsx index 435d40db1..f09240b87 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserverFormSubmissionsTable/MonitoringObserverFormSubmissionsTable.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserverFormSubmissionsTable/MonitoringObserverFormSubmissionsTable.tsx @@ -47,7 +47,7 @@ export function MonitoringObserverFormSubmissionsTable({ const navigateToFormSubmission = useCallback( (submissionId: string) => { - void navigate({ to: '/responses/$submissionId', params: { submissionId }}); + void navigate({ to: '/responses/form-submissions/$submissionId', params: { submissionId }}); }, [navigate] ); diff --git a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx index da33a78bb..041491c2b 100644 --- a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx +++ b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx @@ -76,8 +76,9 @@ export default function CitizenReportDetails(): FunctionComponent { return ( }> + backButton={} + breadcrumbs={<>} + title={`#${citizenReport.citizenReportId}`}>
diff --git a/web/src/features/responses/components/CitizenReportsFormAggregatedDetails/CitizenReportsFormAggregatedDetails.tsx b/web/src/features/responses/components/CitizenReportsFormAggregatedDetails/CitizenReportsFormAggregatedDetails.tsx index e66944b1b..648c61e8a 100644 --- a/web/src/features/responses/components/CitizenReportsFormAggregatedDetails/CitizenReportsFormAggregatedDetails.tsx +++ b/web/src/features/responses/components/CitizenReportsFormAggregatedDetails/CitizenReportsFormAggregatedDetails.tsx @@ -1,3 +1,4 @@ +import { usePrevSearch } from '@/common/prev-search-store'; import type { FunctionComponent } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; @@ -8,12 +9,11 @@ import { Route, } from '@/routes/responses/citizen-reports/$formId.aggregated'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { Link, useRouter } from '@tanstack/react-router'; import { SubmissionType } from '../../models/common'; import { AggregateCard } from '../AggregateCard/AggregateCard'; export default function CitizenReportsFormAggregatedDetails(): FunctionComponent { - const { state } = useRouter(); + const prevSearch = usePrevSearch(); const { formId } = Route.useParams(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); @@ -29,15 +29,8 @@ export default function CitizenReportsFormAggregatedDetails(): FunctionComponent return ( } - breadcrumbs={ -
- - responses - - {formId} -
- } + backButton={} + breadcrumbs={<>} title={`${formCode} - ${mapFormType(formType)}`}>
{Object.values(aggregates).map((aggregate) => { diff --git a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx index 8d8546c88..c239cf2f2 100644 --- a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx +++ b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx @@ -9,7 +9,7 @@ import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { queryClient } from '@/main'; -import { Route, formSubmissionDetailsQueryOptions } from '@/routes/responses/$submissionId'; +import { Route, formSubmissionDetailsQueryOptions } from '@/routes/responses/form-submissions/$submissionId'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; @@ -18,6 +18,8 @@ import { formSubmissionsByEntryKeys, formSubmissionsByObserverKeys } from '../.. import { SubmissionType } from '../../models/common'; import { mapFormSubmissionFollowUpStatus } from '../../utils/helpers'; import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; +import { usePrevSearch } from '@/common/prev-search-store'; +import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; export default function FormSubmissionDetails(): FunctionComponent { const { submissionId } = Route.useParams(); @@ -27,6 +29,7 @@ export default function FormSubmissionDetails(): FunctionComponent { const { data: formSubmission } = useSuspenseQuery( formSubmissionDetailsQueryOptions(currentElectionRoundId, submissionId) ); + const prevSearch = usePrevSearch(); const router = useRouter(); @@ -68,14 +71,16 @@ export default function FormSubmissionDetails(): FunctionComponent { } return ( - + } + breadcrumbs={<>} + title={`#${formSubmission.submissionId}`}>

Observer:

+ disabled={!formSubmission.isOwnObserver || electionRound?.status === ElectionRoundStatus.Archived}> diff --git a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx index 7d187dd2d..3bbcb926d 100644 --- a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx @@ -29,7 +29,7 @@ export function FormSubmissionsAggregatedByFormTable({ const navigateToAggregatedForm = useCallback( (formId: string) => { void navigate({ - to: '/responses/$formId/aggregated', + to: '/responses/form-submissions/$formId/aggregated', params: { formId }, search: { hasFlaggedAnswers: search.hasFlaggedAnswers, diff --git a/web/src/features/responses/components/FormSubmissionsAggregatedDetails/FormSubmissionsAggregatedDetails.tsx b/web/src/features/responses/components/FormSubmissionsAggregatedDetails/FormSubmissionsAggregatedDetails.tsx index 0ba2d3d05..d69923676 100644 --- a/web/src/features/responses/components/FormSubmissionsAggregatedDetails/FormSubmissionsAggregatedDetails.tsx +++ b/web/src/features/responses/components/FormSubmissionsAggregatedDetails/FormSubmissionsAggregatedDetails.tsx @@ -4,7 +4,7 @@ import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { mapFormType } from '@/lib/utils'; -import { formAggregatedDetailsQueryOptions, Route } from '@/routes/responses/$formId.aggregated'; +import { formAggregatedDetailsQueryOptions, Route } from '@/routes/responses/form-submissions/$formId.aggregated'; import { useSuspenseQuery } from '@tanstack/react-query'; import { Link } from '@tanstack/react-router'; import { SubmissionType } from '../../models/common'; @@ -36,16 +36,9 @@ export default function FormSubmissionsAggregatedDetails(): FunctionComponent { return ( } - breadcrumbs={ -
- - responses - - {formId} -
- } - title={`${formCode} - ${mapFormType(formType)}`}> + backButton={} + breadcrumbs={<>} + title={`${formCode} - ${mapFormType(formType)}`}>
{Object.values(aggregates).map((aggregate) => { return ( diff --git a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx index aadf35147..3aca59e18 100644 --- a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx @@ -73,7 +73,7 @@ export function FormSubmissionsByEntryTable({ searchText }: FormSubmissionsByEnt const navigateToFormSubmission = useCallback( (submissionId: string) => { - void navigate({ to: '/responses/$submissionId', params: { submissionId } }); + void navigate({ to: '/responses/form-submissions/$submissionId', params: { submissionId } }); }, [navigate] ); diff --git a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx index cc3f34b2e..70b690734 100644 --- a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx +++ b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx @@ -71,14 +71,7 @@ export default function QuickReportDetails(): FunctionComponent { return ( } - breadcrumbs={ -
- - responses - - {quickReport.id} -
- } + breadcrumbs={<>} title={quickReport.id}>
@@ -160,7 +153,7 @@ export default function QuickReportDetails(): FunctionComponent { onValueChange={handleFollowUpStatusChange} defaultValue={quickReport.followUpStatus} value={quickReport.followUpStatus} - disabled={!quickReport.isOwnObserver|| electionRound?.status === ElectionRoundStatus.Archived}> + disabled={!quickReport.isOwnObserver || electionRound?.status === ElectionRoundStatus.Archived}> diff --git a/web/src/features/responses/components/TextAggregateContent/TextAggregateContent.tsx b/web/src/features/responses/components/TextAggregateContent/TextAggregateContent.tsx index 2cfc2450b..bf6ccf899 100644 --- a/web/src/features/responses/components/TextAggregateContent/TextAggregateContent.tsx +++ b/web/src/features/responses/components/TextAggregateContent/TextAggregateContent.tsx @@ -52,7 +52,7 @@ export function TextAggregateContent({ diff --git a/web/src/features/responses/hooks/form-submissions-queries.ts b/web/src/features/responses/hooks/form-submissions-queries.ts index f619cd56c..5704774e2 100644 --- a/web/src/features/responses/hooks/form-submissions-queries.ts +++ b/web/src/features/responses/hooks/form-submissions-queries.ts @@ -15,7 +15,7 @@ import type { FormSubmissionByObserver, FormSubmissionsFilters, } from '../models/form-submission'; -import { SubmissionsAggregatedByFormParams } from '@/routes/responses/$formId.aggregated'; +import { SubmissionsAggregatedByFormParams } from '@/routes/responses/form-submissions/$formId.aggregated'; const STALE_TIME = 1000 * 60; // one minute diff --git a/web/src/features/responses/utils/column-defs.tsx b/web/src/features/responses/utils/column-defs.tsx index 55f50e660..13ea3d95c 100644 --- a/web/src/features/responses/utils/column-defs.tsx +++ b/web/src/features/responses/utils/column-defs.tsx @@ -196,7 +196,7 @@ export const formSubmissionsByEntryColumnDefs: ColumnDef + to='/responses/form-submissions/$submissionId'>
@@ -340,7 +340,7 @@ export const observerFormSubmissionsColumnDefs: ColumnDef + to='/responses/form-submissions/$submissionId'>
@@ -504,7 +504,7 @@ export const formSubmissionsByFormColumnDefs: ColumnDef + to='/responses/form-submissions/$formId/aggregated'>
@@ -562,7 +562,7 @@ export const aggregatedAnswerExtraInfoColumnDefs: ColumnDef[] ) : ( @@ -1573,7 +1573,7 @@ export const incidentReportsByFormColumnDefs: ColumnDef + to='/responses/form-submissions/$formId/aggregated'>
diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 56647f9e5..e078ba92b 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -22,7 +22,6 @@ import { Route as ForgotPasswordIndexImport } from './routes/forgot-password/ind import { Route as ElectionRoundsIndexImport } from './routes/election-rounds/index' import { Route as ElectionEventIndexImport } from './routes/election-event/index' import { Route as AcceptInviteIndexImport } from './routes/accept-invite/index' -import { Route as ResponsesSubmissionIdImport } from './routes/responses/$submissionId' import { Route as ResetPasswordSuccessImport } from './routes/reset-password/success' import { Route as ObserversObserverIdImport } from './routes/observers/$observerId' import { Route as ObserverGuidesNewImport } from './routes/observer-guides/new' @@ -37,8 +36,8 @@ import { Route as CitizenGuidesNewImport } from './routes/citizen-guides/new' import { Route as AcceptInviteSuccessImport } from './routes/accept-invite/success' import { Route as ResponsesQuickReportsQuickReportIdImport } from './routes/responses/quick-reports/$quickReportId' import { Route as ResponsesIncidentReportsIncidentReportIdImport } from './routes/responses/incident-reports/$incidentReportId' +import { Route as ResponsesFormSubmissionsSubmissionIdImport } from './routes/responses/form-submissions/$submissionId' import { Route as ResponsesCitizenReportsCitizenReportIdImport } from './routes/responses/citizen-reports/$citizenReportId' -import { Route as ResponsesFormIdAggregatedImport } from './routes/responses/$formId.aggregated' import { Route as ObserversObserverIdEditImport } from './routes/observers_.$observerId.edit' import { Route as ObserverGuidesViewGuideIdImport } from './routes/observer-guides/view.$guideId' import { Route as ObserverGuidesEditGuideIdImport } from './routes/observer-guides/edit.$guideId' @@ -50,6 +49,7 @@ import { Route as CitizenNotificationsViewNotificationIdImport } from './routes/ import { Route as CitizenGuidesViewGuideIdImport } from './routes/citizen-guides/view.$guideId' import { Route as CitizenGuidesEditGuideIdImport } from './routes/citizen-guides/edit.$guideId' import { Route as ResponsesIncidentReportsFormIdAggregatedImport } from './routes/responses/incident-reports/$formId.aggregated' +import { Route as ResponsesFormSubmissionsFormIdAggregatedImport } from './routes/responses/form-submissions/$formId.aggregated' import { Route as ResponsesCitizenReportsFormIdAggregatedImport } from './routes/responses/citizen-reports/$formId.aggregated' import { Route as MonitoringObserversViewMonitoringObserverIdTabImport } from './routes/monitoring-observers/view/$monitoringObserverId.$tab' import { Route as MonitoringObserversPushMessagesIdViewImport } from './routes/monitoring-observers/push-messages.$id_.view' @@ -113,11 +113,6 @@ const AcceptInviteIndexRoute = AcceptInviteIndexImport.update({ getParentRoute: () => rootRoute, } as any) -const ResponsesSubmissionIdRoute = ResponsesSubmissionIdImport.update({ - path: '/responses/$submissionId', - getParentRoute: () => rootRoute, -} as any) - const ResetPasswordSuccessRoute = ResetPasswordSuccessImport.update({ path: '/reset-password/success', getParentRoute: () => rootRoute, @@ -192,17 +187,18 @@ const ResponsesIncidentReportsIncidentReportIdRoute = getParentRoute: () => rootRoute, } as any) +const ResponsesFormSubmissionsSubmissionIdRoute = + ResponsesFormSubmissionsSubmissionIdImport.update({ + path: '/responses/form-submissions/$submissionId', + getParentRoute: () => rootRoute, + } as any) + const ResponsesCitizenReportsCitizenReportIdRoute = ResponsesCitizenReportsCitizenReportIdImport.update({ path: '/responses/citizen-reports/$citizenReportId', getParentRoute: () => rootRoute, } as any) -const ResponsesFormIdAggregatedRoute = ResponsesFormIdAggregatedImport.update({ - path: '/responses/$formId/aggregated', - getParentRoute: () => rootRoute, -} as any) - const ObserversObserverIdEditRoute = ObserversObserverIdEditImport.update({ path: '/observers/$observerId/edit', getParentRoute: () => rootRoute, @@ -262,6 +258,12 @@ const ResponsesIncidentReportsFormIdAggregatedRoute = getParentRoute: () => rootRoute, } as any) +const ResponsesFormSubmissionsFormIdAggregatedRoute = + ResponsesFormSubmissionsFormIdAggregatedImport.update({ + path: '/responses/form-submissions/$formId/aggregated', + getParentRoute: () => rootRoute, + } as any) + const ResponsesCitizenReportsFormIdAggregatedRoute = ResponsesCitizenReportsFormIdAggregatedImport.update({ path: '/responses/citizen-reports/$formId/aggregated', @@ -350,10 +352,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResetPasswordSuccessImport parentRoute: typeof rootRoute } - '/responses/$submissionId': { - preLoaderRoute: typeof ResponsesSubmissionIdImport - parentRoute: typeof rootRoute - } '/accept-invite/': { preLoaderRoute: typeof AcceptInviteIndexImport parentRoute: typeof rootRoute @@ -434,14 +432,14 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ObserversObserverIdEditImport parentRoute: typeof rootRoute } - '/responses/$formId/aggregated': { - preLoaderRoute: typeof ResponsesFormIdAggregatedImport - parentRoute: typeof rootRoute - } '/responses/citizen-reports/$citizenReportId': { preLoaderRoute: typeof ResponsesCitizenReportsCitizenReportIdImport parentRoute: typeof rootRoute } + '/responses/form-submissions/$submissionId': { + preLoaderRoute: typeof ResponsesFormSubmissionsSubmissionIdImport + parentRoute: typeof rootRoute + } '/responses/incident-reports/$incidentReportId': { preLoaderRoute: typeof ResponsesIncidentReportsIncidentReportIdImport parentRoute: typeof rootRoute @@ -470,6 +468,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResponsesCitizenReportsFormIdAggregatedImport parentRoute: typeof rootRoute } + '/responses/form-submissions/$formId/aggregated': { + preLoaderRoute: typeof ResponsesFormSubmissionsFormIdAggregatedImport + parentRoute: typeof rootRoute + } '/responses/incident-reports/$formId/aggregated': { preLoaderRoute: typeof ResponsesIncidentReportsFormIdAggregatedImport parentRoute: typeof rootRoute @@ -493,7 +495,6 @@ export const routeTree = rootRoute.addChildren([ ObserverGuidesNewRoute, ObserversObserverIdRoute, ResetPasswordSuccessRoute, - ResponsesSubmissionIdRoute, AcceptInviteIndexRoute, ElectionEventIndexRoute, ElectionRoundsIndexRoute, @@ -514,8 +515,8 @@ export const routeTree = rootRoute.addChildren([ ObserverGuidesEditGuideIdRoute, ObserverGuidesViewGuideIdRoute, ObserversObserverIdEditRoute, - ResponsesFormIdAggregatedRoute, ResponsesCitizenReportsCitizenReportIdRoute, + ResponsesFormSubmissionsSubmissionIdRoute, ResponsesIncidentReportsIncidentReportIdRoute, ResponsesQuickReportsQuickReportIdRoute, CitizenReportAttachmentsElectionRoundIdCitizenReportIdAttachmentIdRoute, @@ -523,6 +524,7 @@ export const routeTree = rootRoute.addChildren([ MonitoringObserversPushMessagesIdViewRoute, MonitoringObserversViewMonitoringObserverIdTabRoute, ResponsesCitizenReportsFormIdAggregatedRoute, + ResponsesFormSubmissionsFormIdAggregatedRoute, ResponsesIncidentReportsFormIdAggregatedRoute, ]) diff --git a/web/src/routes/responses/$formId.aggregated.tsx b/web/src/routes/responses/form-submissions/$formId.aggregated.tsx similarity index 95% rename from web/src/routes/responses/$formId.aggregated.tsx rename to web/src/routes/responses/form-submissions/$formId.aggregated.tsx index e168225bb..f05891d80 100644 --- a/web/src/routes/responses/$formId.aggregated.tsx +++ b/web/src/routes/responses/form-submissions/$formId.aggregated.tsx @@ -8,7 +8,7 @@ import { buildURLSearchParams, redirectIfNotAuth } from '@/lib/utils'; import { queryOptions } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { z } from 'zod'; -import { ZDataSourceSearchSchema } from '..'; +import { ZDataSourceSearchSchema } from '../..'; export function formAggregatedDetailsQueryOptions( electionRoundId: string, @@ -60,7 +60,7 @@ export const SubmissionsAggregatedByFormSchema = z.object({ export type SubmissionsAggregatedByFormParams = z.infer; -export const Route = createFileRoute('/responses/$formId/aggregated')({ +export const Route = createFileRoute('/responses/form-submissions/$formId/aggregated')({ beforeLoad: () => { redirectIfNotAuth(); }, diff --git a/web/src/routes/responses/$submissionId.tsx b/web/src/routes/responses/form-submissions/$submissionId.tsx similarity index 94% rename from web/src/routes/responses/$submissionId.tsx rename to web/src/routes/responses/form-submissions/$submissionId.tsx index 4d70438f3..05fa817c4 100644 --- a/web/src/routes/responses/$submissionId.tsx +++ b/web/src/routes/responses/form-submissions/$submissionId.tsx @@ -20,7 +20,7 @@ export function formSubmissionDetailsQueryOptions(electionRoundId: string, submi }); } -export const Route = createFileRoute('/responses/$submissionId')({ +export const Route = createFileRoute('/responses/form-submissions/$submissionId')({ beforeLoad: () => { redirectIfNotAuth(); }, From 2115f0a9cbd44a32b620b95526a0d6b73976229d Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Sat, 23 Nov 2024 18:44:05 +0200 Subject: [PATCH 2/4] add before load for all auth routes --- .../PushMessageDetails/PushMessageDetails.tsx | 4 +++- web/src/routeTree.gen.ts | 12 ++++++++++++ ...ectionRoundId.$citizenReportId.$attachmentId.tsx | 5 ++++- web/src/routes/election-rounds/$electionRoundId.tsx | 6 +++++- web/src/routes/election-rounds/index.tsx | 4 ++++ web/src/routes/forms/$formId.tsx | 6 +++++- web/src/routes/forms/$formId_.$languageCode.tsx | 4 ++++ ...orms_.$formId.edit-translation.$languageCode.tsx | 9 +++++++-- web/src/routes/forms_.$formId.edit.tsx | 6 +++++- .../routes/monitoring-observers-import/index.tsx | 13 +++++++++++++ .../edit.$monitoringObserverId.tsx | 4 ++++ .../monitoring-observers/push-messages.$id.tsx | 6 +++++- web/src/routes/observers_.$observerId.edit.tsx | 4 ++++ 13 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 web/src/routes/monitoring-observers-import/index.tsx diff --git a/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx b/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx index ade080c45..d42e25b7f 100644 --- a/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx +++ b/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx @@ -18,7 +18,9 @@ export default function PushMessageDetails(): FunctionComponent { const { data: pushMessage } = useSuspenseQuery(pushMessageDetailsQueryOptions(currentElectionRoundId, id)); return ( - } title=''> + } + breadcrumbs={<>} + title={id}>
diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index e078ba92b..3c69078dd 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as ResetPasswordIndexImport } from './routes/reset-password/index import { Route as ObserversIndexImport } from './routes/observers/index' import { Route as NgosIndexImport } from './routes/ngos/index' import { Route as MonitoringObserversIndexImport } from './routes/monitoring-observers/index' +import { Route as MonitoringObserversImportIndexImport } from './routes/monitoring-observers-import/index' import { Route as LoginIndexImport } from './routes/login/index' import { Route as ForgotPasswordIndexImport } from './routes/forgot-password/index' import { Route as ElectionRoundsIndexImport } from './routes/election-rounds/index' @@ -88,6 +89,12 @@ const MonitoringObserversIndexRoute = MonitoringObserversIndexImport.update({ getParentRoute: () => rootRoute, } as any) +const MonitoringObserversImportIndexRoute = + MonitoringObserversImportIndexImport.update({ + path: '/monitoring-observers-import/', + getParentRoute: () => rootRoute, + } as any) + const LoginIndexRoute = LoginIndexImport.update({ path: '/login/', getParentRoute: () => rootRoute, @@ -372,6 +379,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginIndexImport parentRoute: typeof rootRoute } + '/monitoring-observers-import/': { + preLoaderRoute: typeof MonitoringObserversImportIndexImport + parentRoute: typeof rootRoute + } '/monitoring-observers/': { preLoaderRoute: typeof MonitoringObserversIndexImport parentRoute: typeof rootRoute @@ -500,6 +511,7 @@ export const routeTree = rootRoute.addChildren([ ElectionRoundsIndexRoute, ForgotPasswordIndexRoute, LoginIndexRoute, + MonitoringObserversImportIndexRoute, MonitoringObserversIndexRoute, NgosIndexRoute, ObserversIndexRoute, diff --git a/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx b/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx index 1e9d70aab..6217cf928 100644 --- a/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx +++ b/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx @@ -1,6 +1,6 @@ import { authApi } from '@/common/auth-api'; import { Attachment } from '@/features/responses/models/common'; -import { getFileCategory } from '@/lib/utils'; +import { getFileCategory, redirectIfNotAuth } from '@/lib/utils'; import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { useMemo } from 'react'; @@ -32,6 +32,9 @@ export const Route = createFileRoute('/citizen-report-attachments/$electionRound component: AttachmentDetails, loader: ({ context: { queryClient }, params: { electionRoundId, citizenReportId, attachmentId } }) => queryClient.ensureQueryData(citizenReportAttachmentQueryOptions(electionRoundId, citizenReportId, attachmentId)), + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function AttachmentDetails() { diff --git a/web/src/routes/election-rounds/$electionRoundId.tsx b/web/src/routes/election-rounds/$electionRoundId.tsx index f47652ce9..a9a89c2ee 100644 --- a/web/src/routes/election-rounds/$electionRoundId.tsx +++ b/web/src/routes/election-rounds/$electionRoundId.tsx @@ -1,6 +1,7 @@ import { authApi } from '@/common/auth-api'; import { ElectionRound } from '@/features/election-round/models/ElectionRound'; +import { redirectIfNotAuth } from '@/lib/utils'; import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; @@ -23,7 +24,10 @@ export const electionRoundQueryOptions = (electionRoundId: string) => { export const Route = createFileRoute('/election-rounds/$electionRoundId')({ component: ElectionRoundDetails, - loader: ({ context: { queryClient }, params: { electionRoundId } }) => queryClient.ensureQueryData(electionRoundQueryOptions(electionRoundId)) + loader: ({ context: { queryClient }, params: { electionRoundId } }) => queryClient.ensureQueryData(electionRoundQueryOptions(electionRoundId)), + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function ElectionRoundDetails() { diff --git a/web/src/routes/election-rounds/index.tsx b/web/src/routes/election-rounds/index.tsx index c579a602d..b09f8bdce 100644 --- a/web/src/routes/election-rounds/index.tsx +++ b/web/src/routes/election-rounds/index.tsx @@ -5,6 +5,7 @@ import Layout from '@/components/layout/Layout'; import { useCallback, type ReactElement } from 'react'; import CreateElectionRound from '@/features/election-round/components/CreateElectionRound'; import { useElectionRounds } from '@/features/election-round/queries'; +import { redirectIfNotAuth } from '@/lib/utils'; function ElectionRounds(): ReactElement { const navigate = useNavigate(); @@ -29,4 +30,7 @@ function ElectionRounds(): ReactElement { export const Route = createFileRoute('/election-rounds/')({ component: ElectionRounds, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); diff --git a/web/src/routes/forms/$formId.tsx b/web/src/routes/forms/$formId.tsx index ebb28beb7..9b03faf37 100644 --- a/web/src/routes/forms/$formId.tsx +++ b/web/src/routes/forms/$formId.tsx @@ -1,5 +1,6 @@ import type { FunctionComponent } from '@/common/types'; import { formDetailsQueryOptions } from '@/features/forms/queries'; +import { redirectIfNotAuth } from '@/lib/utils'; import { createFileRoute, useLoaderData, useNavigate } from '@tanstack/react-router'; function Details(): FunctionComponent { @@ -26,5 +27,8 @@ export const Route = createFileRoute('/forms/$formId')({ const electionRoundId = currentElectionRoundContext.getState().currentElectionRoundId; return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId)) - } + }, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); diff --git a/web/src/routes/forms/$formId_.$languageCode.tsx b/web/src/routes/forms/$formId_.$languageCode.tsx index c35cffe36..6cbf8458c 100644 --- a/web/src/routes/forms/$formId_.$languageCode.tsx +++ b/web/src/routes/forms/$formId_.$languageCode.tsx @@ -1,5 +1,6 @@ import PreviewForm from '@/features/forms/components/PreviewForm/PreviewForm'; import { formDetailsQueryOptions } from '@/features/forms/queries'; +import { redirectIfNotAuth } from '@/lib/utils'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/forms/$formId/$languageCode')({ @@ -9,6 +10,9 @@ export const Route = createFileRoute('/forms/$formId/$languageCode')({ return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId)); }, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function Details() { diff --git a/web/src/routes/forms_.$formId.edit-translation.$languageCode.tsx b/web/src/routes/forms_.$formId.edit-translation.$languageCode.tsx index c4dfd9543..3614ed49f 100644 --- a/web/src/routes/forms_.$formId.edit-translation.$languageCode.tsx +++ b/web/src/routes/forms_.$formId.edit-translation.$languageCode.tsx @@ -1,13 +1,18 @@ import EditFormTranslation from '@/features/forms/components/EditFormTranslation/EditFormTranslation'; import { formDetailsQueryOptions } from '@/features/forms/queries'; +import { redirectIfNotAuth } from '@/lib/utils'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/forms/$formId/edit-translation/$languageCode')({ component: Edit, - loader: ({ context: { queryClient, currentElectionRoundContext }, params: { formId } }) =>{ + loader: ({ context: { queryClient, currentElectionRoundContext }, params: { formId } }) => { const electionRoundId = currentElectionRoundContext.getState().currentElectionRoundId; - return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId));} + return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId)); + }, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function Edit() { diff --git a/web/src/routes/forms_.$formId.edit.tsx b/web/src/routes/forms_.$formId.edit.tsx index 21e3e8230..3f1c21461 100644 --- a/web/src/routes/forms_.$formId.edit.tsx +++ b/web/src/routes/forms_.$formId.edit.tsx @@ -1,5 +1,6 @@ import EditForm from '@/features/forms/components/EditForm/EditForm'; import { formDetailsQueryOptions } from '@/features/forms/queries'; +import { redirectIfNotAuth } from '@/lib/utils'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/forms/$formId/edit')({ @@ -7,7 +8,10 @@ export const Route = createFileRoute('/forms/$formId/edit')({ loader: ({ context: { queryClient, currentElectionRoundContext }, params: { formId } }) => { const electionRoundId = currentElectionRoundContext.getState().currentElectionRoundId; - return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId)) + return queryClient.ensureQueryData(formDetailsQueryOptions(electionRoundId, formId)); + }, + beforeLoad: () => { + redirectIfNotAuth(); }, }); diff --git a/web/src/routes/monitoring-observers-import/index.tsx b/web/src/routes/monitoring-observers-import/index.tsx new file mode 100644 index 000000000..f719614b2 --- /dev/null +++ b/web/src/routes/monitoring-observers-import/index.tsx @@ -0,0 +1,13 @@ +import { redirectIfNotAuth } from '@/lib/utils'; +import { createFileRoute, Navigate } from '@tanstack/react-router'; + +export const Route = createFileRoute('/monitoring-observers-import/')({ + beforeLoad: () => { + redirectIfNotAuth(); + }, + component: Component, +}); + +function Component() { + return ; +} diff --git a/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx b/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx index 4d41c3325..4ef79975d 100644 --- a/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx +++ b/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx @@ -2,6 +2,7 @@ import { authApi } from '@/common/auth-api'; import EditMonitoringObserver from '@/features/monitoring-observers/components/EditMonitoringObserver/EditMonitoringObserver'; import { monitoringObserversKeys } from '@/features/monitoring-observers/hooks/monitoring-observers-queries'; import { MonitoringObserver } from '@/features/monitoring-observers/models/monitoring-observer'; +import { redirectIfNotAuth } from '@/lib/utils'; import { queryOptions } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; @@ -30,6 +31,9 @@ export const Route = createFileRoute('/monitoring-observers/edit/$monitoringObse return queryClient.ensureQueryData(monitoringObserverDetailsQueryOptions(electionRoundId, monitoringObserverId)); }, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function Edit() { diff --git a/web/src/routes/monitoring-observers/push-messages.$id.tsx b/web/src/routes/monitoring-observers/push-messages.$id.tsx index b38ee2d44..2ef222e27 100644 --- a/web/src/routes/monitoring-observers/push-messages.$id.tsx +++ b/web/src/routes/monitoring-observers/push-messages.$id.tsx @@ -1,6 +1,7 @@ import type { FunctionComponent } from '@/common/types'; import { createFileRoute, useLoaderData, useNavigate } from '@tanstack/react-router'; import { pushMessageDetailsQueryOptions } from './push-messages.$id_.view'; +import { redirectIfNotAuth } from '@/lib/utils'; function Details(): FunctionComponent { const navigate = useNavigate({ from: '/monitoring-observers/push-messages/$id' }); @@ -23,5 +24,8 @@ export const Route = createFileRoute('/monitoring-observers/push-messages/$id')( const electionRoundId = currentElectionRoundContext.getState().currentElectionRoundId; return queryClient.ensureQueryData(pushMessageDetailsQueryOptions(electionRoundId, id)); - } + }, + beforeLoad: () => { + redirectIfNotAuth(); + }, }); diff --git a/web/src/routes/observers_.$observerId.edit.tsx b/web/src/routes/observers_.$observerId.edit.tsx index a3e15d08b..98210495b 100644 --- a/web/src/routes/observers_.$observerId.edit.tsx +++ b/web/src/routes/observers_.$observerId.edit.tsx @@ -1,11 +1,15 @@ import EditObserver from '@/features/observers/components/EditObserver/EditObserver'; import { createFileRoute } from '@tanstack/react-router'; import { observerDetailsQueryOptions } from './observers/$observerId'; +import { redirectIfNotAuth } from '@/lib/utils'; export const Route = createFileRoute('/observers/$observerId/edit')({ component: Edit, loader: ({ context: { queryClient }, params: { observerId } }) => queryClient.ensureQueryData(observerDetailsQueryOptions(observerId)), + beforeLoad: () => { + redirectIfNotAuth(); + }, }); function Edit() { From 05e1c9f398f1aea4df3f5c17ef68829c398ef7d0 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Wed, 27 Nov 2024 16:45:02 +0200 Subject: [PATCH 3/4] Rework and cleanup observers import --- .../GetElectionsOverview/Endpoint.cs | 2 +- web/package.json | 2 + web/pnpm-lock.yaml | 18 ++ web/src/components/layout/Layout.tsx | 2 +- web/src/components/ui/file-uploader.tsx | 4 +- web/src/components/ui/stepper.tsx | 107 +++++---- .../MonitoringObserverQuickReports.tsx | 6 +- .../MonitoringObserverQuickReportsTable.tsx | 14 +- .../ImportObserverDataTableRowActions.tsx | 71 ++++++ .../ImportedObserversDataTable.tsx | 226 ++++++++++++++++++ .../MonitoringObserversImport.tsx | 188 +++++++++++++++ .../modals/delete-modal.tsx | 54 +++++ .../modals/edit-modal.tsx | 115 +++++++++ .../ImportMonitoringObserversDialog.tsx | 188 --------------- .../ImportMonitoringObserversErrorsDialog.tsx | 92 ------- .../MonitoringObserversList.tsx | 77 +++--- web/src/routeTree.gen.ts | 23 +- ...RoundId.$citizenReportId.$attachmentId.tsx | 2 +- .../monitoring-observers-import/index.tsx | 13 - .../routes/monitoring-observers/import.tsx | 10 + 20 files changed, 807 insertions(+), 407 deletions(-) create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportObserverDataTableRowActions.tsx create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportedObserversDataTable.tsx create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/edit-modal.tsx delete mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversDialog.tsx delete mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversErrorsDialog.tsx delete mode 100644 web/src/routes/monitoring-observers-import/index.tsx create mode 100644 web/src/routes/monitoring-observers/import.tsx diff --git a/api/src/Feature.Statistics/GetElectionsOverview/Endpoint.cs b/api/src/Feature.Statistics/GetElectionsOverview/Endpoint.cs index 8c05255d4..1aed3f2cf 100644 --- a/api/src/Feature.Statistics/GetElectionsOverview/Endpoint.cs +++ b/api/src/Feature.Statistics/GetElectionsOverview/Endpoint.cs @@ -126,7 +126,7 @@ AND FS."NumberOfQuestionsAnswered" > 0 WHERE "ElectionRoundId" = ANY (@electionRoundIds) AND "NumberOfQuestionsAnswered" > 0 - ) AS "NumberOfFormSubmissions"; + ) AS ""NumberOfQuestionsAnswered""; ----------------------------- -- number of questions answered diff --git a/web/package.json b/web/package.json index 93b39f1cb..2a0481967 100644 --- a/web/package.json +++ b/web/package.json @@ -59,6 +59,7 @@ "@tiptap/react": "^2.8.0", "@tiptap/starter-kit": "^2.8.0", "@types/lodash": "^4.17.7", + "@types/papaparse": "^5.3.15", "@uidotdev/usehooks": "^2.4.1", "axios": "^1.6.2", "chart.js": "^4.4.2", @@ -74,6 +75,7 @@ "i18next-browser-languagedetector": "^8.0.0", "lodash": "^4.17.21", "lucide-react": "^0.294.0", + "papaparse": "^5.4.1", "qs": "^6.12.0", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a5ffe9139..6e50d8d38 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: '@types/lodash': specifier: ^4.17.7 version: 4.17.7 + '@types/papaparse': + specifier: ^5.3.15 + version: 5.3.15 '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -179,6 +182,9 @@ importers: lucide-react: specifier: ^0.294.0 version: 0.294.0(react@18.2.0) + papaparse: + specifier: ^5.4.1 + version: 5.4.1 qs: specifier: ^6.12.0 version: 6.12.0 @@ -2121,6 +2127,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/papaparse@5.3.15': + resolution: {integrity: sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3834,6 +3843,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + papaparse@5.4.1: + resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -6848,6 +6860,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/papaparse@5.3.15': + dependencies: + '@types/node': 20.5.1 + '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.11': {} @@ -8846,6 +8862,8 @@ snapshots: p-try@2.2.0: {} + papaparse@5.4.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/web/src/components/layout/Layout.tsx b/web/src/components/layout/Layout.tsx index 3eb295268..6e6c7f9cc 100644 --- a/web/src/components/layout/Layout.tsx +++ b/web/src/components/layout/Layout.tsx @@ -4,7 +4,7 @@ import Breadcrumbs from './Breadcrumbs/Breadcrumbs'; import BackButton from './Breadcrumbs/BackButton'; interface LayoutProps { - title: string; + title?: string; subtitle?: string; enableBreadcrumbs?: boolean; breadcrumbs?: ReactNode; diff --git a/web/src/components/ui/file-uploader.tsx b/web/src/components/ui/file-uploader.tsx index 0f57943f1..759619e21 100644 --- a/web/src/components/ui/file-uploader.tsx +++ b/web/src/components/ui/file-uploader.tsx @@ -137,7 +137,7 @@ export function FileUploader(props: FileUploaderProps) { return (
- )} - + : null} {files?.length ? (
{files?.map((file, index) => ( diff --git a/web/src/components/ui/stepper.tsx b/web/src/components/ui/stepper.tsx index 562c9cd75..40005bd42 100644 --- a/web/src/components/ui/stepper.tsx +++ b/web/src/components/ui/stepper.tsx @@ -86,6 +86,16 @@ const StepperProvider = ({ value, children }: StepperContextProviderProps) => { // <---------- HOOKS ----------> +function usePrevious(value: T): T | undefined { + const ref = React.useRef() + + React.useEffect(() => { + ref.current = value + }, [value]) + + return ref.current +} + function useStepper() { const context = React.useContext(StepperContext) @@ -98,6 +108,8 @@ function useStepper() { const isLastStep = context.activeStep === context.steps.length - 1 const hasCompletedAllSteps = context.activeStep === context.steps.length + const previousActiveStep = usePrevious(context.activeStep) + const currentStep = context.steps[context.activeStep] const isOptionalStep = !!currentStep?.optional @@ -110,6 +122,7 @@ function useStepper() { isOptionalStep, isDisabledStep, currentStep, + previousActiveStep, } } @@ -147,7 +160,7 @@ interface StepOptions { responsive?: boolean checkIcon?: IconType errorIcon?: IconType - onClickStep?: (step: number) => void + onClickStep?: (step: number, setStep: (step: number) => void) => void mobileBreakpoint?: string variant?: "circle" | "circle-alt" | "line" expandVerticalSteps?: boolean @@ -262,6 +275,7 @@ const Stepper = React.forwardRef( expandVerticalSteps, steps, scrollTracking, + styles, }} >
{ errorIcon?: IconType isCompletedStep?: boolean isKeepError?: boolean - onClickStep?: (step: number) => void + onClickStep?: (step: number, setStep: (step: number) => void) => void } interface StepSharedProps extends StepProps { @@ -452,12 +466,16 @@ type VerticalStepProps = StepSharedProps & { } const verticalStepVariants = cva( - "flex flex-col relative transition-all duration-200", + [ + "flex flex-col relative transition-all duration-200", + "data-[completed=true]:[&:not(:last-child)]:after:bg-primary", + "data-[invalid=true]:[&:not(:last-child)]:after:bg-destructive", + ], { variants: { variant: { circle: cn( - "pb-[var(--step-gap)] gap-[var(--step-gap)]", + "[&:not(:last-child)]:pb-[var(--step-gap)] [&:not(:last-child)]:gap-[var(--step-gap)]", "[&:not(:last-child)]:after:content-[''] [&:not(:last-child)]:after:w-[2px] [&:not(:last-child)]:after:bg-border", "[&:not(:last-child)]:after:inset-x-[calc(var(--step-icon-size)/2)]", "[&:not(:last-child)]:after:absolute", @@ -501,12 +519,17 @@ const VerticalStep = React.forwardRef( scrollTracking, orientation, steps, + setStep, + isLastStep: isLastStepCurrentStep, + previousActiveStep, } = useStepper() const opacity = hasVisited ? 1 : 0.8 const localIsLoading = isLoading || state === "loading" const localIsError = isError || state === "error" + const isLastStep = index === steps.length - 1 + const active = variant === "line" ? isCompletedStep || isCurrentStep : isCompletedStep const checkIcon = checkIconProp || checkIconContext @@ -516,7 +539,27 @@ const VerticalStep = React.forwardRef( if (!expandVerticalSteps) { return ( - + { + if ( + // If the step is the first step and the previous step + // was the last step or if the step is not the first step + // This prevents initial scrolling when the stepper + // is located anywhere other than the top of the view. + scrollTracking && + ((index === 0 && + previousActiveStep && + previousActiveStep === steps.length) || + (index && index > 0)) + ) { + node?.scrollIntoView({ + behavior: "smooth", + block: "center", + }) + } + }} + className="overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up" + > {children} @@ -533,8 +576,7 @@ const VerticalStep = React.forwardRef( verticalStepVariants({ variant: variant?.includes("circle") ? "circle" : "line", }), - isCompletedStep && - "[&:not(:last-child)]:after:bg-blue-500 [&:not(:last-child)]:after:data-[invalid=true]:bg-destructive", + isLastStepCurrentStep && "gap-[var(--step-gap)]", styles?.["vertical-step"] )} data-optional={steps[index || 0]?.optional} @@ -543,7 +585,8 @@ const VerticalStep = React.forwardRef( data-clickable={clickable || !!onClickStep} data-invalid={localIsError} onClick={() => - onClickStep?.(index || 0) || onClickStepGeneral?.(index || 0) + onClickStep?.(index || 0, setStep) || + onClickStepGeneral?.(index || 0, setStep) } >
( "stepper__vertical-step-container", "flex items-center", variant === "line" && - "border-s-[3px] data-[active=true]:border-blue-500 py-2 ps-3", + "border-s-[3px] data-[active=true]:border-primary py-2 ps-3", styles?.["vertical-step-container"] )} > @@ -580,17 +623,9 @@ const VerticalStep = React.forwardRef( />
{ - if (scrollTracking) { - node?.scrollIntoView({ - behavior: "smooth", - block: "center", - }) - } - }} className={cn( "stepper__vertical-step-content", - "min-h-4", + !isLastStep && "min-h-4", variant !== "line" && "ps-[--step-icon-size]", variant === "line" && orientation === "vertical" && "min-h-0", styles?.["vertical-step-content"] @@ -617,6 +652,7 @@ const HorizontalStep = React.forwardRef( errorIcon: errorIconContext, styles, steps, + setStep, } = useStepper() const { @@ -653,14 +689,14 @@ const HorizontalStep = React.forwardRef( "[&:not(:last-child)]:flex-1", "[&:not(:last-child)]:after:transition-all [&:not(:last-child)]:after:duration-200", "[&:not(:last-child)]:after:content-[''] [&:not(:last-child)]:after:h-[2px] [&:not(:last-child)]:after:bg-border", + "data-[completed=true]:[&:not(:last-child)]:after:bg-primary", + "data-[invalid=true]:[&:not(:last-child)]:after:bg-destructive", variant === "circle-alt" && "justify-start flex-col flex-1 [&:not(:last-child)]:after:relative [&:not(:last-child)]:after:order-[-1] [&:not(:last-child)]:after:start-[50%] [&:not(:last-child)]:after:end-[50%] [&:not(:last-child)]:after:top-[calc(var(--step-icon-size)/2)] [&:not(:last-child)]:after:w-[calc((100%-var(--step-icon-size))-(var(--step-gap)))]", variant === "circle" && - "[&:not(:last-child)]:after:flex-1 [&:not(:last-child)]:after:ms-2 [&:not(:last-child)]:after:me-2", + "[&:not(:last-child)]:after:flex-1 [&:not(:last-child)]:after:ms-[var(--step-gap)] [&:not(:last-child)]:after:me-[var(--step-gap)]", variant === "line" && - "flex-col flex-1 border-t-[3px] data-[active=true]:border-blue-500", - isCompletedStep && - "[&:not(:last-child)]:after:bg-blue-500 [&:not(:last-child)]:after:data-[invalid=true]:bg-destructive", + "flex-col flex-1 border-t-[3px] data-[active=true]:border-primary", styles?.["horizontal-step"] )} data-optional={steps[index || 0]?.optional} @@ -668,7 +704,7 @@ const HorizontalStep = React.forwardRef( data-active={active} data-invalid={localIsError} data-clickable={clickable} - onClick={() => onClickStep?.(index || 0)} + onClick={() => onClickStep?.(index || 0, setStep)} ref={ref} >
Object.keys(search).length !== 0); const [columnsVisibility, setColumnsVisibility] = useState(observerQuickReportsColumns); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserverQuickReportsTable/MonitoringObserverQuickReportsTable.tsx b/web/src/features/monitoring-observers/components/MonitoringObserverQuickReportsTable/MonitoringObserverQuickReportsTable.tsx index 071f491a1..12f7fc53a 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserverQuickReportsTable/MonitoringObserverQuickReportsTable.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserverQuickReportsTable/MonitoringObserverQuickReportsTable.tsx @@ -2,16 +2,14 @@ import { DataSources, type FunctionComponent } from '@/common/types'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { CardContent } from '@/components/ui/card'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { QuickReportFilterRequest } from '@/features/responses/components/QuickReportsTab/QuickReportsTab'; import { useQuickReports } from '@/features/responses/hooks/quick-reports'; import { observerQuickReportsColumnDefs } from '@/features/responses/utils/column-defs'; -import { getRouteApi } from '@tanstack/react-router'; +import { Route } from '@/routes/monitoring-observers/view/$monitoringObserverId.$tab'; +import { useNavigate } from '@tanstack/react-router'; import type { VisibilityState } from '@tanstack/react-table'; import { useDebounce } from '@uidotdev/usehooks'; import { useCallback, useMemo } from 'react'; -import type { MonitoringObserverDetailsRouteSearch } from '../../models/monitoring-observer'; -import { QuickReportFilterRequest } from '@/features/responses/components/QuickReportsTab/QuickReportsTab'; - -const routeApi = getRouteApi('/monitoring-observers/view/$monitoringObserverId/$tab'); type QuickReportsTableByEntryProps = { columnsVisibility: VisibilityState; @@ -22,9 +20,9 @@ export function MonitoringObserverQuickReportsTable({ columnsVisibility, searchText, }: QuickReportsTableByEntryProps): FunctionComponent { - const navigate = routeApi.useNavigate(); - const { monitoringObserverId } = routeApi.useParams(); - const search = routeApi.useSearch(); + const navigate = useNavigate(); + const { monitoringObserverId } = Route.useParams(); + const search = Route.useSearch(); const debouncedSearch = useDebounce(search, 300); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportObserverDataTableRowActions.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportObserverDataTableRowActions.tsx new file mode 100644 index 000000000..34b87e1ee --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportObserverDataTableRowActions.tsx @@ -0,0 +1,71 @@ + + +import { Row } from '@tanstack/react-table'; +import * as React from 'react'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; +import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'; +import { ImportObserverRow } from './MonitoringObserversImport'; +import DeleteDialog from './modals/delete-modal'; +import EditDialog from './modals/edit-modal'; + +interface DataTableRowActionsProps { + row: Row; + updateObserver: (observer: ImportObserverRow) => void; + deleteObserver: (observer: ImportObserverRow) => void; +} + +export function ImportObserverDataTableRowActions({ row, deleteObserver, updateObserver }: DataTableRowActionsProps) { + const [dialogContent, setDialogContent] = React.useState(null); + const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); + + const handleEditClick = () => { + setDialogContent(); + }; + + return ( + + + + + + + Actions + + + + + + Edit + + + setShowDeleteDialog(true)} className='text-red-600'> + + Delete + + + + + {dialogContent && {dialogContent}} + + + ); +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportedObserversDataTable.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportedObserversDataTable.tsx new file mode 100644 index 000000000..d7e51e2d9 --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/ImportedObserversDataTable.tsx @@ -0,0 +1,226 @@ +import { FunctionComponent } from '@/common/types'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ImportObserverRow } from './MonitoringObserversImport'; + +import { DataTablePagination } from '@/components/ui/DataTable/DataTablePagination'; +import { ColumnDef } from '@tanstack/react-table'; + +import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +import { ExclamationTriangleIcon } from '@heroicons/react/24/solid'; +import { useMemo, useState } from 'react'; +import { ImportObserverDataTableRowActions } from './ImportObserverDataTableRowActions'; +import { Input } from '@/components/ui/input'; + +type ImportedObserversDataTableProps = { + data: ImportObserverRow[]; + updateObserver: (observer: ImportObserverRow) => void; + deleteObserver: (observer: ImportObserverRow) => void; +}; + +export function ImportedObserversDataTable({ + data, + updateObserver, + deleteObserver, +}: ImportedObserversDataTableProps): FunctionComponent { + const tableCols: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'firstName', + header: ({ column }) => , + cell: ({ row }) => + row.original.errors?.some((er) => er.path.some((path) => path === 'firstName')) ? ( +
+ {row.original.firstName} + + + + + + + + + +
+ {row.original.errors + ?.filter((error) => error.path.some((path) => path === 'firstName')) + .map((error) =>
{error.message}
)} +
+
+
+
+
+ ) : ( +
{row.original.firstName}
+ ), + }, + { + accessorKey: 'lastName', + header: ({ column }) => , + cell: ({ row }) => + row.original.errors?.some((er) => er.path.some((path) => path === 'lastName')) ? ( +
+ {row.original.lastName} + + + + + + + + + +
+ {row.original.errors + ?.filter((error) => error.path.some((path) => path === 'lastName')) + .map((error) =>
{error.message}
)} +
+
+
+
+
+ ) : ( +
{row.original.lastName}
+ ), + }, + { + accessorKey: 'email', + header: ({ column }) => , + cell: ({ row }) => + row.original.errors?.some((er) => er.path.some((path) => path === 'email')) ? ( +
+ {row.original.email} + + + + + + + + + +
+ {row.original.errors + ?.filter((error) => error.path.some((path) => path === 'email')) + .map((error) =>
{error.message}
)} +
+
+
+
+
+ ) : ( +
{row.original.email}
+ ), + }, + { + accessorKey: 'phoneNumber', + header: ({ column }) => , + cell: ({ row }) =>
{row.original.phoneNumber}
, + }, + + { + accessorKey: 'errors', + header: ({ column }) => , + cell: ({ row }) => + row.original.errors?.length ? ( + + + + + {row.original.errors?.length} + + + +
+ {row.original.errors?.map((error) =>
{error.message}
)} +
+
+
+
+ ) : null, + }, + + { + id: 'actions', + cell: ({ row }) => ( + + ), + }, + ], + [updateObserver, deleteObserver] + ); + const [globalFilter, setGlobalFilter] = useState(''); + + const table = useReactTable({ + columns: tableCols, + data, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + globalFilterFn: (row, columnId, filterValue) => { + // Custom filter function + return Object.values(row.original).some((value) => + String(value).toLowerCase().includes(filterValue.toLowerCase()) + ); + }, + state: { globalFilter }, + }); + + const rows = table.getFilteredRowModel().rows; + + return ( +
+
+ setGlobalFilter(e.target.value)} /> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + + {rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ +
+ ); +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx new file mode 100644 index 000000000..e0a62f33c --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx @@ -0,0 +1,188 @@ +import { FunctionComponent } from '@/common/types'; +import Layout from '@/components/layout/Layout'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { FileUploader } from '@/components/ui/file-uploader'; +import { Separator } from '@/components/ui/separator'; +import Papa from 'papaparse'; +import { useMemo, useState } from 'react'; +import { ZodIssue, z } from 'zod'; + +import { authApi } from '@/common/auth-api'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { downloadImportExample } from '@/features/monitoring-observers/helpers'; +import { queryClient } from '@/main'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; +import { LoaderIcon } from 'lucide-react'; +import { ImportedObserversDataTable } from './ImportedObserversDataTable'; +import { useNavigate } from '@tanstack/react-router'; + +export const importObserversSchema = z.object({ + firstName: z + .string() + .min(1, { message: 'First name is required.' }) + .max(256, { message: 'First name cannot exceed 256 characters.' }), + lastName: z + .string() + .min(1, { message: 'Last name is required.' }) + .max(256, { message: 'Last name cannot exceed 256 characters.' }), + email: z + .string() + .min(1, { message: 'Email is required.' }) + .refine((value) => z.string().email().safeParse(value).success, { + message: 'Invalid email format.', + }), + phoneNumber: z.string().max(256, { message: 'Phone number cannot exceed 256 characters.' }).optional(), +}); + +export type ImportObserverRow = z.infer & { id: string; errors: ZodIssue[] }; + +export function MonitoringObserversImport(): FunctionComponent { + const [observers, setObservers] = useState([]); + const { t } = useTranslation('translation', { keyPrefix: 'observers.addObserver' }); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const navigate = useNavigate(); + + function deleteObserver(observer: ImportObserverRow) { + setObservers((prev) => [...prev.filter((obs) => obs.id !== observer.id)]); + } + + function updateObserver(observer: ImportObserverRow) { + const validationResult = importObserversSchema.safeParse(observer); + + const observerWithErorrs = { + ...observer, + errors: validationResult.success ? [] : validationResult.error.errors, + }; + + setObservers((prevData) => prevData.map((o) => (o.id === observer.id ? { ...o, ...observerWithErorrs } : o))); + } + + const hasInvalidObservers = useMemo(() => { + return observers.some((observer) => observer.errors.length > 0); + }, [observers]); + + const { mutate, isPending } = useMutation({ + mutationFn: ({ electionRoundId, observers }: { electionRoundId: string; observers: ImportObserverRow[] }) => { + return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers }); + }, + + onSuccess: (_, { electionRoundId }) => { + toast({ + title: 'Success', + description: t('onSuccess'), + }); + + queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) }); + navigate({ to: '/monitoring-observers' }); + }, + onError: () => { + toast({ + title: t('onError'), + description: 'Please contact tech support', + variant: 'destructive', + }); + }, + }); + + function handleImportObservers() { + mutate({ electionRoundId: currentElectionRoundId, observers }); + } + + return ( + + + + Import monitoring observer list + + + In order to successfully import a list of monitoring observers, please use the template provided below. + Download the template, fill it in with the observer information and then upload it. No other format is + accepted for import. + + + + +
+
+ + monitoring_observers_template.csv +
+
+ { + const file = files[0]; + if (!file) { + setObservers([]); + } else { + Papa.parse(file, { + header: true, + skipEmptyLines: true, + // worker: true, + transformHeader: (header) => header.charAt(0).toLowerCase() + header.slice(1), + async complete(results) { + if (results.errors.length) { + console.error('Parsing errors:', results.errors); + // Optionally show an error message to the user. + } + + const validatedObservers = results.data.map((observer) => { + const observerWithId = { + ...observer, + id: crypto.randomUUID(), + }; + + const validationResult = importObserversSchema.safeParse(observerWithId); + + return { + ...observerWithId, + errors: validationResult.success ? [] : validationResult.error.errors, + }; + }); + + setObservers(validatedObservers); + }, + }); + } + }} + /> +
+
+ {observers.length ? ( + + + Observers to be imported + +
+ Review the data and correct the errors before import {' '} + +
{' '} +
+ +
+ +
+ {' '} + {' '} +
+
+
+ ) : null} +
+ ); +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx new file mode 100644 index 000000000..af5b1a6a0 --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx @@ -0,0 +1,54 @@ +"use client" + +// * * This is just a demostration of delete modal, actual functionality may vary + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + } from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { ImportObserverRow } from "../MonitoringObserversImport"; + + type DeleteProps = { + observer: ImportObserverRow; + isOpen: boolean; + showActionToggle: (open: boolean) => void; + deleteObserver: (observer:ImportObserverRow)=>void; + }; + + export default function DeleteDialog({ + observer, + isOpen, + showActionToggle, + deleteObserver + }: DeleteProps) { + return ( + + + + Are you sure absolutely sure ? + + This action cannot be undone. You are about to delete {observer.firstName} {observer.lastName} {observer.email} + + + + Cancel + + + + + ); + } \ No newline at end of file diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/edit-modal.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/edit-modal.tsx new file mode 100644 index 000000000..192710381 --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/edit-modal.tsx @@ -0,0 +1,115 @@ + +import { Button } from '@/components/ui/button'; +import { DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { ImportObserverRow, importObserversSchema } from '../MonitoringObserversImport'; +import { z } from 'zod'; +import { useEffect } from 'react'; + +type EditProps = { + observer: ImportObserverRow; + updateObserver: (observer: ImportObserverRow) => void; +}; + +export default function EditDialog({ observer, updateObserver }: EditProps) { + const editObserversSchema = importObserversSchema.extend({ + id: z.string(), + }); + + const form = useForm({ + resolver: zodResolver(editObserversSchema), + mode: 'onChange', + reValidateMode: 'onChange', + defaultValues: { + id: observer.id, + firstName: observer.firstName, + lastName: observer.lastName, + email: observer.email, + phoneNumber: observer.phoneNumber, + }, + }); + + useEffect(() => { + form.trigger(); + }, [form.trigger]); + + function onSubmit(updatedObserver: ImportObserverRow) { + updateObserver(updatedObserver); + } + + return ( + <> + + Edit Observer + +
+
+ + ( + + First name + + + + + + )} + /> + + ( + + Last name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + ( + + Phone number + + + + + + )} + /> + + + + +
+ + ); +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversDialog.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversDialog.tsx deleted file mode 100644 index 879d4c191..000000000 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversDialog.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { authApi } from '@/common/auth-api'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Separator } from '@/components/ui/separator'; -import { toast } from '@/components/ui/use-toast'; -import { queryClient } from '@/main'; -import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; -import { useMutation } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; - -import { FileUploader } from '@/components/ui/file-uploader'; -import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { downloadImportExample } from '../../helpers'; -import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; -export interface ImportMonitoringObserversDialogProps { - onImportError: (fileId: string) => void; - open: boolean; - onOpenChange: (open: any) => void; -} - -function ImportMonitoringObserversDialog({ onImportError, open, onOpenChange }: ImportMonitoringObserversDialogProps) { - const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); - - const importObserversSchema = z.object({ - file: z.array(z.instanceof(File)).length(1), - }); - - type ImportObserverType = z.infer; - - const form = useForm({ - resolver: zodResolver(importObserversSchema), - defaultValues: { - file: [], - }, - }); - - useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset({}, { keepValues: false }); - onOpenChange(false); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - - const importObserversMutation = useMutation({ - mutationFn: ({ electionRoundId, file }: { electionRoundId: string; file: File }) => { - // create a new FormData object and append the file to it - const formData = new FormData(); - formData.append('file', file); - - return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers:import`, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - }, - - onSuccess: (_, {electionRoundId}) => { - queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.lists(electionRoundId) }); - queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.tags(electionRoundId) }); - - toast({ - title: 'Success', - description: 'Import was successful', - }); - }, - - onError: (error: AxiosError, variables, ctx) => { - if (error.response?.status === 400) { - // @ts-ignore - const importErrorsFileId = error.response.data.id; - if (importErrorsFileId) { - onImportError(importErrorsFileId); - } else { - toast({ - title: 'Error importing monitoring observers', - description: 'Please contact Platform admins', - variant: 'destructive', - }); - } - } else { - toast({ - title: 'Error importing monitoring observers', - description: 'Please contact Platform admins', - variant: 'destructive', - }); - } - - onOpenChange(false); - }, - }); - - function onSubmit({ file }: ImportObserverType): void { - importObserversMutation.mutate({ electionRoundId: currentElectionRoundId, file: file[0]! }); - } - - return ( - - { - e.preventDefault(); - }} - onEscapeKeyDown={(e) => { - e.preventDefault(); - }}> - - Import monitoring observer list - - -
- In order to successfully import a list of monitoring observers, please use the template provided below. - Download the template, fill it in with the observer information and then upload it. No other format is - accepted for import. -
-
-
-
-

- Download template * -

-
-
- - monitoring_observers_template.csv -
-
28kb
-
- -
- - ( -
- - - - - - - -
- )} - /> - - - - - - - - - -
-
-
- ); -} - -export default ImportMonitoringObserversDialog; diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversErrorsDialog.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversErrorsDialog.tsx deleted file mode 100644 index b8c14553b..000000000 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/ImportMonitoringObserversErrorsDialog.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { authApi } from '@/common/auth-api'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Separator } from '@/components/ui/separator'; -import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; - -export interface ImportMonitoringObserversErrorsDialogProps { - fileId: string; - open: boolean; - onOpenChange: (open: any) => void; -} - -function ImportMonitoringObserversErrorsDialog({ - fileId, - open, - onOpenChange -}: ImportMonitoringObserversErrorsDialogProps) { - - const downloadImportErrorsFile = async () => { - const res = await authApi.get(`/import-errors/${fileId}`, { responseType: "blob" }); - const csvData = res.data; - - const blob = new Blob([csvData], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = 'import-errors.csv'; - - document.body.appendChild(a); - a.click(); - - window.URL.revokeObjectURL(url); - }; - - return ( - - { - e.preventDefault(); - }} onEscapeKeyDown={(e) => { - e.preventDefault(); - }}> - - Data import failed - - -
- We encountered issues during the data import process from your CSV file. - To assist you in resolving these issues, you can download a detailed error report by clicking the link provided below. - We kindly ask you to review this report thoroughly and address any errors before proceeding with the reimport of your data. -
-
-
-
-

- Download error response -

-
-
- - import-errors.csv -
-
-
- If you require any further assistance or have questions, please do not hesitate to reach out to our support team -
- -
- - - - - -
-
- ) -} - -export default ImportMonitoringObserversErrorsDialog; \ No newline at end of file diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index 5812ae1a0..b267bbf94 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -3,7 +3,6 @@ import TableTagList from '@/components/table-tag-list/TableTagList'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { DropdownMenu, @@ -16,14 +15,17 @@ import { Separator } from '@/components/ui/separator'; import { useDialog } from '@/components/ui/use-dialog'; import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; -import { useNavigate, useRouter } from '@tanstack/react-router'; +import { Link, useNavigate, useRouter } from '@tanstack/react-router'; import { CellContext, ColumnDef } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; import { DateTimeFormat } from '@/common/formats'; +import { ElectionRoundStatus } from '@/common/types'; import { TableCellProps } from '@/components/ui/DataTable/DataTable'; +import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import i18n from '@/i18n'; @@ -35,12 +37,8 @@ import { Plus } from 'lucide-react'; import { MonitoringObserversListFilters } from '../../filtering/MonitoringObserversListFilters'; import { monitoringObserversKeys, useMonitoringObservers } from '../../hooks/monitoring-observers-queries'; import { MonitoringObserver, MonitoringObserverStatus } from '../../models/monitoring-observer'; -import ImportMonitoringObserversDialog from '../MonitoringObserversList/ImportMonitoringObserversDialog'; -import ImportMonitoringObserversErrorsDialog from '../MonitoringObserversList/ImportMonitoringObserversErrorsDialog'; import ConfirmResendInvitationDialog from './ConfirmResendInvitationDialog'; import CreateMonitoringObserverDialog from './CreateMonitoringObserverDialog'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; -import { ElectionRoundStatus } from '@/common/types'; function MonitoringObserversList() { const navigate = useNavigate(); @@ -137,11 +135,9 @@ function MonitoringObserversList() { const debouncedSearch = useDebounce(search, 300); const debouncedSearchText = useDebounce(searchText, 300); - const [importErrorsFileId, setImportErrorsFileId] = useState(); const [monitoringObserverId, setMonitoringObserverId] = useState(); const createMonitoringObserverDialog = useDialog(); const importMonitoringObserversDialog = useDialog(); - const importMonitoringObserverErrorsDialog = useDialog(); const confirmResendInvitesDialog = useDialog(); const { filteringIsActive, navigateHandler } = useFilteringContainer(); const [filtersExpanded, setFiltersExpanded] = useState(false); @@ -262,43 +258,35 @@ function MonitoringObserversList() { Monitoring observers list -
- {!!importErrorsFileId && ( - - )} +
+ + + - { - setImportErrorsFileId(fileId); - importMonitoringObserverErrorsDialog.trigger(); - }} - /> - @@ -323,7 +311,10 @@ function MonitoringObserversList() { Export monitoring observer list - diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 3c69078dd..f26be3745 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -17,7 +17,6 @@ import { Route as ResetPasswordIndexImport } from './routes/reset-password/index import { Route as ObserversIndexImport } from './routes/observers/index' import { Route as NgosIndexImport } from './routes/ngos/index' import { Route as MonitoringObserversIndexImport } from './routes/monitoring-observers/index' -import { Route as MonitoringObserversImportIndexImport } from './routes/monitoring-observers-import/index' import { Route as LoginIndexImport } from './routes/login/index' import { Route as ForgotPasswordIndexImport } from './routes/forgot-password/index' import { Route as ElectionRoundsIndexImport } from './routes/election-rounds/index' @@ -27,6 +26,7 @@ import { Route as ResetPasswordSuccessImport } from './routes/reset-password/suc import { Route as ObserversObserverIdImport } from './routes/observers/$observerId' import { Route as ObserverGuidesNewImport } from './routes/observer-guides/new' import { Route as NgosNgoIdImport } from './routes/ngos/$ngoId' +import { Route as MonitoringObserversImportImport } from './routes/monitoring-observers/import' import { Route as MonitoringObserversCreateNewMessageImport } from './routes/monitoring-observers/create-new-message' import { Route as MonitoringObserversTabImport } from './routes/monitoring-observers/$tab' import { Route as FormsFormIdImport } from './routes/forms/$formId' @@ -89,12 +89,6 @@ const MonitoringObserversIndexRoute = MonitoringObserversIndexImport.update({ getParentRoute: () => rootRoute, } as any) -const MonitoringObserversImportIndexRoute = - MonitoringObserversImportIndexImport.update({ - path: '/monitoring-observers-import/', - getParentRoute: () => rootRoute, - } as any) - const LoginIndexRoute = LoginIndexImport.update({ path: '/login/', getParentRoute: () => rootRoute, @@ -140,6 +134,11 @@ const NgosNgoIdRoute = NgosNgoIdImport.update({ getParentRoute: () => rootRoute, } as any) +const MonitoringObserversImportRoute = MonitoringObserversImportImport.update({ + path: '/monitoring-observers/import', + getParentRoute: () => rootRoute, +} as any) + const MonitoringObserversCreateNewMessageRoute = MonitoringObserversCreateNewMessageImport.update({ path: '/monitoring-observers/create-new-message', @@ -343,6 +342,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MonitoringObserversCreateNewMessageImport parentRoute: typeof rootRoute } + '/monitoring-observers/import': { + preLoaderRoute: typeof MonitoringObserversImportImport + parentRoute: typeof rootRoute + } '/ngos/$ngoId': { preLoaderRoute: typeof NgosNgoIdImport parentRoute: typeof rootRoute @@ -379,10 +382,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginIndexImport parentRoute: typeof rootRoute } - '/monitoring-observers-import/': { - preLoaderRoute: typeof MonitoringObserversImportIndexImport - parentRoute: typeof rootRoute - } '/monitoring-observers/': { preLoaderRoute: typeof MonitoringObserversIndexImport parentRoute: typeof rootRoute @@ -502,6 +501,7 @@ export const routeTree = rootRoute.addChildren([ FormsFormIdRoute, MonitoringObserversTabRoute, MonitoringObserversCreateNewMessageRoute, + MonitoringObserversImportRoute, NgosNgoIdRoute, ObserverGuidesNewRoute, ObserversObserverIdRoute, @@ -511,7 +511,6 @@ export const routeTree = rootRoute.addChildren([ ElectionRoundsIndexRoute, ForgotPasswordIndexRoute, LoginIndexRoute, - MonitoringObserversImportIndexRoute, MonitoringObserversIndexRoute, NgosIndexRoute, ObserversIndexRoute, diff --git a/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx b/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx index 6217cf928..1be170e0d 100644 --- a/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx +++ b/web/src/routes/citizen-report-attachments/$electionRoundId.$citizenReportId.$attachmentId.tsx @@ -4,7 +4,7 @@ import { getFileCategory, redirectIfNotAuth } from '@/lib/utils'; import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute } from '@tanstack/react-router'; import { useMemo } from 'react'; -import ReactPlayer from 'react-player'; +import ReactPlayer from 'react-player/lazy'; export const citizenReportAttachmentQueryOptions = ( electionRoundId: string, diff --git a/web/src/routes/monitoring-observers-import/index.tsx b/web/src/routes/monitoring-observers-import/index.tsx deleted file mode 100644 index f719614b2..000000000 --- a/web/src/routes/monitoring-observers-import/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { redirectIfNotAuth } from '@/lib/utils'; -import { createFileRoute, Navigate } from '@tanstack/react-router'; - -export const Route = createFileRoute('/monitoring-observers-import/')({ - beforeLoad: () => { - redirectIfNotAuth(); - }, - component: Component, -}); - -function Component() { - return ; -} diff --git a/web/src/routes/monitoring-observers/import.tsx b/web/src/routes/monitoring-observers/import.tsx new file mode 100644 index 000000000..717dc7454 --- /dev/null +++ b/web/src/routes/monitoring-observers/import.tsx @@ -0,0 +1,10 @@ +import { MonitoringObserversImport } from '@/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport'; +import { redirectIfNotAuth } from '@/lib/utils'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/monitoring-observers/import')({ + beforeLoad: () => { + redirectIfNotAuth(); + }, + component: MonitoringObserversImport +}); From ad30e081455fa5cccf9f869a786ffaa7abbd2be9 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Wed, 27 Nov 2024 16:51:05 +0200 Subject: [PATCH 4/4] cleanup --- web/src/components/ui/dual-range-slider.tsx | 2 -- web/src/components/ui/timeline-slider.tsx | 2 -- .../MonitoringObserversImport/modals/delete-modal.tsx | 4 ---- 3 files changed, 8 deletions(-) diff --git a/web/src/components/ui/dual-range-slider.tsx b/web/src/components/ui/dual-range-slider.tsx index c746aa262..9911bf412 100644 --- a/web/src/components/ui/dual-range-slider.tsx +++ b/web/src/components/ui/dual-range-slider.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import * as SliderPrimitive from '@radix-ui/react-slider'; diff --git a/web/src/components/ui/timeline-slider.tsx b/web/src/components/ui/timeline-slider.tsx index a732e2051..b3e6cc77d 100644 --- a/web/src/components/ui/timeline-slider.tsx +++ b/web/src/components/ui/timeline-slider.tsx @@ -1,5 +1,3 @@ -'use client'; - import * as React from 'react'; import * as SliderPrimitive from '@radix-ui/react-slider'; diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx index af5b1a6a0..d51836ba3 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/modals/delete-modal.tsx @@ -1,7 +1,3 @@ -"use client" - -// * * This is just a demostration of delete modal, actual functionality may vary - import { AlertDialog, AlertDialogCancel,