From a1948a3de8d06778782c089bffa79893d04bd0a8 Mon Sep 17 00:00:00 2001 From: "ion.dormenco" Date: Tue, 17 Mar 2026 14:12:48 +0200 Subject: [PATCH] tnp work --- web/package.json | 4 +- web/pnpm-lock.yaml | 95 ++++++-- web/src/api/auth/accept-invite.ts | 12 + web/src/api/auth/forgot-password.ts | 10 + web/src/api/auth/index.ts | 6 + web/src/api/auth/login.ts | 7 + web/src/api/auth/reset-password.ts | 12 + web/src/api/auth/set-password.ts | 11 + .../election-event/delete-citizen-guide.ts | 6 + .../election-event/delete-observer-guide.ts | 6 + .../get-citizen-guide-details.ts | 18 ++ .../api/election-event/get-citizen-guides.ts | 15 ++ .../election-event/get-coalition-details.ts | 11 + .../api/election-event/get-election-event.ts | 11 + .../get-observer-guide-details.ts | 18 ++ .../api/election-event/get-observer-guides.ts | 15 ++ .../election-event/update-citizen-guide.ts | 19 ++ .../api/election-event/update-guide-access.ts | 16 ++ .../election-event/update-observer-guide.ts | 11 + .../election-event/upload-citizen-guide.ts | 10 + .../election-event/upload-observer-guide.ts | 10 + .../api/election-rounds/add-monitoring-ngo.ts | 6 + .../election-rounds/archive-election-round.ts | 6 + .../election-rounds/create-election-round.ts | 13 ++ .../election-rounds/delete-monitoring-ngo.ts | 6 + .../get-available-monitoring-ngos.ts | 24 ++ .../get-election-round-details.ts | 13 ++ .../get-election-round-statistics.ts | 19 ++ .../election-rounds/get-election-rounds.ts | 29 +++ .../election-rounds/get-monitoring-ngos.ts | 21 ++ .../election-rounds/start-election-round.ts | 6 + .../unarchive-election-round.ts | 6 + .../election-rounds/unstart-election-round.ts | 6 + .../election-rounds/update-election-round.ts | 12 + .../create-monitoring-observers.ts | 17 ++ .../export-monitoring-observers.ts | 10 + .../get-monitoring-observer-details.ts | 18 ++ .../get-monitoring-observers.ts | 35 +++ .../get-push-message-details.ts | 18 ++ .../monitoring-observers/get-push-messages.ts | 31 +++ .../get-targeted-monitoring-observers.ts | 32 +++ .../resend-monitoring-observer-invites.ts | 11 + .../send-push-notification.ts | 14 ++ .../update-monitoring-observer.ts | 14 ++ web/src/api/observers/create-observer.ts | 7 + web/src/api/observers/delete-observer.ts | 6 + web/src/api/observers/get-observer-details.ts | 13 ++ web/src/api/observers/get-observers.ts | 21 ++ .../api/observers/toggle-observer-status.ts | 8 + web/src/api/observers/update-observer.ts | 7 + .../ElectionEventDescription.tsx | 37 +-- web/src/components/FormEditor/FormEditor.tsx | 213 ++++++++++-------- .../FormTranslationEditor.tsx | 52 +++-- .../LocationsDashboard/LocationsDashboard.tsx | 23 +- .../usePasswordSetterDialog.ts | 21 +- .../CreatePollingStationDialog.tsx | 11 +- .../PollingStationsDashboard.tsx | 23 +- .../rich-text-editor/styles/index.css | 2 + web/src/components/tag/tag-input.tsx | 18 +- web/src/components/ui/file-uploader.tsx | 11 +- web/src/components/ui/sonner.tsx | 52 +++++ web/src/components/ui/toast.tsx | 127 ----------- web/src/components/ui/toaster.tsx | 33 --- web/src/components/ui/use-toast.ts | 192 ---------------- web/src/context/auth.context.tsx | 10 +- .../CitizenNotificationMessageForm.tsx | 16 +- web/src/features/auth/AcceptInvite.tsx | 22 +- web/src/features/auth/ForgotPassword.tsx | 16 +- web/src/features/auth/ResetPassword.tsx | 17 +- .../components/Guides/AddGuideForm.tsx | 92 ++++---- .../Guides/EditGuideAccessDialog.tsx | 18 +- .../components/Guides/EditGuideForm.tsx | 191 ++++++++-------- .../components/Guides/GuidesDashboard.tsx | 34 ++- .../hooks/citizen-guides-hooks.ts | 19 +- .../election-event/hooks/coalition-hooks.ts | 8 +- .../hooks/election-event-hooks.ts | 8 +- .../hooks/observer-guides-hooks.ts | 19 +- .../CreateElectionRoundDialog.tsx | 22 +- .../ElectionRoundDataTableRowActions.tsx | 51 ++--- .../ElectionRoundEdit/ElectionRoundEdit.tsx | 25 +- .../AddMonitoringNgoDialog.tsx | 15 +- .../MonitoringNgosDashboard.tsx | 15 +- .../MonitoringNgosDashboard/queries.ts | 28 +-- web/src/features/election-rounds/hooks.tsx | 15 +- web/src/features/election-rounds/queries.ts | 32 +-- .../components/Dashboard/Dashboard.tsx | 50 +--- .../FormTemplateEdit/FormTemplateEdit.tsx | 48 ++-- .../FormTemplateNew/FormTemplateNew.tsx | 40 ++-- .../FormTemplateTranslationEdit.tsx | 14 +- .../forms/components/Dashboard/Dashboard.tsx | 50 +--- .../Dashboard/EditFormAccessDialog.tsx | 14 +- .../forms/components/FormEdit/FormEdit.tsx | 40 +--- .../forms/components/FormNew/FormNew.tsx | 41 ++-- .../FormTranslationEdit.tsx | 24 +- web/src/features/forms/hooks.ts | 27 +-- .../LocationsImport/LocationsImport.tsx | 17 +- .../EditMonitoringObserver.tsx | 17 +- .../MonitoringObserversImport.tsx | 21 +- .../CreateMonitoringObserverDialog.tsx | 19 +- .../MonitoringObserversList.tsx | 26 +-- .../PushMessageForm/PushMessageForm.tsx | 15 +- .../hooks/monitoring-observers-queries.ts | 28 +-- .../hooks/push-messages-queries.ts | 47 +--- .../hooks/statistics-queries.ts | 6 +- .../ngos/components/CreateNGODialog.tsx | 13 +- .../components/admins/AddNgoAdminDialog.tsx | 23 +- .../features/ngos/hooks/ngo-admin-queries.ts | 41 +--- web/src/features/ngos/hooks/ngos-queries.ts | 51 ++--- .../components/EditObserver/EditObserver.tsx | 2 +- .../observers/hooks/observers-queries.ts | 70 ++---- .../PollingStationsImport.tsx | 21 +- .../CitizenReportDetails.tsx | 23 +- .../ExportDataButton/ExportDataButton.tsx | 14 +- .../FormSubmissionDetails.tsx | 21 +- .../IncidentReportDetails.tsx | 19 +- .../QuickReportDetails/QuickReportDetails.tsx | 13 +- web/src/routes/__root.tsx | 4 +- .../edit.$monitoringObserverId.tsx | 12 +- .../push-messages.$id_.view.tsx | 12 +- web/src/routes/observers/$observerId.tsx | 10 +- web/src/styles/tailwind.css | 4 + 121 files changed, 1519 insertions(+), 1618 deletions(-) create mode 100644 web/src/api/auth/accept-invite.ts create mode 100644 web/src/api/auth/forgot-password.ts create mode 100644 web/src/api/auth/index.ts create mode 100644 web/src/api/auth/login.ts create mode 100644 web/src/api/auth/reset-password.ts create mode 100644 web/src/api/auth/set-password.ts create mode 100644 web/src/api/election-event/delete-citizen-guide.ts create mode 100644 web/src/api/election-event/delete-observer-guide.ts create mode 100644 web/src/api/election-event/get-citizen-guide-details.ts create mode 100644 web/src/api/election-event/get-citizen-guides.ts create mode 100644 web/src/api/election-event/get-coalition-details.ts create mode 100644 web/src/api/election-event/get-election-event.ts create mode 100644 web/src/api/election-event/get-observer-guide-details.ts create mode 100644 web/src/api/election-event/get-observer-guides.ts create mode 100644 web/src/api/election-event/update-citizen-guide.ts create mode 100644 web/src/api/election-event/update-guide-access.ts create mode 100644 web/src/api/election-event/update-observer-guide.ts create mode 100644 web/src/api/election-event/upload-citizen-guide.ts create mode 100644 web/src/api/election-event/upload-observer-guide.ts create mode 100644 web/src/api/election-rounds/add-monitoring-ngo.ts create mode 100644 web/src/api/election-rounds/archive-election-round.ts create mode 100644 web/src/api/election-rounds/create-election-round.ts create mode 100644 web/src/api/election-rounds/delete-monitoring-ngo.ts create mode 100644 web/src/api/election-rounds/get-available-monitoring-ngos.ts create mode 100644 web/src/api/election-rounds/get-election-round-details.ts create mode 100644 web/src/api/election-rounds/get-election-round-statistics.ts create mode 100644 web/src/api/election-rounds/get-election-rounds.ts create mode 100644 web/src/api/election-rounds/get-monitoring-ngos.ts create mode 100644 web/src/api/election-rounds/start-election-round.ts create mode 100644 web/src/api/election-rounds/unarchive-election-round.ts create mode 100644 web/src/api/election-rounds/unstart-election-round.ts create mode 100644 web/src/api/election-rounds/update-election-round.ts create mode 100644 web/src/api/monitoring-observers/create-monitoring-observers.ts create mode 100644 web/src/api/monitoring-observers/export-monitoring-observers.ts create mode 100644 web/src/api/monitoring-observers/get-monitoring-observer-details.ts create mode 100644 web/src/api/monitoring-observers/get-monitoring-observers.ts create mode 100644 web/src/api/monitoring-observers/get-push-message-details.ts create mode 100644 web/src/api/monitoring-observers/get-push-messages.ts create mode 100644 web/src/api/monitoring-observers/get-targeted-monitoring-observers.ts create mode 100644 web/src/api/monitoring-observers/resend-monitoring-observer-invites.ts create mode 100644 web/src/api/monitoring-observers/send-push-notification.ts create mode 100644 web/src/api/monitoring-observers/update-monitoring-observer.ts create mode 100644 web/src/api/observers/create-observer.ts create mode 100644 web/src/api/observers/delete-observer.ts create mode 100644 web/src/api/observers/get-observer-details.ts create mode 100644 web/src/api/observers/get-observers.ts create mode 100644 web/src/api/observers/toggle-observer-status.ts create mode 100644 web/src/api/observers/update-observer.ts create mode 100644 web/src/components/ui/sonner.tsx delete mode 100644 web/src/components/ui/toast.tsx delete mode 100644 web/src/components/ui/toaster.tsx delete mode 100644 web/src/components/ui/use-toast.ts diff --git a/web/package.json b/web/package.json index 9646ebe58..f55ab1055 100644 --- a/web/package.json +++ b/web/package.json @@ -75,7 +75,8 @@ "i18next": "^23.16.8", "i18next-browser-languagedetector": "^8.0.3", "lodash": "^4.17.21", - "lucide-react": "^0.294.0", + "lucide-react": "^0.575.0", + "next-themes": "^0.4.6", "papaparse": "^5.5.2", "qs": "^6.14.0", "react": "^18.3.1", @@ -89,6 +90,7 @@ "react-i18next": "^14.1.3", "react-phone-number-input": "^3.4.11", "react-player": "^2.16.0", + "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "uuid": "^9.0.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 06f39545a..d0ef73f0e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -180,8 +180,11 @@ importers: specifier: ^4.17.21 version: 4.17.21 lucide-react: - specifier: ^0.294.0 - version: 0.294.0(react@18.3.1) + specifier: ^0.575.0 + version: 0.575.0(react@18.3.1) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) papaparse: specifier: ^5.5.2 version: 5.5.2 @@ -221,6 +224,9 @@ importers: react-player: specifier: ^2.16.0 version: 2.16.0(react@18.3.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -1121,6 +1127,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1133,15 +1142,21 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -2645,6 +2660,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3992,10 +4012,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lucide-react@0.294.0: - resolution: {integrity: sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==} + lucide-react@0.575.0: + resolution: {integrity: sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==} peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} @@ -4124,6 +4144,12 @@ packages: engines: {node: '>= 4.4.x'} hasBin: true + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -4740,8 +4766,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + sax@1.4.4: + resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + engines: {node: '>=11.0.0'} saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} @@ -4821,6 +4848,12 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -6126,6 +6159,12 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -6136,19 +6175,28 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.11': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 optional: true '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': + optional: true + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + optional: true + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -7749,6 +7797,9 @@ snapshots: acorn@8.14.0: {} + acorn@8.16.0: + optional: true + agent-base@6.0.2: dependencies: debug: 4.4.0 @@ -9356,7 +9407,7 @@ snapshots: dependencies: yallist: 4.0.0 - lucide-react@0.294.0(react@18.3.1): + lucide-react@0.575.0(react@18.3.1): dependencies: react: 18.3.1 @@ -9481,9 +9532,14 @@ snapshots: needle@3.3.1: dependencies: iconv-lite: 0.6.3 - sax: 1.4.1 + sax: 1.4.4 optional: true + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -10162,7 +10218,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.1: + sax@1.4.4: optional: true saxes@6.0.0: @@ -10252,6 +10308,11 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -10434,8 +10495,8 @@ snapshots: terser@5.29.2: dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true diff --git a/web/src/api/auth/accept-invite.ts b/web/src/api/auth/accept-invite.ts new file mode 100644 index 000000000..7d5296bce --- /dev/null +++ b/web/src/api/auth/accept-invite.ts @@ -0,0 +1,12 @@ +import { noAuthApi } from '@/common/no-auth-api'; + +export interface AcceptInviteRequest { + password: string; + confirmPassword: string; + invitationToken: string; +} + +export async function acceptInvite(payload: AcceptInviteRequest): Promise { + await noAuthApi.post('/auth/accept-invite', payload); +} + diff --git a/web/src/api/auth/forgot-password.ts b/web/src/api/auth/forgot-password.ts new file mode 100644 index 000000000..da051f23f --- /dev/null +++ b/web/src/api/auth/forgot-password.ts @@ -0,0 +1,10 @@ +import { noAuthApi } from '@/common/no-auth-api'; + +export interface ForgotPasswordRequest { + email: string; +} + +export async function requestPasswordReset(payload: ForgotPasswordRequest): Promise { + await noAuthApi.post('auth/forgot-password', payload); +} + diff --git a/web/src/api/auth/index.ts b/web/src/api/auth/index.ts new file mode 100644 index 000000000..a91221639 --- /dev/null +++ b/web/src/api/auth/index.ts @@ -0,0 +1,6 @@ +export * from './login'; +export * from './accept-invite'; +export * from './forgot-password'; +export * from './reset-password'; +export * from './set-password'; + diff --git a/web/src/api/auth/login.ts b/web/src/api/auth/login.ts new file mode 100644 index 000000000..c2a43f291 --- /dev/null +++ b/web/src/api/auth/login.ts @@ -0,0 +1,7 @@ +import { authApi, type ILoginResponse, type LoginDTO } from '@/common/auth-api'; + +export async function login(payload: LoginDTO): Promise { + const response = await authApi.post('auth/login', payload); + return response.data; +} + diff --git a/web/src/api/auth/reset-password.ts b/web/src/api/auth/reset-password.ts new file mode 100644 index 000000000..699a943af --- /dev/null +++ b/web/src/api/auth/reset-password.ts @@ -0,0 +1,12 @@ +import { noAuthApi } from '@/common/no-auth-api'; + +export interface ResetPasswordRequest { + password: string; + token: string; + email: string; +} + +export async function resetPassword(payload: ResetPasswordRequest): Promise { + await noAuthApi.post('auth/reset-password', payload); +} + diff --git a/web/src/api/auth/set-password.ts b/web/src/api/auth/set-password.ts new file mode 100644 index 000000000..c2c7dc937 --- /dev/null +++ b/web/src/api/auth/set-password.ts @@ -0,0 +1,11 @@ +import { authApi } from '@/common/auth-api'; + +export interface SetPasswordRequest { + aspNetUserId: string; + newPassword: string; +} + +export async function setPassword(payload: SetPasswordRequest): Promise { + await authApi.post('auth/set-password', payload); +} + diff --git a/web/src/api/election-event/delete-citizen-guide.ts b/web/src/api/election-event/delete-citizen-guide.ts new file mode 100644 index 000000000..a72861a18 --- /dev/null +++ b/web/src/api/election-event/delete-citizen-guide.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function deleteCitizenGuide(electionRoundId: string, guideId: string) { + return authApi.delete(`/election-rounds/${electionRoundId}/citizen-guides/${guideId}`); +} + diff --git a/web/src/api/election-event/delete-observer-guide.ts b/web/src/api/election-event/delete-observer-guide.ts new file mode 100644 index 000000000..502415956 --- /dev/null +++ b/web/src/api/election-event/delete-observer-guide.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function deleteObserverGuide(electionRoundId: string, guideId: string) { + return authApi.delete(`/election-rounds/${electionRoundId}/observer-guide/${guideId}`); +} + diff --git a/web/src/api/election-event/get-citizen-guide-details.ts b/web/src/api/election-event/get-citizen-guide-details.ts new file mode 100644 index 000000000..da526f6b3 --- /dev/null +++ b/web/src/api/election-event/get-citizen-guide-details.ts @@ -0,0 +1,18 @@ +import { authApi } from '@/common/auth-api'; +import type { GuideModel } from '@/features/election-event/models/guide'; + +export async function getCitizenGuideDetails( + electionRoundId: string, + guideId: string +): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/citizen-guides/${guideId}` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch citizen guide details'); + } + + return response.data; +} + diff --git a/web/src/api/election-event/get-citizen-guides.ts b/web/src/api/election-event/get-citizen-guides.ts new file mode 100644 index 000000000..b307d6eed --- /dev/null +++ b/web/src/api/election-event/get-citizen-guides.ts @@ -0,0 +1,15 @@ +import { authApi } from '@/common/auth-api'; +import type { GuideModel } from '@/features/election-event/models/guide'; + +type CitizenGuidesResponse = { + guides: GuideModel[]; +}; + +export async function getCitizenGuides(electionRoundId: string): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/citizen-guides` + ); + + return response.data; +} + diff --git a/web/src/api/election-event/get-coalition-details.ts b/web/src/api/election-event/get-coalition-details.ts new file mode 100644 index 000000000..76d56382f --- /dev/null +++ b/web/src/api/election-event/get-coalition-details.ts @@ -0,0 +1,11 @@ +import { authApi } from '@/common/auth-api'; +import type { Coalition } from '@/common/types'; + +export async function getCoalitionDetails(electionRoundId: string): Promise { + const response = await authApi.get(`/election-rounds/${electionRoundId}/coalitions:my`); + + return { + ...response.data, + }; +} + diff --git a/web/src/api/election-event/get-election-event.ts b/web/src/api/election-event/get-election-event.ts new file mode 100644 index 000000000..628a9d921 --- /dev/null +++ b/web/src/api/election-event/get-election-event.ts @@ -0,0 +1,11 @@ +import { authApi } from '@/common/auth-api'; +import type { ElectionEvent } from '@/features/election-event/models/election-event'; + +export async function getElectionEvent(electionRoundId: string): Promise { + const response = await authApi.get(`/election-rounds/${electionRoundId}`); + + return { + ...response.data, + }; +} + diff --git a/web/src/api/election-event/get-observer-guide-details.ts b/web/src/api/election-event/get-observer-guide-details.ts new file mode 100644 index 000000000..0f8ba88c0 --- /dev/null +++ b/web/src/api/election-event/get-observer-guide-details.ts @@ -0,0 +1,18 @@ +import { authApi } from '@/common/auth-api'; +import type { GuideModel } from '@/features/election-event/models/guide'; + +export async function getObserverGuideDetails( + electionRoundId: string, + guideId: string +): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/observer-guide/${guideId}` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch observer guide details'); + } + + return response.data; +} + diff --git a/web/src/api/election-event/get-observer-guides.ts b/web/src/api/election-event/get-observer-guides.ts new file mode 100644 index 000000000..fb76182e8 --- /dev/null +++ b/web/src/api/election-event/get-observer-guides.ts @@ -0,0 +1,15 @@ +import { authApi } from '@/common/auth-api'; +import type { GuideModel } from '@/features/election-event/models/guide'; + +type ObserverGuidesResponse = { + guides: GuideModel[]; +}; + +export async function getObserverGuides(electionRoundId: string): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/observer-guide` + ); + + return response.data; +} + diff --git a/web/src/api/election-event/update-citizen-guide.ts b/web/src/api/election-event/update-citizen-guide.ts new file mode 100644 index 000000000..856929f41 --- /dev/null +++ b/web/src/api/election-event/update-citizen-guide.ts @@ -0,0 +1,19 @@ +import { authApi } from '@/common/auth-api'; +import type { GuidePageType, GuideType } from '@/features/election-event/models/guide'; + +export type EditGuidePayload = { + guidePageType: GuidePageType; + guideType: GuideType; + title: string; + websiteUrl?: string; + text?: string; +}; + +export function updateCitizenGuide( + electionRoundId: string, + guideId: string, + guide: EditGuidePayload +) { + return authApi.put(`/election-rounds/${electionRoundId}/citizen-guides/${guideId}`, guide); +} + diff --git a/web/src/api/election-event/update-guide-access.ts b/web/src/api/election-event/update-guide-access.ts new file mode 100644 index 000000000..eb80df884 --- /dev/null +++ b/web/src/api/election-event/update-guide-access.ts @@ -0,0 +1,16 @@ +import { authApi } from '@/common/auth-api'; + +export function updateGuideAccess( + electionRoundId: string, + coalitionId: string, + guideId: string, + ngoMembersIds: string[] +) { + return authApi.put( + `/election-rounds/${electionRoundId}/coalitions/${coalitionId}/guides/${guideId}:access`, + { + ngoMembersIds, + } + ); +} + diff --git a/web/src/api/election-event/update-observer-guide.ts b/web/src/api/election-event/update-observer-guide.ts new file mode 100644 index 000000000..d539b9ec4 --- /dev/null +++ b/web/src/api/election-event/update-observer-guide.ts @@ -0,0 +1,11 @@ +import { authApi } from '@/common/auth-api'; +import type { EditGuidePayload } from './update-citizen-guide'; + +export function updateObserverGuide( + electionRoundId: string, + guideId: string, + guide: EditGuidePayload +) { + return authApi.put(`/election-rounds/${electionRoundId}/observer-guide/${guideId}`, guide); +} + diff --git a/web/src/api/election-event/upload-citizen-guide.ts b/web/src/api/election-event/upload-citizen-guide.ts new file mode 100644 index 000000000..51f586811 --- /dev/null +++ b/web/src/api/election-event/upload-citizen-guide.ts @@ -0,0 +1,10 @@ +import { authApi } from '@/common/auth-api'; + +export function uploadCitizenGuide(electionRoundId: string, formData: FormData) { + return authApi.post(`/election-rounds/${electionRoundId}/citizen-guides`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} + diff --git a/web/src/api/election-event/upload-observer-guide.ts b/web/src/api/election-event/upload-observer-guide.ts new file mode 100644 index 000000000..6aa64a957 --- /dev/null +++ b/web/src/api/election-event/upload-observer-guide.ts @@ -0,0 +1,10 @@ +import { authApi } from '@/common/auth-api'; + +export function uploadObserverGuide(electionRoundId: string, formData: FormData) { + return authApi.post(`/election-rounds/${electionRoundId}/observer-guide`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} + diff --git a/web/src/api/election-rounds/add-monitoring-ngo.ts b/web/src/api/election-rounds/add-monitoring-ngo.ts new file mode 100644 index 000000000..67ea15cd2 --- /dev/null +++ b/web/src/api/election-rounds/add-monitoring-ngo.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function addMonitoringNgo(electionRoundId: string, ngoId: string) { + return authApi.post(`election-rounds/${electionRoundId}/monitoring-ngos`, { ngoId }); +} + diff --git a/web/src/api/election-rounds/archive-election-round.ts b/web/src/api/election-rounds/archive-election-round.ts new file mode 100644 index 000000000..10761f7a4 --- /dev/null +++ b/web/src/api/election-rounds/archive-election-round.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function archiveElectionRound(electionRoundId: string) { + return authApi.post(`/election-rounds/${electionRoundId}:archive`); +} + diff --git a/web/src/api/election-rounds/create-election-round.ts b/web/src/api/election-rounds/create-election-round.ts new file mode 100644 index 000000000..521d4f20c --- /dev/null +++ b/web/src/api/election-rounds/create-election-round.ts @@ -0,0 +1,13 @@ +import { authApi } from '@/common/auth-api'; +import type { ElectionRoundModel } from '@/features/election-rounds/models/types'; +import type { ElectionRoundRequest } from '@/features/election-rounds/components/ElectionRoundForm/ElectionRoundForm'; +import { DateOnlyFormat } from '@/common/formats'; +import { format } from 'date-fns/format'; + +export function createElectionRound(electionRound: ElectionRoundRequest) { + return authApi.post(`/election-rounds`, { + ...electionRound, + startDate: format(electionRound.startDate, DateOnlyFormat), + }); +} + diff --git a/web/src/api/election-rounds/delete-monitoring-ngo.ts b/web/src/api/election-rounds/delete-monitoring-ngo.ts new file mode 100644 index 000000000..98197114d --- /dev/null +++ b/web/src/api/election-rounds/delete-monitoring-ngo.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function deleteMonitoringNgo(electionRoundId: string, ngoId: string) { + return authApi.delete(`election-rounds/${electionRoundId}/monitoring-ngos/${ngoId}`); +} + diff --git a/web/src/api/election-rounds/get-available-monitoring-ngos.ts b/web/src/api/election-rounds/get-available-monitoring-ngos.ts new file mode 100644 index 000000000..4ac42b0ab --- /dev/null +++ b/web/src/api/election-rounds/get-available-monitoring-ngos.ts @@ -0,0 +1,24 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import type { MonitoringNgoModel } from '@/features/election-rounds/models/types'; + +export async function getAvailableMonitoringNgos( + electionRoundId: string, + params: DataTableParameters +): Promise> { + const response = await authApi.get>( + `election-rounds/${electionRoundId}/monitoring-ngos:available`, + { + params: { + ...params.otherParams, + }, + } + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch ngo admins'); + } + + return response.data; +} + diff --git a/web/src/api/election-rounds/get-election-round-details.ts b/web/src/api/election-rounds/get-election-round-details.ts new file mode 100644 index 000000000..90291b2e1 --- /dev/null +++ b/web/src/api/election-rounds/get-election-round-details.ts @@ -0,0 +1,13 @@ +import { authApi } from '@/common/auth-api'; +import type { ElectionRoundModel } from '@/features/election-rounds/models/types'; + +export async function getElectionRoundDetails(electionRoundId: string): Promise { + const response = await authApi.get(`/election-rounds/${electionRoundId}`); + + if (response.status !== 200) { + throw new Error('Failed to fetch election round'); + } + + return response.data; +} + diff --git a/web/src/api/election-rounds/get-election-round-statistics.ts b/web/src/api/election-rounds/get-election-round-statistics.ts new file mode 100644 index 000000000..4b76fb81c --- /dev/null +++ b/web/src/api/election-rounds/get-election-round-statistics.ts @@ -0,0 +1,19 @@ +import { authApi } from '@/common/auth-api'; +import type { DataSources } from '@/common/types'; +import type { MonitoringNgoStats } from '@/features/ngo-admin-dashboard/models/ngo-admin-statistics-models'; + +export async function getElectionRoundStatistics( + electionRoundId: string, + dataSource: DataSources, +): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/statistics?dataSource=${dataSource}`, + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch election round statistics'); + } + + return response.data; +} + diff --git a/web/src/api/election-rounds/get-election-rounds.ts b/web/src/api/election-rounds/get-election-rounds.ts new file mode 100644 index 000000000..12932b21f --- /dev/null +++ b/web/src/api/election-rounds/get-election-rounds.ts @@ -0,0 +1,29 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import type { ElectionRoundModel } from '@/features/election-rounds/models/types'; +import { buildURLSearchParams } from '@/lib/utils'; + +export async function getElectionRounds( + queryParams: DataTableParameters +): Promise> { + const params = { + ...queryParams.otherParams, + PageNumber: String(queryParams.pageNumber), + PageSize: String(queryParams.pageSize), + SortColumnName: queryParams.sortColumnName, + SortOrder: queryParams.sortOrder, + }; + + const searchParams = buildURLSearchParams(params); + + const response = await authApi.get>(`/election-rounds`, { + params: searchParams, + }); + + if (response.status !== 200) { + throw new Error('Failed to fetch election rounds'); + } + + return response.data; +} + diff --git a/web/src/api/election-rounds/get-monitoring-ngos.ts b/web/src/api/election-rounds/get-monitoring-ngos.ts new file mode 100644 index 000000000..35f2a3f05 --- /dev/null +++ b/web/src/api/election-rounds/get-monitoring-ngos.ts @@ -0,0 +1,21 @@ +import { authApi } from '@/common/auth-api'; +import type { MonitoringNgoModel } from '@/features/election-rounds/models/types'; + +type MonitoringNgosPageResponse = { + monitoringNgos: MonitoringNgoModel[]; +}; + +export async function getMonitoringNgos( + electionRoundId: string +): Promise { + const response = await authApi.get( + `election-rounds/${electionRoundId}/monitoring-ngos` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch monitoring NGOs for election round'); + } + + return response.data; +} + diff --git a/web/src/api/election-rounds/start-election-round.ts b/web/src/api/election-rounds/start-election-round.ts new file mode 100644 index 000000000..c239724e4 --- /dev/null +++ b/web/src/api/election-rounds/start-election-round.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function startElectionRound(electionRoundId: string) { + return authApi.post(`/election-rounds/${electionRoundId}:start`); +} + diff --git a/web/src/api/election-rounds/unarchive-election-round.ts b/web/src/api/election-rounds/unarchive-election-round.ts new file mode 100644 index 000000000..2aaa3904b --- /dev/null +++ b/web/src/api/election-rounds/unarchive-election-round.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function unarchiveElectionRound(electionRoundId: string) { + return authApi.post(`/election-rounds/${electionRoundId}:unarchive`); +} + diff --git a/web/src/api/election-rounds/unstart-election-round.ts b/web/src/api/election-rounds/unstart-election-round.ts new file mode 100644 index 000000000..8285d3524 --- /dev/null +++ b/web/src/api/election-rounds/unstart-election-round.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function unstartElectionRound(electionRoundId: string) { + return authApi.post(`/election-rounds/${electionRoundId}:unstart`); +} + diff --git a/web/src/api/election-rounds/update-election-round.ts b/web/src/api/election-rounds/update-election-round.ts new file mode 100644 index 000000000..35933b099 --- /dev/null +++ b/web/src/api/election-rounds/update-election-round.ts @@ -0,0 +1,12 @@ +import { authApi } from '@/common/auth-api'; +import type { ElectionRoundRequest } from '@/features/election-rounds/components/ElectionRoundForm/ElectionRoundForm'; +import { DateOnlyFormat } from '@/common/formats'; +import { format } from 'date-fns/format'; + +export function updateElectionRound(electionRoundId: string, electionRound: ElectionRoundRequest) { + return authApi.put(`/election-rounds/${electionRoundId}`, { + ...electionRound, + startDate: format(electionRound.startDate, DateOnlyFormat), + }); +} + diff --git a/web/src/api/monitoring-observers/create-monitoring-observers.ts b/web/src/api/monitoring-observers/create-monitoring-observers.ts new file mode 100644 index 000000000..af0472e57 --- /dev/null +++ b/web/src/api/monitoring-observers/create-monitoring-observers.ts @@ -0,0 +1,17 @@ +import { authApi } from '@/common/auth-api'; + +export type CreateMonitoringObserverPayload = { + firstName: string; + lastName: string; + email: string; + phoneNumber?: string; + tags: string[]; +}; + +export function createMonitoringObservers( + electionRoundId: string, + observers: CreateMonitoringObserverPayload[] +) { + return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers }); +} + diff --git a/web/src/api/monitoring-observers/export-monitoring-observers.ts b/web/src/api/monitoring-observers/export-monitoring-observers.ts new file mode 100644 index 000000000..ff4331b69 --- /dev/null +++ b/web/src/api/monitoring-observers/export-monitoring-observers.ts @@ -0,0 +1,10 @@ +import { authApi } from '@/common/auth-api'; + +export async function exportMonitoringObservers(electionRoundId: string) { + const res = await authApi.get(`/election-rounds/${electionRoundId}/monitoring-observers:export`, { + responseType: 'blob', + }); + + return res.data as Blob; +} + diff --git a/web/src/api/monitoring-observers/get-monitoring-observer-details.ts b/web/src/api/monitoring-observers/get-monitoring-observer-details.ts new file mode 100644 index 000000000..ed2c8e248 --- /dev/null +++ b/web/src/api/monitoring-observers/get-monitoring-observer-details.ts @@ -0,0 +1,18 @@ +import { authApi } from '@/common/auth-api'; +import type { MonitoringObserver } from '@/features/monitoring-observers/models/monitoring-observer'; + +export async function getMonitoringObserverDetails( + electionRoundId: string, + monitoringObserverId: string +): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/monitoring-observers/${monitoringObserverId}` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch monitoring observer details'); + } + + return response.data; +} + diff --git a/web/src/api/monitoring-observers/get-monitoring-observers.ts b/web/src/api/monitoring-observers/get-monitoring-observers.ts new file mode 100644 index 000000000..0c71900d0 --- /dev/null +++ b/web/src/api/monitoring-observers/get-monitoring-observers.ts @@ -0,0 +1,35 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import { buildURLSearchParams, isQueryFiltered } from '@/lib/utils'; +import type { MonitoringObserver } from '@/features/monitoring-observers/models/monitoring-observer'; + +export async function getMonitoringObservers( + electionRoundId: string, + queryParams: DataTableParameters +): Promise & { isEmpty: boolean }> { + const params = { + ...queryParams.otherParams, + PageNumber: String(queryParams.pageNumber), + PageSize: String(queryParams.pageSize), + SortColumnName: queryParams.sortColumnName, + SortOrder: queryParams.sortOrder, + }; + const searchParams = buildURLSearchParams(params); + + const response = await authApi.get>( + `/election-rounds/${electionRoundId}/monitoring-observers`, + { + params: searchParams, + } + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch monitoring observers'); + } + + return { + ...response.data, + isEmpty: !isQueryFiltered(queryParams.otherParams ?? {}) && response.data.items.length === 0, + }; +} + diff --git a/web/src/api/monitoring-observers/get-push-message-details.ts b/web/src/api/monitoring-observers/get-push-message-details.ts new file mode 100644 index 000000000..cbe65c428 --- /dev/null +++ b/web/src/api/monitoring-observers/get-push-message-details.ts @@ -0,0 +1,18 @@ +import { authApi } from '@/common/auth-api'; +import type { PushMessageDetailedModel } from '@/features/monitoring-observers/models/push-message'; + +export async function getPushMessageDetails( + electionRoundId: string, + pushMessageId: string +): Promise { + const response = await authApi.get( + `/election-rounds/${electionRoundId}/notifications/${pushMessageId}` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch notification details'); + } + + return response.data; +} + diff --git a/web/src/api/monitoring-observers/get-push-messages.ts b/web/src/api/monitoring-observers/get-push-messages.ts new file mode 100644 index 000000000..53a27093c --- /dev/null +++ b/web/src/api/monitoring-observers/get-push-messages.ts @@ -0,0 +1,31 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import { buildURLSearchParams, isQueryFiltered } from '@/lib/utils'; +import type { PushMessageModel } from '@/features/monitoring-observers/models/push-message'; + +export async function getPushMessages( + electionRoundId: string, + queryParams: DataTableParameters +): Promise & { isEmpty: boolean }> { + const params = { + ...queryParams.otherParams, + PageNumber: String(queryParams.pageNumber), + PageSize: String(queryParams.pageSize), + SortColumnName: queryParams.sortColumnName, + SortOrder: queryParams.sortOrder, + }; + const searchParams = buildURLSearchParams(params); + + const response = await authApi.get>( + `/election-rounds/${electionRoundId}/notifications:listSent`, + { + params: searchParams, + } + ); + + return { + ...response.data, + isEmpty: !isQueryFiltered(params) && response.data.items.length === 0, + }; +} + diff --git a/web/src/api/monitoring-observers/get-targeted-monitoring-observers.ts b/web/src/api/monitoring-observers/get-targeted-monitoring-observers.ts new file mode 100644 index 000000000..631f829cf --- /dev/null +++ b/web/src/api/monitoring-observers/get-targeted-monitoring-observers.ts @@ -0,0 +1,32 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import { buildURLSearchParams } from '@/lib/utils'; +import type { TargetedMonitoringObserver } from '@/features/monitoring-observers/models/targeted-monitoring-observer'; + +export async function getTargetedMonitoringObservers( + electionRoundId: string, + queryParams: DataTableParameters +): Promise> { + const params = { + ...queryParams.otherParams, + PageNumber: String(queryParams.pageNumber), + PageSize: String(queryParams.pageSize), + SortColumnName: queryParams.sortColumnName, + SortOrder: queryParams.sortOrder, + }; + const searchParams = buildURLSearchParams(params); + + const response = await authApi.get>( + `election-rounds/${electionRoundId}/notifications:listRecipients`, + { + params: searchParams, + } + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch notification'); + } + + return response.data; +} + diff --git a/web/src/api/monitoring-observers/resend-monitoring-observer-invites.ts b/web/src/api/monitoring-observers/resend-monitoring-observer-invites.ts new file mode 100644 index 000000000..eed9ea385 --- /dev/null +++ b/web/src/api/monitoring-observers/resend-monitoring-observer-invites.ts @@ -0,0 +1,11 @@ +import { authApi } from '@/common/auth-api'; + +export function resendMonitoringObserverInvites( + electionRoundId: string, + monitoringObserverIds: (string | undefined)[] +) { + return authApi.put(`/election-rounds/${electionRoundId}/monitoring-observers:resend-invites`, { + ids: monitoringObserverIds.filter((id) => !!id), + }); +} + diff --git a/web/src/api/monitoring-observers/send-push-notification.ts b/web/src/api/monitoring-observers/send-push-notification.ts new file mode 100644 index 000000000..884a63db9 --- /dev/null +++ b/web/src/api/monitoring-observers/send-push-notification.ts @@ -0,0 +1,14 @@ +import { authApi } from '@/common/auth-api'; +import type { SendPushNotificationRequest } from '@/features/monitoring-observers/models/push-message'; +import type { PushMessageTargetedObserversSearchParams } from '@/features/monitoring-observers/models/search-params'; + +export function sendPushNotification( + electionRoundId: string, + request: SendPushNotificationRequest & { title: string; body: string } +) { + return authApi.post( + `/election-rounds/${electionRoundId}/notifications:send`, + request + ); +} + diff --git a/web/src/api/monitoring-observers/update-monitoring-observer.ts b/web/src/api/monitoring-observers/update-monitoring-observer.ts new file mode 100644 index 000000000..6876713f0 --- /dev/null +++ b/web/src/api/monitoring-observers/update-monitoring-observer.ts @@ -0,0 +1,14 @@ +import { authApi } from '@/common/auth-api'; +import type { UpdateMonitoringObserverRequest } from '@/features/monitoring-observers/models/monitoring-observer'; + +export function updateMonitoringObserver( + electionRoundId: string, + monitoringObserverId: string, + request: UpdateMonitoringObserverRequest +) { + return authApi.post( + `/election-rounds/${electionRoundId}/monitoring-observers/${monitoringObserverId}`, + request + ); +} + diff --git a/web/src/api/observers/create-observer.ts b/web/src/api/observers/create-observer.ts new file mode 100644 index 000000000..c1056974f --- /dev/null +++ b/web/src/api/observers/create-observer.ts @@ -0,0 +1,7 @@ +import { authApi } from '@/common/auth-api'; +import type { AddObserverFormData } from '@/features/observers/models/observer'; + +export function createObserver(values: AddObserverFormData) { + return authApi.post(`/observers`, values); +} + diff --git a/web/src/api/observers/delete-observer.ts b/web/src/api/observers/delete-observer.ts new file mode 100644 index 000000000..d04ba250f --- /dev/null +++ b/web/src/api/observers/delete-observer.ts @@ -0,0 +1,6 @@ +import { authApi } from '@/common/auth-api'; + +export function deleteObserver(observerId: string) { + return authApi.delete(`/observers/${observerId}`); +} + diff --git a/web/src/api/observers/get-observer-details.ts b/web/src/api/observers/get-observer-details.ts new file mode 100644 index 000000000..8947a8647 --- /dev/null +++ b/web/src/api/observers/get-observer-details.ts @@ -0,0 +1,13 @@ +import { authApi } from '@/common/auth-api'; +import type { Observer } from '@/features/observers/models/observer'; + +export async function getObserverDetails(observerId: string): Promise { + const response = await authApi.get(`/observers/${observerId}`); + + if (response.status !== 200) { + throw new Error('Failed to fetch observer details'); + } + + return response.data; +} + diff --git a/web/src/api/observers/get-observers.ts b/web/src/api/observers/get-observers.ts new file mode 100644 index 000000000..d6f59ed2c --- /dev/null +++ b/web/src/api/observers/get-observers.ts @@ -0,0 +1,21 @@ +import { authApi } from '@/common/auth-api'; +import type { DataTableParameters, PageResponse } from '@/common/types'; +import type { Observer } from '@/features/observers/models/observer'; + +export async function getObservers( + queryParams: DataTableParameters +): Promise> { + const response = await authApi.get>('/observers', { + params: { + ...queryParams.otherParams, + status: (queryParams.otherParams as any)?.observerStatus, + }, + }); + + if (response.status !== 200) { + throw new Error('Failed to fetch observers'); + } + + return response.data; +} + diff --git a/web/src/api/observers/toggle-observer-status.ts b/web/src/api/observers/toggle-observer-status.ts new file mode 100644 index 000000000..0b12bf4c8 --- /dev/null +++ b/web/src/api/observers/toggle-observer-status.ts @@ -0,0 +1,8 @@ +import { authApi } from '@/common/auth-api'; + +export function toggleObserverStatus(observerId: string, isObserverActive: boolean) { + const ACTION = isObserverActive ? 'deactivate' : 'activate'; + + return authApi.put(`/observers/${observerId}:${ACTION}`, {}); +} + diff --git a/web/src/api/observers/update-observer.ts b/web/src/api/observers/update-observer.ts new file mode 100644 index 000000000..78638bb64 --- /dev/null +++ b/web/src/api/observers/update-observer.ts @@ -0,0 +1,7 @@ +import { authApi } from '@/common/auth-api'; +import type { EditObserverFormData } from '@/features/observers/models/observer'; + +export function updateObserver(observerId: string, values: EditObserverFormData) { + return authApi.put(`/observers/${observerId}`, values); +} + diff --git a/web/src/components/ElectionEventDescription/ElectionEventDescription.tsx b/web/src/components/ElectionEventDescription/ElectionEventDescription.tsx index 76c8a7f00..8b716ef30 100644 --- a/web/src/components/ElectionEventDescription/ElectionEventDescription.tsx +++ b/web/src/components/ElectionEventDescription/ElectionEventDescription.tsx @@ -11,17 +11,17 @@ import { useUnstartElectionRound, } from '@/features/election-rounds/hooks'; import { PencilIcon } from '@heroicons/react/24/outline'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; import { ArchiveIcon, FileEdit, PlayIcon } from 'lucide-react'; import { useCallback, useContext } from 'react'; import { useTranslation } from 'react-i18next'; +import { toast } from "sonner"; import { electionRoundDetailsQueryOptions } from '../../features/election-event/hooks/election-event-hooks'; import CoalitionDescription from '../CoalitionDescription/CoalitionDescription'; import ElectionRoundStatusBadge from '../ElectionRoundStatusBadge/ElectionRoundStatusBadge'; import { useConfirm } from '../ui/alert-dialog-provider'; import { Button } from '../ui/button'; -import { useToast } from '../ui/use-toast'; -import { useSuspenseQuery } from '@tanstack/react-query'; export default function ElectionEventDescription() { const { t } = useTranslation(); @@ -30,7 +30,6 @@ export default function ElectionEventDescription() { const { userRole } = useContext(AuthContext); - const { toast } = useToast(); const confirm = useConfirm(); const router = useRouter(); @@ -52,15 +51,11 @@ export default function ElectionEventDescription() { onSuccess: () => { router.invalidate(); - toast({ - title: 'Election round archived successfully', - }); + toast('Election round archived successfully'); }, onError: () => - toast({ - title: 'Error archiving election round', + toast.error('Error archiving election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } @@ -78,16 +73,12 @@ export default function ElectionEventDescription() { onSuccess: () => { router.invalidate(); - toast({ - title: 'Election round drafted successfully', - }); + toast('Election round drafted successfully'); }, onError: () => { router.invalidate(); - toast({ - title: 'Error drafting election round', + toast.error('Error drafting election round',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -105,15 +96,11 @@ export default function ElectionEventDescription() { electionRoundId: electionEvent!.id, onSuccess: () => { router.invalidate(); - toast({ - title: 'Election round unarchived successfully', - }); + toast('Election round unarchived successfully'); }, onError: () => - toast({ - title: 'Error unarchiving election round', + toast.error('Error unarchiving election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } @@ -130,15 +117,11 @@ export default function ElectionEventDescription() { electionRoundId: electionEvent!.id, onSuccess: () => { router.invalidate(); - toast({ - title: 'Election round started successfully', - }); + toast('Election round started successfully'); }, onError: () => - toast({ - title: 'Error starting election round', + toast.error('Error starting election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } diff --git a/web/src/components/FormEditor/FormEditor.tsx b/web/src/components/FormEditor/FormEditor.tsx index cf58bf8c9..29d73fef3 100644 --- a/web/src/components/FormEditor/FormEditor.tsx +++ b/web/src/components/FormEditor/FormEditor.tsx @@ -16,12 +16,11 @@ import { isSingleSelectQuestion, isTextQuestion, } from '@/common/guards'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Button } from '@/components/ui/button'; import { LanguageBadge } from '@/components/ui/language-badge'; import { cn, ensureTranslatedStringCorrectness, isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; -import { useBlocker } from '@tanstack/react-router'; -import { FC, useEffect, useMemo, useState } from 'react'; +import { useBlocker, useNavigate } from '@tanstack/react-router'; +import { FC, useMemo, useState } from 'react'; import { FormFull } from '../../features/forms/models'; import { @@ -33,9 +32,18 @@ import { EditTextQuestionType, ZEditQuestionType, } from '@/common/form-requests'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog"; import { FormTemplateFull } from '@/features/form-templates/models'; import EditFormDetails from './FormDetailEditor'; - export const ZEditFormType = z .object({ languageCode: z.string().trim().min(1, 'Language code is required.'), @@ -198,12 +206,12 @@ export type EditFormType = z.infer; interface FormEditorProps { formData?: FormFull | FormTemplateFull; - onSaveForm: (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => void; + onSaveForm: (formData: EditFormType) => Promise; + onNavigateAway: () => void; hasCitizenReportingOption: boolean; } -const FormEditor: FC = ({ hasCitizenReportingOption, formData, onSaveForm }) => { - const confirm = useConfirm(); +const FormEditor: FC = ({ hasCitizenReportingOption, formData, onSaveForm, onNavigateAway }) => { const [navigateAwayAfterSave, setNavigateAwayAfterSave] = useState(false); const editQuestions = useMemo(() => formData?.questions.map((question) => { if (isNumberQuestion(question)) { @@ -369,12 +377,21 @@ const FormEditor: FC = ({ hasCitizenReportingOption, formData, reValidateMode: 'onChange', }); - // Subscribe to isDirty by accessing it in component body - // This ensures React tracks form state changes + const save = async (values: EditFormType) => { + if (form.formState.isDirty) { + await onSaveForm(values); + form.reset(form.getValues(), { keepValues: true, keepDirty: false, keepIsSubmitSuccessful: false }); + } + + if (navigateAwayAfterSave) { + onNavigateAway(); + } + }; + const isDirty = form.formState.isDirty; const { proceed, reset, status } = useBlocker({ - shouldBlockFn: () => isDirty, + shouldBlockFn: () => isDirty && !navigateAwayAfterSave, withResolver: true, }); @@ -384,95 +401,95 @@ const FormEditor: FC = ({ hasCitizenReportingOption, formData, defaultValue: formData?.defaultLanguage, }); - useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset({}, { keepValues: true }); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - - useEffect(() => { - if (status === 'blocked') { - confirm({ - title: `Unsaved Changes Detected`, - body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', - actionButton: 'Leave', - cancelButton: 'Stay', - }).then((confirmed) => { - if (confirmed) { - proceed(); - } else { - reset(); - } - }); - } - }, [status, confirm, proceed, reset]); - return ( -
- onSaveForm(data, navigateAwayAfterSave))}> - - - - Form details - - - Questions - - - - - -
- Form details -
- -
- - - -
-
- - - -
- - {languageCode && ( -
- Form questions - -
- )} -
-
- -
- - - -
-
-
-
-
- - -
-
-
- + <> +
+ save(values))}> + + + + Form details + + + Questions + + + + + +
+ Form details +
+ +
+ + + +
+
+ + + +
+ + {languageCode && ( +
+ Form questions + +
+ )} +
+
+ +
+ + + +
+
+
+
+
+ + +
+
+
+ + + { + if (!open) reset?.() + }}> + + + Are you absolutely sure? + + You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue? + + + + reset?.()}> + Cancel + + proceed?.()}> + Continue + + + + + ); }; diff --git a/web/src/components/FormTranslationEditor/FormTranslationEditor.tsx b/web/src/components/FormTranslationEditor/FormTranslationEditor.tsx index 60c4b4a11..e3f90cf52 100644 --- a/web/src/components/FormTranslationEditor/FormTranslationEditor.tsx +++ b/web/src/components/FormTranslationEditor/FormTranslationEditor.tsx @@ -33,6 +33,16 @@ import { useBlocker } from '@tanstack/react-router'; import { useEffect, useMemo, useState } from 'react'; import { EditFormType, ZEditFormType } from '../FormEditor/FormEditor'; import FormDetailsTranslationEditor from './FormDetailsTranslationEditor'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog"; export interface FormTranslationEditorProps { formData: FormFull | FormTemplateFull; @@ -224,30 +234,15 @@ export default function FormTranslationEditor({ }, [form.formState.isSubmitSuccessful, form.reset]); const isDirty = form.formState.isDirty; + const isSubmitSuccessful = form.formState.isSubmitSuccessful; const { proceed, reset, status } = useBlocker({ - shouldBlockFn: () => isDirty, + shouldBlockFn: () => isDirty && !isSubmitSuccessful, withResolver: true, }); - useEffect(() => { - if (status === 'blocked') { - confirm({ - title: `Unsaved Changes Detected`, - body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', - actionButton: 'Leave', - cancelButton: 'Stay', - }).then((confirmed) => { - if (confirmed) { - proceed(); - } else { - reset(); - } - }); - } - }, [status, confirm, proceed, reset]); - return ( + <>
+ { + if (!open) reset?.() + }}> + + + Are you absolutely sure? + + You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue? + + + + reset?.()}> + Cancel + + proceed?.()}> + Continue + + + + + ); } diff --git a/web/src/components/LocationsDashboard/LocationsDashboard.tsx b/web/src/components/LocationsDashboard/LocationsDashboard.tsx index 4016fb55b..d6ab04312 100644 --- a/web/src/components/LocationsDashboard/LocationsDashboard.tsx +++ b/web/src/components/LocationsDashboard/LocationsDashboard.tsx @@ -19,8 +19,8 @@ import { ArrowUpTrayIcon, FunnelIcon } from '@heroicons/react/24/outline'; import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router'; import { useDebounce } from '@uidotdev/usehooks'; import { useCallback, useContext, useMemo, useState, type ReactElement } from 'react'; +import { toast } from 'sonner'; import { LocationDataTableRowActions } from '../LocationDataTableRowActions/LocationDataTableRowActions'; -import { useToast } from '../ui/use-toast'; import { locationColDefs } from './column-defs'; import { useDeleteLocationMutation, useLocations, useUpdateLocationMutation } from './hooks'; @@ -29,7 +29,6 @@ export default function LocationsDashboard(): ReactElement { const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: electionRound } = useElectionRoundDetails(currentElectionRoundId); const { userRole } = useContext(AuthContext); - const { toast } = useToast(); const router = useRouter(); const { mutate: deleteLocationMutation } = useDeleteLocationMutation(); @@ -44,15 +43,11 @@ export default function LocationsDashboard(): ReactElement { queryClient.invalidateQueries({ queryKey: locationsKeys.all(currentElectionRoundId) }); router.invalidate(); - toast({ - title: 'Success', - description: 'Location deleted', - }); + toast('Location deleted'); }, onError: () => - toast({ - title: 'Error occured when deleting location', - variant: 'destructive', + toast.error('Error occured when deleting location',{ + description: 'Please contact tech support', }), }), [currentElectionRoundId, deleteLocationMutation] @@ -68,15 +63,11 @@ export default function LocationsDashboard(): ReactElement { queryClient.invalidateQueries({ queryKey: locationsKeys.all(currentElectionRoundId) }); router.invalidate(); - toast({ - title: 'Success', - description: 'Location updated', - }); + toast('Location updated'); }, onError: () => - toast({ - title: 'Error occured when updating location', - variant: 'destructive', + toast.error('Error occured when updating location',{ + description: 'Please contact tech support', }), }), [currentElectionRoundId, updateLocationMutation] diff --git a/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts b/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts index 3e16b3a31..b756af394 100644 --- a/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts +++ b/web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts @@ -1,15 +1,11 @@ import { authApi } from '@/common/auth-api'; -import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation'; -import { ProblemDetails } from '@/common/types'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; import { useCallback, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { z } from 'zod'; import { useDialog } from '../ui/use-dialog'; -import { toast } from '../ui/use-toast'; - const passwordSetterSchema = z .object({ newPassword: z.string().min(8, 'Password must be at least 8 characters long'), @@ -70,18 +66,11 @@ export const usePasswordSetterDialog = () => { onSuccess: () => { form.reset({}); internalOnOpenChange(false); - toast({ - title: 'Success', - description: 'Password set', - }); + toast('Password set'); }, - - onError: (error: AxiosError) => { - addFormValidationErrorsFromBackend(form, error); - toast({ - title: 'Error setting password', - description: 'Please contact Platform admins', - variant: 'destructive', + onError: () => { + toast.error('Error setting password',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx b/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx index 9182cb087..8525f46e0 100644 --- a/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx +++ b/web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx @@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { ImportPollingStationRow } from '@/features/polling-stations/PollingStationsImport/PollingStationsImport'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; @@ -12,6 +11,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; export interface CreatePollingStationDialogProps { open: boolean; @@ -34,10 +34,7 @@ function CreatePollingStationDialog({ open, onOpenChange }: CreatePollingStation }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: t('addPollingStation.onSuccess'), - }); + toast(t('addPollingStation.onSuccess')); queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(electionRoundId) }); @@ -45,10 +42,8 @@ function CreatePollingStationDialog({ open, onOpenChange }: CreatePollingStation onOpenChange(false); }, onError: (err) => { - toast({ - title: t('addPollingStation.onError'), + toast.error(t('addPollingStation.onError'),{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx b/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx index 20ecf18d0..b976687b8 100644 --- a/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx +++ b/web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx @@ -20,9 +20,9 @@ import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router' import { useDebounce } from '@uidotdev/usehooks'; import { Plus } from 'lucide-react'; import { useCallback, useContext, useMemo, useState, type ReactElement } from 'react'; +import { toast } from 'sonner'; import { PollingStationDataTableRowActions } from '../PollingStationDataTableRowActions/PollingStationDataTableRowActions'; import { useDialog } from '../ui/use-dialog'; -import { useToast } from '../ui/use-toast'; import { pollingStationColDefs } from './column-defs'; import CreatePollingStationDialog from './CreatePollingStationDialog'; import { useDeletePollingStationMutation, usePollingStations, useUpdatePollingStationMutation } from './hooks'; @@ -32,7 +32,6 @@ export default function PollingStationsDashboard(): ReactElement { const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: electionRound } = useElectionRoundDetails(currentElectionRoundId); const { userRole } = useContext(AuthContext); - const { toast } = useToast(); const router = useRouter(); const { mutate: deletePollingStationMutation } = useDeletePollingStationMutation(); const { mutate: updatePollingStationMutation } = useUpdatePollingStationMutation(); @@ -47,15 +46,11 @@ export default function PollingStationsDashboard(): ReactElement { queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(currentElectionRoundId) }); router.invalidate(); - toast({ - title: 'Success', - description: 'Polling station deleted', - }); + toast('Polling station deleted'); }, onError: () => - toast({ - title: 'Error occured when deleting polling station', - variant: 'destructive', + toast.error('Error occured when deleting polling station',{ + description: 'Please contact tech support', }), }), [currentElectionRoundId, deletePollingStationMutation] @@ -71,15 +66,11 @@ export default function PollingStationsDashboard(): ReactElement { queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(currentElectionRoundId) }); router.invalidate(); - toast({ - title: 'Success', - description: 'Polling station updated', - }); + toast('Polling station updated'); }, onError: () => - toast({ - title: 'Error occured when updating polling station', - variant: 'destructive', + toast.error('Error occured when updating polling station',{ + description: 'Please contact tech support', }), }), [currentElectionRoundId, updatePollingStationMutation] diff --git a/web/src/components/rich-text-editor/styles/index.css b/web/src/components/rich-text-editor/styles/index.css index d32e88ac9..09a46bb90 100644 --- a/web/src/components/rich-text-editor/styles/index.css +++ b/web/src/components/rich-text-editor/styles/index.css @@ -43,6 +43,8 @@ --hljs-name: rgb(37, 151, 146); --hljs-selector-tag: rgb(200, 80, 15); --hljs-number: rgb(61, 160, 103); + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; } .minimal-tiptap-editor .ProseMirror :focus { diff --git a/web/src/components/tag/tag-input.tsx b/web/src/components/tag/tag-input.tsx index 8c4db9f42..65000ba2b 100644 --- a/web/src/components/tag/tag-input.tsx +++ b/web/src/components/tag/tag-input.tsx @@ -3,12 +3,12 @@ import { Input } from '../ui/input'; import { Button } from '../ui/button'; import { type VariantProps } from 'class-variance-authority'; import { CommandInput } from '@/components/ui/command'; -import { toast } from '../ui/use-toast'; import { v4 as uuid } from 'uuid'; import { TagPopover } from './tag-popover'; import { TagList } from './tag-list'; import { tagVariants } from './tag'; import { Autocomplete } from './auto-complete'; +import { toast } from 'sonner'; export enum Delimiter { Comma = ',', @@ -113,10 +113,8 @@ const TagInput = React.forwardRef((props, ref) const inputRef = React.useRef(null); if ((maxTags !== undefined && maxTags < 0) || (props.minTags !== undefined && props.minTags < 0)) { - toast({ - title: 'maxTags and minTags cannot be less than 0', + toast.error('maxTags and minTags cannot be less than 0',{ description: 'Please set maxTags and minTags to a value greater than or equal to 0', - variant: 'destructive', }); return null; } @@ -134,10 +132,8 @@ const TagInput = React.forwardRef((props, ref) // Check if the tag is in the autocomplete options if restrictTagsToAutocomplete is true if (restrictTagsToAutocompleteOptions && !autocompleteOptions?.some((option) => option.text === newTagText)) { - toast({ - title: 'Invalid Tag', + toast.error('Invalid Tag',{ description: 'Please select a tag from the autocomplete options.', - variant: 'destructive', }); return; } @@ -147,20 +143,16 @@ const TagInput = React.forwardRef((props, ref) } if (minLength && newTagText.length < minLength) { - toast({ - title: 'Tag is too short', + toast.error('Tag is too short',{ description: 'Please enter a tag with more characters', - variant: 'destructive', }); return; } // Validate maxLength if (maxLength && newTagText.length > maxLength) { - toast({ - title: 'Tag is too long', + toast.error('Tag is too long',{ description: 'Please enter a tag with less characters', - variant: 'destructive', }); return; } diff --git a/web/src/components/ui/file-uploader.tsx b/web/src/components/ui/file-uploader.tsx index 77d1fa018..a387de958 100644 --- a/web/src/components/ui/file-uploader.tsx +++ b/web/src/components/ui/file-uploader.tsx @@ -5,7 +5,7 @@ import Dropzone, { type DropzoneProps, type FileRejection } from 'react-dropzone import { Button } from '@/components/ui/button'; import { useControllableState } from '@/components/ui/use-controllable-state'; import { cn, formatBytes } from '@/lib/utils'; -import { toast } from './use-toast'; +import { toast } from 'sonner'; interface FileUploaderProps extends React.HTMLAttributes { /** @@ -91,15 +91,12 @@ export function FileUploader(props: FileUploaderProps) { const onDrop = React.useCallback( (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) { - toast({ - title: 'Cannot upload more than 1 file at a time', - variant: 'destructive', - }); + toast.error('Cannot upload more than 1 file at a time'); return; } if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) { - toast({ title: `Cannot upload more than ${maxFileCount} files`, variant: 'destructive' }); + toast.error(`Cannot upload more than ${maxFileCount} files`); return; } @@ -115,7 +112,7 @@ export function FileUploader(props: FileUploaderProps) { if (rejectedFiles.length > 0) { rejectedFiles.forEach(({ file }) => { - toast({ title: `File ${file.name} was rejected`, variant: 'destructive' }); + toast.error(`File ${file.name} was rejected`); }); } }, diff --git a/web/src/components/ui/sonner.tsx b/web/src/components/ui/sonner.tsx new file mode 100644 index 000000000..cf2921c2c --- /dev/null +++ b/web/src/components/ui/sonner.tsx @@ -0,0 +1,52 @@ +"use client" + +import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + + return ( + + ), + info: ( + + ), + warning: ( + + ), + error: ( + + ), + loading: ( + + ), + }} + + style={{ + "--normal-bg": "hsl(var(--popover))", + "--normal-text": "hsl(var(--popover-foreground))", + "--normal-border": "hsl(var(--border))", + "--border-radius": "var(--radius)", + } as React.CSSProperties} + toastOptions={{ + classNames: { + toast: + "group toast bg-popover text-popover-foreground border border-border shadow-xl rounded-lg", + description: "text-muted-foreground", + actionButton: + "bg-primary text-primary-foreground hover:bg-primary/90", + cancelButton: + "bg-muted text-muted-foreground hover:bg-muted/80", + }, + }} + {...props} + /> + ) +} + +export { Toaster } diff --git a/web/src/components/ui/toast.tsx b/web/src/components/ui/toast.tsx deleted file mode 100644 index a82247753..000000000 --- a/web/src/components/ui/toast.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} diff --git a/web/src/components/ui/toaster.tsx b/web/src/components/ui/toaster.tsx deleted file mode 100644 index a2209ba58..000000000 --- a/web/src/components/ui/toaster.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" -import { useToast } from "@/components/ui/use-toast" - -export function Toaster() { - const { toasts } = useToast() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ) - })} - -
- ) -} diff --git a/web/src/components/ui/use-toast.ts b/web/src/components/ui/use-toast.ts deleted file mode 100644 index 16713070d..000000000 --- a/web/src/components/ui/use-toast.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Inspired by react-hot-toast library -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } diff --git a/web/src/context/auth.context.tsx b/web/src/context/auth.context.tsx index 2cfc44b3b..f668575a9 100644 --- a/web/src/context/auth.context.tsx +++ b/web/src/context/auth.context.tsx @@ -1,7 +1,7 @@ import { ILoginResponse, LoginDTO, authApi } from '@/common/auth-api'; -import { useToast } from '@/components/ui/use-toast'; import { parseJwt } from '@/lib/utils'; import { createContext, useEffect, useState } from 'react'; +import { toast } from 'sonner'; export type AuthContextType = { signIn: (user: LoginDTO) => Promise; @@ -30,8 +30,6 @@ const AuthContextProvider = ({ children }: React.PropsWithChildren) => { const [userRole, setUserRole] = useState('Unknown'); const [isPlatformAdmin, setIsPlatformAdmin] = useState(false); - const { toast } = useToast(); - useEffect(() => { const token = localStorage.getItem('token'); setIsAuthenticated(!!token); @@ -57,11 +55,7 @@ const AuthContextProvider = ({ children }: React.PropsWithChildren) => { return true; } catch (error: any) { if (error.response.status === 400) { - toast({ - title: 'Error', - description: 'You have entered an invalid email or password', - variant: 'destructive', - }); + toast.error('You have entered an invalid email or password'); } return false; } diff --git a/web/src/features/CitizenNotifications/CitizenNotificationMessageForm/CitizenNotificationMessageForm.tsx b/web/src/features/CitizenNotifications/CitizenNotificationMessageForm/CitizenNotificationMessageForm.tsx index 864a6b1de..025a7987e 100644 --- a/web/src/features/CitizenNotifications/CitizenNotificationMessageForm/CitizenNotificationMessageForm.tsx +++ b/web/src/features/CitizenNotifications/CitizenNotificationMessageForm/CitizenNotificationMessageForm.tsx @@ -9,13 +9,13 @@ import { z } from 'zod'; import { authApi } from '@/common/auth-api'; import { ElectionRoundStatus, type FunctionComponent } from '@/common/types'; import { RichTextEditor } from '@/components/rich-text-editor'; -import { toast } from '@/components/ui/use-toast'; +import { Button } from '@/components/ui/button'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; +import { toast } from 'sonner'; import { citizenNotificationsKeys } from '../hooks/citizen-notifications-queries'; -import { Button } from '@/components/ui/button'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; const createPushMessageSchema = z.object({ title: z.string().min(1, { message: 'Your message must have a title before sending.' }), @@ -53,14 +53,16 @@ function CitizenNotificationMessageForm(): FunctionComponent { onSuccess: async () => { queryClient.invalidateQueries({ queryKey: citizenNotificationsKeys.all(currentElectionRoundId) }); - toast({ - title: 'Success', - description: 'Notification sent', - }); + toast('Notification sent'); router.invalidate(); await navigate({ to: '/election-event/$tab', params: { tab: 'citizen-notifications' } }); }, + onError: () => { + toast.error('Error sending notification',{ + description: 'Please contact tech support', + }); + }, }); function onSubmit(values: z.infer): void { diff --git a/web/src/features/auth/AcceptInvite.tsx b/web/src/features/auth/AcceptInvite.tsx index 6e00b587f..e3803c642 100644 --- a/web/src/features/auth/AcceptInvite.tsx +++ b/web/src/features/auth/AcceptInvite.tsx @@ -2,17 +2,16 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { noAuthApi } from '@/common/no-auth-api'; +import Logo from '@/components/layout/Header/Logo'; import { Button } from '@/components/ui/button'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { useNavigate } from '@tanstack/react-router'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import Logo from '@/components/layout/Header/Logo'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { PasswordInput } from '@/components/ui/password-input'; import { Route as AcceptInviteRoute } from '@/routes/accept-invite/index'; import { useMutation } from '@tanstack/react-query'; -import { noAuthApi } from '@/common/no-auth-api'; -import { toast } from '@/components/ui/use-toast'; -import { PasswordInput } from '@/components/ui/password-input'; +import { useNavigate } from '@tanstack/react-router'; +import { toast } from 'sonner'; const formSchema = z .object({ @@ -53,19 +52,14 @@ function AcceptInvite() { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'Password was set successfully', - }); + toast('Password was set successfully'); navigate({ to: '/accept-invite/success' }); }, onError: () => { - toast({ - title: 'Error accepting invite', + toast.error('Error accepting invite',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/auth/ForgotPassword.tsx b/web/src/features/auth/ForgotPassword.tsx index ecbe43031..e4cada50c 100644 --- a/web/src/features/auth/ForgotPassword.tsx +++ b/web/src/features/auth/ForgotPassword.tsx @@ -2,14 +2,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { noAuthApi } from '@/common/no-auth-api'; +import Logo from '@/components/layout/Header/Logo'; import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import Logo from '@/components/layout/Header/Logo'; -import { noAuthApi } from '@/common/no-auth-api'; import { useMutation } from '@tanstack/react-query'; -import { toast } from '@/components/ui/use-toast'; +import { toast } from 'sonner'; const formSchema = z.object({ email: z @@ -39,9 +39,11 @@ function ForgotPassword() { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'An email was sent with reset password instructions', + toast('An email was sent with reset password instructions'); + }, + onError: () => { + toast.error('Error sending reset password instructions',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/auth/ResetPassword.tsx b/web/src/features/auth/ResetPassword.tsx index 7ff130a61..3fafffefb 100644 --- a/web/src/features/auth/ResetPassword.tsx +++ b/web/src/features/auth/ResetPassword.tsx @@ -2,19 +2,19 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import Logo from '@/components/layout/Header/Logo'; import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import Logo from '@/components/layout/Header/Logo'; -import { Route as ResetPasswordRoute } from '@/routes/reset-password/index'; -import { useMutation } from '@tanstack/react-query'; import { noAuthApi } from '@/common/no-auth-api'; -import { toast } from '@/components/ui/use-toast'; -import { useNavigate } from '@tanstack/react-router'; import type { FunctionComponent } from '@/common/types'; import { PasswordInput } from '@/components/ui/password-input'; +import { Route as ResetPasswordRoute } from '@/routes/reset-password/index'; +import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { toast } from 'sonner'; interface ResetPasswordRequest { password: string; @@ -63,10 +63,7 @@ function ResetPassword(): FunctionComponent { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'Password was reset successfully', - }); + toast('Password was reset successfully'); navigate({ to: '/reset-password/success' }); }, }); diff --git a/web/src/features/election-event/components/Guides/AddGuideForm.tsx b/web/src/features/election-event/components/Guides/AddGuideForm.tsx index a5d3f0fa8..630188a21 100644 --- a/web/src/features/election-event/components/Guides/AddGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/AddGuideForm.tsx @@ -1,23 +1,36 @@ -import { authApi } from '@/common/auth-api'; +import { uploadCitizenGuide } from '@/api/election-event/upload-citizen-guide'; +import { uploadObserverGuide } from '@/api/election-event/upload-observer-guide'; import { FunctionComponent } from '@/common/types'; import { RichTextEditor } from '@/components/rich-text-editor'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { FileUploader } from '@/components/ui/file-uploader'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; +import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; import { useBlocker } from '@tanstack/react-router'; import { ReactNode, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { z } from 'zod'; import { citizenGuidesKeys } from '../../hooks/citizen-guides-hooks'; import { observerGuidesKeys } from '../../hooks/observer-guides-hooks'; import { GuideModel, GuidePageType, GuideType } from '../../models/guide'; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog"; + export interface AddGuideFormProps { guidePageType: GuidePageType; guideType: GuideType; @@ -118,16 +131,11 @@ export default function AddGuideForm({ formData.append('Text', guide.text!); } - const url = - guidePageType === GuidePageType.Observer - ? `/election-rounds/${electionRoundId}/observer-guide` - : `/election-rounds/${electionRoundId}/citizen-guides`; + if (guidePageType === GuidePageType.Observer) { + return uploadObserverGuide(electionRoundId, formData); + } - return authApi.post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); + return uploadCitizenGuide(electionRoundId, formData); }, onSuccess: ({ data }, { electionRoundId, guidePageType }) => { @@ -138,20 +146,14 @@ export default function AddGuideForm({ } onSuccess?.(data); + form.reset({}, { keepValues: true, keepDirty: false }); - toast({ - title: 'Success', - description: 'Upload was successful', - }); + toast('Upload was successful'); }, - onError: () => { - toast({ - title: 'Error uploading citizen guide', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error uploading citizen guide',{ + description: 'Please contact tech support', }); - onError?.(); }, }); @@ -161,35 +163,13 @@ export default function AddGuideForm({ } const isDirty = form.formState.isDirty; + const isSubmitSuccessful = form.formState.isSubmitSuccessful; const { proceed, reset, status } = useBlocker({ - shouldBlockFn: () => isDirty, + shouldBlockFn: () => isDirty && !isSubmitSuccessful, withResolver: true, }); - useEffect(() => { - if (status === 'blocked') { - confirm({ - title: `Unsaved Changes Detected`, - body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', - actionButton: 'Leave', - cancelButton: 'Stay', - }).then((confirmed) => { - if (confirmed) { - proceed(); - } else { - reset(); - } - }); - } - }, [status, confirm, proceed, reset]); - - useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset({}, { keepValues: true }); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - return ( <>
@@ -272,6 +252,26 @@ export default function AddGuideForm({ {children}
+ { + if (!open) reset?.() + }}> + + + Are you absolutely sure? + + You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue? + + + + reset?.()}> + Cancel + + proceed?.()}> + Continue + + + + ); } diff --git a/web/src/features/election-event/components/Guides/EditGuideAccessDialog.tsx b/web/src/features/election-event/components/Guides/EditGuideAccessDialog.tsx index 9851423a6..1b7d506f9 100644 --- a/web/src/features/election-event/components/Guides/EditGuideAccessDialog.tsx +++ b/web/src/features/election-event/components/Guides/EditGuideAccessDialog.tsx @@ -1,8 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { create } from 'zustand'; -import { useMutation } from '@tanstack/react-query'; -import { useRouter } from '@tanstack/react-router'; -import { authApi } from '@/common/auth-api'; +import { updateGuideAccess } from '@/api/election-event/update-guide-access'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -17,11 +13,15 @@ import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useCoalitionDetails } from '@/features/election-event/hooks/coalition-hooks'; import { queryClient } from '@/main'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from '@tanstack/react-router'; import { sortBy } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { toast } from 'sonner'; +import { create } from 'zustand'; import { observerGuidesKeys } from '../../hooks/observer-guides-hooks'; import { GuideModel } from '../../models/guide'; @@ -88,11 +88,9 @@ function EditGuideAccessDialog() { guideId: string; ngoMembersIds: string[]; }) => - authApi.put(`/election-rounds/${electionRoundId}/coalitions/${coalitionId}/guides/${guideId}:access`, { - ngoMembersIds, - }), + updateGuideAccess(electionRoundId, coalitionId, guideId, ngoMembersIds), onSuccess: async () => { - toast({ title: 'Success', description: 'Access modified' }); + toast('Access modified'); await queryClient.invalidateQueries({ queryKey: observerGuidesKeys.all(currentElectionRoundId) }); router.invalidate(); dismiss(); diff --git a/web/src/features/election-event/components/Guides/EditGuideForm.tsx b/web/src/features/election-event/components/Guides/EditGuideForm.tsx index 00c1fc33c..98724c033 100644 --- a/web/src/features/election-event/components/Guides/EditGuideForm.tsx +++ b/web/src/features/election-event/components/Guides/EditGuideForm.tsx @@ -1,17 +1,27 @@ -import { authApi } from '@/common/auth-api'; +import { updateCitizenGuide } from '@/api/election-event/update-citizen-guide'; +import { updateObserverGuide } from '@/api/election-event/update-observer-guide'; import { FunctionComponent } from '@/common/types'; import { RichTextEditor } from '@/components/rich-text-editor'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { useBlocker } from '@tanstack/react-router'; -import { ReactNode, useEffect } from 'react'; +import { ReactNode } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { z } from 'zod'; import { citizenGuideDetailsQueryOptions, citizenGuidesKeys } from '../../hooks/citizen-guides-hooks'; import { observerGuideDetailsQueryOptions, observerGuidesKeys } from '../../hooks/observer-guides-hooks'; @@ -36,14 +46,12 @@ export default function EditGuideForm({ }: EditGuideFormProps): FunctionComponent { const queryClient = useQueryClient(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); - const confirm = useConfirm(); const { data: guide } = guidePageType === GuidePageType.Observer ? useSuspenseQuery(observerGuideDetailsQueryOptions(currentElectionRoundId, guideId)) : useSuspenseQuery(citizenGuideDetailsQueryOptions(currentElectionRoundId, guideId)); - const { toast } = useToast(); const editGuideFormSchema = z .object({ @@ -107,22 +115,22 @@ export default function EditGuideForm({ const updateTextGuideMutation = useMutation({ mutationFn: ({ electionRoundId, - form, + guide, guideId, }: { electionRoundId: string; - form: EditGuideType; + guide: EditGuideType; guideId: string; }) => { - const url = - form.guidePageType === GuidePageType.Observer - ? `/election-rounds/${electionRoundId}/observer-guide/${guideId}` - : `/election-rounds/${electionRoundId}/citizen-guides/${guideId}`; - return authApi.put(url, form); + if (guide.guidePageType === GuidePageType.Observer) { + return updateObserverGuide(electionRoundId, guideId, guide); + } + + return updateCitizenGuide(electionRoundId, guideId, guide); }, - onSuccess: (_, { electionRoundId, form }) => { - if (form.guidePageType === GuidePageType.Observer) { + onSuccess: (_, { electionRoundId, guide }) => { + if (guide.guidePageType === GuidePageType.Observer) { queryClient.invalidateQueries({ queryKey: observerGuidesKeys.all(electionRoundId) }); } else { queryClient.invalidateQueries({ queryKey: citizenGuidesKeys.all(electionRoundId) }); @@ -130,19 +138,14 @@ export default function EditGuideForm({ onSuccess?.(); - toast({ - title: 'Success', - description: 'Update was successful', - }); + toast('Update was successful'); }, onError: () => { onError?.(); - toast({ - title: 'Error updating guide', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error updating guide', { + description: 'Please contact tech support', }); }, }); @@ -150,93 +153,95 @@ export default function EditGuideForm({ const isDirty = form.formState.isDirty; const { proceed, reset, status } = useBlocker({ - shouldBlockFn: () => isDirty, + shouldBlockFn: () => isDirty, withResolver: true, }); - useEffect(() => { - if (status === 'blocked') { - confirm({ - title: `Unsaved Changes Detected`, - body: 'You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue?', - actionButton: 'Leave', - cancelButton: 'Stay', - }).then((confirmed) => { - if (confirmed) { - proceed(); - } else { - reset(); - } - }); - } - }, [status, confirm, proceed, reset]); - useEffect(() => { - if (form.formState.isSubmitSuccessful) { - form.reset({}, { keepValues: true }); - } - }, [form.formState.isSubmitSuccessful, form.reset]); - - function onSubmit(form: EditGuideType) { - updateTextGuideMutation.mutate({ electionRoundId: currentElectionRoundId, form, guideId }); + async function onSubmit(guide: EditGuideType) { + await updateTextGuideMutation.mutateAsync({ electionRoundId: currentElectionRoundId, guide, guideId }); + form.reset(form.getValues(), { keepValues: true, keepDirty: false, keepIsSubmitSuccessful: false }); } return ( -
- - ( - - - Title * - - - - - - - )} - /> - {guide.guideType === GuideType.Website && ( + <> + + ( - - - Guide url * + + + Title * - + - + )} /> - )} + {guide.guideType === GuideType.Website && ( + ( + + + Guide url * + + + + + + + )} + /> + )} - {guide.guideType === GuideType.Text && ( - ( - - - Text * - - - - - - - )} - /> - )} + {guide.guideType === GuideType.Text && ( + ( + + + Text * + + + + + + + )} + /> + )} - {children} - - + {children} + + + + { + if (!open) reset?.() + }}> + + + Are you absolutely sure? + + You have unsaved changes. If you leave this page, your changes will be lost. Are you sure you want to continue? + + + + reset?.()}> + Cancel + + proceed?.()}> + Continue + + + + + ); } diff --git a/web/src/features/election-event/components/Guides/GuidesDashboard.tsx b/web/src/features/election-event/components/Guides/GuidesDashboard.tsx index ee9dba0fb..e5b8d1f2b 100644 --- a/web/src/features/election-event/components/Guides/GuidesDashboard.tsx +++ b/web/src/features/election-event/components/Guides/GuidesDashboard.tsx @@ -14,10 +14,12 @@ import { DocumentTextIcon, EllipsisVerticalIcon, LinkIcon, PaperClipIcon } from import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; import { format } from 'date-fns'; -import { authApi } from '@/common/auth-api'; +import { deleteCitizenGuide } from '@/api/election-event/delete-citizen-guide'; +import { deleteObserverGuide } from '@/api/election-event/delete-observer-guide'; import { DateTimeFormat } from '@/common/formats'; +import { ElectionRoundStatus } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; -import { toast } from '@/components/ui/use-toast'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import i18n from '@/i18n'; import { queryClient } from '@/main'; @@ -25,15 +27,14 @@ import { useMutation } from '@tanstack/react-query'; import { Link, useNavigate } from '@tanstack/react-router'; import { ChevronDown } from 'lucide-react'; import { useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { citizenGuidesKeys, useCitizenGuides } from '../../hooks/citizen-guides-hooks'; +import { useElectionRoundDetails } from '../../hooks/election-event-hooks'; import { observerGuidesKeys, useObserverGuides } from '../../hooks/observer-guides-hooks'; import { GuideModel, GuidePageType, GuideType } from '../../models/guide'; import AddGuideDialog from './AddGuideDialog'; -import EditGuideDialog from './EditGuideDialog'; -import { useElectionRoundDetails } from '../../hooks/election-event-hooks'; -import { ElectionRoundStatus } from '@/common/types'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import EditGuideAccessDialog, { useEditGuideAccessDialog } from './EditGuideAccessDialog'; +import EditGuideDialog from './EditGuideDialog'; export interface GuidesDashboardProps { guidePageType: GuidePageType; @@ -75,11 +76,11 @@ export default function GuidesDashboard({ guidePageType }: GuidesDashboardProps) electionRoundId: string; guideId: string; }) => { - const url = - guidePageType === GuidePageType.Observer - ? `/election-rounds/${electionRoundId}/observer-guide/${guideId}` - : `/election-rounds/${electionRoundId}/citizen-guides/${guideId}`; - return authApi.delete(url); + if (guidePageType === GuidePageType.Observer) { + return deleteObserverGuide(electionRoundId, guideId); + } + + return deleteCitizenGuide(electionRoundId, guideId); }, onSuccess: (_, { electionRoundId, guidePageType }) => { @@ -89,17 +90,12 @@ export default function GuidesDashboard({ guidePageType }: GuidesDashboardProps) queryClient.invalidateQueries({ queryKey: citizenGuidesKeys.all(electionRoundId) }); - toast({ - title: 'Success', - description: 'Delete was successful', - }); + toast('Delete was successful'); }, onError: () => { - toast({ - title: 'Error deleting guide', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error deleting guide',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/election-event/hooks/citizen-guides-hooks.ts b/web/src/features/election-event/hooks/citizen-guides-hooks.ts index 347b39011..34ef2a128 100644 --- a/web/src/features/election-event/hooks/citizen-guides-hooks.ts +++ b/web/src/features/election-event/hooks/citizen-guides-hooks.ts @@ -1,4 +1,5 @@ -import { authApi } from '@/common/auth-api'; +import { getCitizenGuideDetails } from '@/api/election-event/get-citizen-guide-details'; +import { getCitizenGuides } from '@/api/election-event/get-citizen-guides'; import { queryOptions, useQuery, UseQueryResult } from '@tanstack/react-query'; import { queryClient } from '@/main'; @@ -17,15 +18,13 @@ export function useCitizenGuides(electionRoundId: string): CitizenGuideResult { return useQuery({ queryKey: citizenGuidesKeys.all(electionRoundId), queryFn: async () => { - const response = await authApi.get<{ guides: GuideModel[] }>( - `/election-rounds/${electionRoundId}/citizen-guides` - ); + const response = await getCitizenGuides(electionRoundId); - response.data.guides.forEach((guide) => { + response.guides.forEach((guide) => { queryClient.setQueryData(citizenGuidesKeys.details(electionRoundId, guide.id), guide); }); - return response.data.guides; + return response.guides; }, staleTime: STALE_TIME, enabled: !!electionRoundId, @@ -36,13 +35,7 @@ export const citizenGuideDetailsQueryOptions = (electionRoundId: string, guideId return queryOptions({ queryKey: citizenGuidesKeys.details(electionRoundId, guideId), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}/citizen-guides/${guideId}`); - - if (response.status !== 200) { - throw new Error('Failed to fetch citizen guide details'); - } - - return response.data; + return getCitizenGuideDetails(electionRoundId, guideId); }, enabled: !!electionRoundId, staleTime: STALE_TIME, diff --git a/web/src/features/election-event/hooks/coalition-hooks.ts b/web/src/features/election-event/hooks/coalition-hooks.ts index a3941649a..067c85027 100644 --- a/web/src/features/election-event/hooks/coalition-hooks.ts +++ b/web/src/features/election-event/hooks/coalition-hooks.ts @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { getCoalitionDetails } from '@/api/election-event/get-coalition-details'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { Coalition } from './../../../common/types'; @@ -14,11 +14,7 @@ export function useCoalitionDetails(electionRoundId: string): CoalitionDetailRes return useQuery({ queryKey: coalitionKeys.all(electionRoundId!), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}/coalitions:my`); - - return { - ...response.data, - }; + return getCoalitionDetails(electionRoundId); }, enabled: !!electionRoundId, staleTime: STALE_TIME, diff --git a/web/src/features/election-event/hooks/election-event-hooks.ts b/web/src/features/election-event/hooks/election-event-hooks.ts index 92c600aba..737e93b05 100644 --- a/web/src/features/election-event/hooks/election-event-hooks.ts +++ b/web/src/features/election-event/hooks/election-event-hooks.ts @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { getElectionEvent } from '@/api/election-event/get-election-event'; import { queryOptions, useQuery, UseQueryResult } from '@tanstack/react-query'; import { electionRoundKeys } from '@/features/election-rounds/queries'; @@ -12,11 +12,7 @@ export const electionRoundDetailsQueryOptions = (electionRoundId: string) => { return queryOptions({ queryKey: electionRoundKeys.detail(electionRoundId!), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}`); - - return { - ...response.data, - }; + return getElectionEvent(electionRoundId); }, staleTime: STALE_TIME, enabled: !!electionRoundId, diff --git a/web/src/features/election-event/hooks/observer-guides-hooks.ts b/web/src/features/election-event/hooks/observer-guides-hooks.ts index 35f7cb3a0..6f6f20cf4 100644 --- a/web/src/features/election-event/hooks/observer-guides-hooks.ts +++ b/web/src/features/election-event/hooks/observer-guides-hooks.ts @@ -1,4 +1,5 @@ -import { authApi } from '@/common/auth-api'; +import { getObserverGuideDetails } from '@/api/election-event/get-observer-guide-details'; +import { getObserverGuides } from '@/api/election-event/get-observer-guides'; import { queryOptions, useQuery, UseQueryResult } from '@tanstack/react-query'; import { GuideModel } from '../models/guide'; @@ -18,15 +19,13 @@ export function useObserverGuides(electionRoundId: string): ObserverGuideResult return useQuery({ queryKey: observerGuidesKeys.all(electionRoundId), queryFn: async () => { - const response = await authApi.get<{ guides: GuideModel[] }>( - `/election-rounds/${electionRoundId}/observer-guide` - ); + const response = await getObserverGuides(electionRoundId); - response.data.guides.forEach((guide) => { + response.guides.forEach((guide) => { queryClient.setQueryData(observerGuidesKeys.details(electionRoundId, guide.id), guide); }); - return response.data.guides; + return response.guides; }, staleTime: STALE_TIME, enabled: !!electionRoundId, @@ -37,13 +36,7 @@ export const observerGuideDetailsQueryOptions = (electionRoundId: string, guideI return queryOptions({ queryKey: observerGuidesKeys.details(electionRoundId, guideId), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}/observer-guide/${guideId}`); - - if (response.status !== 200) { - throw new Error('Failed to fetch observer guide details'); - } - - return response.data; + return getObserverGuideDetails(electionRoundId, guideId); }, enabled: !!electionRoundId, staleTime: STALE_TIME, diff --git a/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx b/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx index 03e49d353..849115adc 100644 --- a/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx +++ b/web/src/features/election-rounds/components/CreateElectionRoundDialog/CreateElectionRoundDialog.tsx @@ -1,13 +1,11 @@ -import { authApi } from '@/common/auth-api'; -import { DateOnlyFormat } from '@/common/formats'; +import { createElectionRound } from '@/api/election-rounds/create-election-round'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, 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 { useMutation } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; -import { format } from 'date-fns/format'; +import { toast } from 'sonner'; import { ElectionRoundModel } from '../../models/types'; import { electionRoundKeys } from '../../queries'; import ElectionRoundForm, { ElectionRoundRequest } from '../ElectionRoundForm/ElectionRoundForm'; @@ -22,10 +20,7 @@ function CreateElectionRoundDialog({ open, onOpenChange }: ElectionRoundFormProp const createElectionRoundMutation = useMutation({ mutationFn: (electionRound: ElectionRoundRequest) => { - return authApi.post(`/election-rounds`, { - ...electionRound, - startDate: format(electionRound.startDate, DateOnlyFormat), - }); + return createElectionRound(electionRound); }, onSuccess: async ({ data }) => { @@ -37,17 +32,12 @@ function CreateElectionRoundDialog({ open, onOpenChange }: ElectionRoundFormProp }); onOpenChange(false); - toast({ - title: 'Success', - description: 'Election round created', - }); + toast('Election round created'); }, onError: () => { - toast({ - title: 'Error creating election round', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error creating election round',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/election-rounds/components/Dashboard/ElectionRoundDataTableRowActions.tsx b/web/src/features/election-rounds/components/Dashboard/ElectionRoundDataTableRowActions.tsx index 901dd536d..d7e874fc9 100644 --- a/web/src/features/election-rounds/components/Dashboard/ElectionRoundDataTableRowActions.tsx +++ b/web/src/features/election-rounds/components/Dashboard/ElectionRoundDataTableRowActions.tsx @@ -10,12 +10,12 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { ElectionRoundStatus } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Dialog } from '@/components/ui/dialog'; -import { ArchiveIcon, MoreHorizontal, Pencil, PlayIcon, Trash2, FileEdit, Eye } from 'lucide-react'; -import { ElectionRoundModel } from '../../models/types'; import { Route } from '@/routes/election-rounds'; -import { ElectionRoundStatus } from '@/common/types'; +import { ArchiveIcon, Eye, FileEdit, MoreHorizontal, Pencil, PlayIcon, Trash2 } from 'lucide-react'; +import { toast } from 'sonner'; import { useArchiveElectionRound, useDeleteElectionRound, @@ -23,7 +23,7 @@ import { useUnarchiveElectionRound, useUnstartElectionRound, } from '../../hooks'; -import { useToast } from '@/components/ui/use-toast'; +import { ElectionRoundModel } from '../../models/types'; interface ElectionRoundDataTableRowActionsProps { electionRound: ElectionRoundModel; @@ -32,7 +32,6 @@ interface ElectionRoundDataTableRowActionsProps { export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoundDataTableRowActionsProps) { const confirm = useConfirm(); const navigate = Route.useNavigate(); - const { toast } = useToast(); const { mutate: deleteElectionRound } = useDeleteElectionRound(); const { mutate: unstartElectionRound } = useUnstartElectionRound(); @@ -50,14 +49,10 @@ export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoun deleteElectionRound({ electionRoundId: electionRound.id, onSuccess: () => - toast({ - title: 'Election round deleted successfully', - }), + toast('Election round deleted successfully'), onError: () => - toast({ - title: 'Error deleting election round', + toast.error('Error deleting election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } @@ -74,14 +69,10 @@ export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoun archiveElectionRound({ electionRoundId: electionRound.id, onSuccess: () => - toast({ - title: 'Election round archived successfully', - }), + toast('Election round archived successfully'), onError: () => - toast({ - title: 'Error archiving election round', + toast.error('Error archiving election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } @@ -98,15 +89,11 @@ export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoun unstartElectionRound({ electionRoundId: electionRound.id, onSuccess: () => - toast({ - title: 'Election round drafted successfully', - }), + toast('Election round drafted successfully'), onError: () => - toast({ - title: 'Error drafting election round', + toast.error('Error drafting election round',{ description: 'Please contact tech support', - variant: 'destructive', - }), + }) }); } }, [electionRound, confirm]); @@ -122,14 +109,10 @@ export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoun unarchiveElectionRound({ electionRoundId: electionRound.id, onSuccess: () => - toast({ - title: 'Election round unarchived successfully', - }), + toast('Election round unarchived successfully'), onError: () => - toast({ - title: 'Error unarchiving election round', + toast.error('Error unarchiving election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } @@ -146,14 +129,10 @@ export function ElectionRoundDataTableRowActions({ electionRound }: ElectionRoun startElectionRound({ electionRoundId: electionRound.id, onSuccess: () => - toast({ - title: 'Election round started successfully', - }), + toast( 'Election round started successfully'), onError: () => - toast({ - title: 'Error starting election round', + toast.error('Error starting election round',{ description: 'Please contact tech support', - variant: 'destructive', }), }); } diff --git a/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx b/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx index b478e3396..524d40f45 100644 --- a/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx +++ b/web/src/features/election-rounds/components/ElectionRoundEdit/ElectionRoundEdit.tsx @@ -1,19 +1,17 @@ -import { authApi } from '@/common/auth-api'; -import { DateOnlyFormat } from '@/common/formats'; +import { updateElectionRound } from '@/api/election-rounds/update-election-round'; +import { ElectionRoundStatus } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; -import { toast } from '@/components/ui/use-toast'; import { queryClient } from '@/main'; import { Route } from '@/routes/election-rounds/$electionRoundId/edit'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; -import { format } from 'date-fns/format'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { electionRoundDetailsQueryOptions, electionRoundKeys } from '../../queries'; import ElectionRoundForm, { ElectionRoundRequest } from '../ElectionRoundForm/ElectionRoundForm'; -import { ElectionRoundStatus } from '@/common/types'; function ElectionRoundEdit() { const router = useRouter(); const { electionRoundId } = Route.useParams(); @@ -28,10 +26,7 @@ function ElectionRoundEdit() { electionRoundId: string; electionRound: ElectionRoundRequest; }) => { - return authApi.put(`/election-rounds/${electionRoundId}`, { - ...electionRound, - startDate: format(electionRound.startDate, DateOnlyFormat), - }); + return updateElectionRound(electionRoundId, electionRound); }, onSuccess: async (_, { electionRoundId }) => { @@ -42,17 +37,11 @@ function ElectionRoundEdit() { params: { electionRoundId }, }); - toast({ - title: 'Success', - description: 'Election round created', - }); + toast('Election round created'); }, - onError: () => { - toast({ - title: 'Error creating election round', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error creating election round',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx b/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx index 160a62fc0..dad288e6a 100644 --- a/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx @@ -1,15 +1,15 @@ -import { authApi } from '@/common/auth-api'; +import { addMonitoringNgo } from '@/api/election-rounds/add-monitoring-ngo'; import { Button } from '@/components/ui/button'; import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; -import { toast } from '@/components/ui/use-toast'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ColumnDef } from '@tanstack/react-table'; import { useDebounce } from '@uidotdev/usehooks'; import { Plus } from 'lucide-react'; import { ChangeEvent, useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { MonitoringNgoModel } from '../../models/types'; import { monitoringNgoKeys, useAvailableMonitoringNgos } from './queries'; @@ -41,18 +41,19 @@ function AddMonitoringNgoDialog({ open, onOpenChange, electionRoundId }: AddMoni const queryClient = useQueryClient(); const addMonitoringNgoMutation = useMutation({ mutationFn: async (ngoId: string) => { - return await authApi.post(`election-rounds/${electionRoundId}/monitoring-ngos`, { ngoId }); + return await addMonitoringNgo(electionRoundId, ngoId); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: monitoringNgoKeys.all(electionRoundId) }); onOpenChange(false); - toast({ - title: 'Success', - description: 'Added monitoring NGO', + toast('Added monitoring NGO'); + }, + onError: () => { + toast.error('Error adding monitoring NGO',{ + description: 'Please contact tech support', }); }, - //TODO Add error handling }); const monitoringNgosColDefs: ColumnDef[] = [ diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx b/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx index 65f5b2990..785df9d9b 100644 --- a/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { deleteMonitoringNgo } from '@/api/election-rounds/delete-monitoring-ngo'; import type { FunctionComponent } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Button, buttonVariants } from '@/components/ui/button'; @@ -13,7 +13,6 @@ import { import { Separator } from '@/components/ui/separator'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { useDialog } from '@/components/ui/use-dialog'; -import { toast } from '@/components/ui/use-toast'; import { NgoStatusBadge } from '@/features/ngos/components/NgoStatusBadges'; import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -21,6 +20,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { MonitoringNgoModel } from '../../models/types'; import AddMonitoringNgoDialog from './AddMonitoringNgoDialog'; import { monitoringNgoKeys, useMonitoringNgos } from './queries'; @@ -90,17 +90,18 @@ function MonitoringNgosDashboard({ electionRoundId }: MonitoringNgosDashboardPro const confirm = useConfirm(); const deleteMonitoringNgoMutation = useMutation({ mutationFn: async (ngoId: string) => { - return await authApi.delete(`election-rounds/${electionRoundId}/monitoring-ngos/${ngoId}`); + return await deleteMonitoringNgo(electionRoundId, ngoId); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: monitoringNgoKeys.all(electionRoundId) }); - toast({ - title: 'Success', - description: 'Removed monitoring NGO', + toast('Monitoring NGO removed'); + }, + onError: () => { + toast.error('Error occured when removing monitoring NGO',{ + description: 'Please contact tech support', }); }, - //TODO Add error handling }); const monitoringNgosColDefs: ColumnDef[] = [ diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts b/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts index e6088020f..5764065df 100644 --- a/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts @@ -1,4 +1,5 @@ -import { authApi } from '@/common/auth-api'; +import { getAvailableMonitoringNgos } from '@/api/election-rounds/get-available-monitoring-ngos'; +import { getMonitoringNgos } from '@/api/election-rounds/get-monitoring-ngos'; import { DataTableParameters, ElectionRoundStatus, PageResponse } from '@/common/types'; import { UseQueryResult, useQuery } from '@tanstack/react-query'; import { MonitoringNgoModel } from '../../models/types'; @@ -25,15 +26,7 @@ export function useMonitoringNgos(electionRoundId: string): UseQueryResult { - const response = await authApi.get( - `election-rounds/${electionRoundId}/monitoring-ngos` - ); - - if (response.status !== 200) { - throw new Error('Failed to fetch monitoring NGOs for election round'); - } - - return response.data; + return getMonitoringNgos(electionRoundId); }, enabled: !!electionRoundId, @@ -48,20 +41,7 @@ export function useAvailableMonitoringNgos( return useQuery({ queryKey: monitoringNgoKeys.availableForMonitoring(electionRoundId, p), queryFn: async () => { - const response = await authApi.get>( - `election-rounds/${electionRoundId}/monitoring-ngos:available`, - { - params: { - ...p.otherParams, - }, - } - ); - - if (response.status !== 200) { - throw new Error('Failed to fetch ngo admins'); - } - - return response.data; + return getAvailableMonitoringNgos(electionRoundId, p); }, staleTime: STALE_TIME, }); diff --git a/web/src/features/election-rounds/hooks.tsx b/web/src/features/election-rounds/hooks.tsx index c5b2ae51d..8030d9d59 100644 --- a/web/src/features/election-rounds/hooks.tsx +++ b/web/src/features/election-rounds/hooks.tsx @@ -1,4 +1,7 @@ -import { authApi } from '@/common/auth-api'; +import { archiveElectionRound } from '@/api/election-rounds/archive-election-round'; +import { startElectionRound } from '@/api/election-rounds/start-election-round'; +import { unarchiveElectionRound } from '@/api/election-rounds/unarchive-election-round'; +import { unstartElectionRound } from '@/api/election-rounds/unstart-election-round'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; import { electionRoundKeys } from './queries'; @@ -6,7 +9,7 @@ import { electionRoundKeys } from './queries'; export function useArchiveElectionRound() { return useMutation({ mutationFn: ({ electionRoundId }: { electionRoundId: string; onSuccess?: () => void; onError?: () => void }) => { - return authApi.post(`/election-rounds/${electionRoundId}:archive`); + return archiveElectionRound(electionRoundId); }, onSuccess: (_, { onSuccess }) => { @@ -21,7 +24,7 @@ export function useArchiveElectionRound() { export function useUnarchiveElectionRound() { return useMutation({ mutationFn: ({ electionRoundId }: { electionRoundId: string; onSuccess?: () => void; onError?: () => void }) => { - return authApi.post(`/election-rounds/${electionRoundId}:unarchive`); + return unarchiveElectionRound(electionRoundId); }, onSuccess: (_, { onSuccess }) => { @@ -36,7 +39,7 @@ export function useUnarchiveElectionRound() { export function useStartElectionRound() { return useMutation({ mutationFn: ({ electionRoundId }: { electionRoundId: string; onSuccess?: () => void; onError?: () => void }) => { - return authApi.post(`/election-rounds/${electionRoundId}:start`); + return startElectionRound(electionRoundId); }, onSuccess: (_, { onSuccess }) => { @@ -51,7 +54,7 @@ export function useStartElectionRound() { export function useUnstartElectionRound() { return useMutation({ mutationFn: ({ electionRoundId }: { electionRoundId: string; onSuccess?: () => void; onError?: () => void }) => { - return authApi.post(`/election-rounds/${electionRoundId}:unstart`); + return unstartElectionRound(electionRoundId); }, onSuccess: (_, { onSuccess }) => { @@ -66,7 +69,7 @@ export function useUnstartElectionRound() { export function useDeleteElectionRound() { return useMutation({ mutationFn: ({ electionRoundId }: { electionRoundId: string; onSuccess?: () => void; onError?: () => void }) => { - return authApi.post(`/election-rounds/${electionRoundId}:unstart`); + return unstartElectionRound(electionRoundId); }, onSuccess: (_, { onSuccess }) => { diff --git a/web/src/features/election-rounds/queries.ts b/web/src/features/election-rounds/queries.ts index be3fd4c07..96b85a160 100644 --- a/web/src/features/election-rounds/queries.ts +++ b/web/src/features/election-rounds/queries.ts @@ -1,8 +1,8 @@ -import { authApi } from '@/common/auth-api'; +import { getElectionRoundDetails } from '@/api/election-rounds/get-election-round-details'; +import { getElectionRounds } from '@/api/election-rounds/get-election-rounds'; import { DataTableParameters, ElectionRoundStatus, PageResponse } from '@/common/types'; import { UseQueryResult, queryOptions, useQuery } from '@tanstack/react-query'; import { ElectionRoundModel } from './models/types'; -import { buildURLSearchParams } from '@/lib/utils'; const STALE_TIME = 1000 * 60 * 15; // fifteen minutes export const electionRoundKeys = { @@ -25,25 +25,7 @@ export function useElectionRounds( return useQuery({ queryKey: electionRoundKeys.list(queryParams), queryFn: async () => { - const params = { - ...queryParams.otherParams, - PageNumber: String(queryParams.pageNumber), - PageSize: String(queryParams.pageSize), - SortColumnName: queryParams.sortColumnName, - SortOrder: queryParams.sortOrder, - }; - - const searchParams = buildURLSearchParams(params); - - const response = await authApi.get>(`/election-rounds`, { - params: searchParams, - }); - - if (response.status !== 200) { - throw new Error('Failed to fetch election rounds'); - } - - return response.data; + return getElectionRounds(queryParams); }, staleTime: STALE_TIME, }); @@ -53,13 +35,7 @@ export const electionRoundDetailsQueryOptions = (electionRoundId: string) => { return queryOptions({ queryKey: electionRoundKeys.detail(electionRoundId), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}`); - - if (response.status !== 200) { - throw new Error('Failed to fetch election round'); - } - - return response.data; + return getElectionRoundDetails(electionRoundId); }, enabled: !!electionRoundId, }); diff --git a/web/src/features/form-templates/components/Dashboard/Dashboard.tsx b/web/src/features/form-templates/components/Dashboard/Dashboard.tsx index 1996816d7..179747493 100644 --- a/web/src/features/form-templates/components/Dashboard/Dashboard.tsx +++ b/web/src/features/form-templates/components/Dashboard/Dashboard.tsx @@ -21,7 +21,6 @@ import { Input } from '@/components/ui/input'; import { LanguageBadge } from '@/components/ui/language-badge'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { toast } from '@/components/ui/use-toast'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { useLanguages } from '@/hooks/languages'; import i18n from '@/i18n'; @@ -42,6 +41,7 @@ import { useDebounce } from '@uidotdev/usehooks'; import { format } from 'date-fns'; import { difference } from 'lodash'; import { useCallback, useMemo, useState, type ReactElement } from 'react'; +import { toast } from 'sonner'; import { formTemlatesKeys, useFormTemplates } from '../../queries'; import { FormTemplateFilters } from './FormTemplateFilters'; @@ -350,10 +350,7 @@ export default function FormTemplatesDashboard(): ReactElement { }, onSuccess: (_data) => { - toast({ - title: 'Success', - description: 'Translation deleted', - }); + toast('Translation deleted'); queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all() }); router.invalidate(); @@ -389,11 +386,7 @@ export default function FormTemplatesDashboard(): ReactElement { }, onSuccess: async (_, { newLanguageCodes, originalLanguageCodes }) => { - toast({ - title: 'Success', - description: 'Translations added', - }); - + toast('Translations added'); addTranslationsDialog.dismiss(); const addedLanguages = difference(newLanguageCodes, originalLanguageCodes); @@ -424,11 +417,7 @@ export default function FormTemplatesDashboard(): ReactElement { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'Form template published', - }); - + toast('Form template published'); queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all() }); router.invalidate(); }, @@ -436,18 +425,14 @@ export default function FormTemplatesDashboard(): ReactElement { onError: (error) => { // @ts-ignore if (error.response.status === 400) { - toast({ - title: 'Error publishing form template', + toast.error('Error publishing form template',{ description: 'You are missing translations. Please translate all fields and try again', - variant: 'destructive', }); return; } - toast({ - title: 'Error publishing form template', + toast.error('Error publishing form template',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -458,20 +443,15 @@ export default function FormTemplatesDashboard(): ReactElement { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'Form template obsoleted', - }); + toast('Form template obsoleted'); queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all() }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error obsoleting form', + toast.error('Error obsoleting form',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -482,20 +462,15 @@ export default function FormTemplatesDashboard(): ReactElement { }, onSuccess: () => { - toast({ - title: 'Success', - description: 'Form template duplicated', - }); + toast('Form template duplicated'); queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all() }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error cloning form template', + toast.error('Error cloning form template',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -505,10 +480,7 @@ export default function FormTemplatesDashboard(): ReactElement { return authApi.delete(`/form-templates/${formTemplateId}`); }, onSuccess: async () => { - toast({ - title: 'Success', - description: 'Form template deleted', - }); + toast('Form template deleted'); await queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all() }); router.invalidate(); }, diff --git a/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx b/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx index b4534fc46..aa456c7a6 100644 --- a/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx +++ b/web/src/features/form-templates/components/FormTemplateEdit/FormTemplateEdit.tsx @@ -1,19 +1,18 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; +import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; -import { useToast } from '@/components/ui/use-toast'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/form-templates/$formTemplateId_.edit'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { UpdateFormTemplateRequest } from '../../models'; import { formTemlatesKeys, formTemplateDetailsQueryOptions } from '../../queries'; -import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; function FormTemplateEdit() { const { formTemplateId } = Route.useParams(); @@ -21,53 +20,29 @@ function FormTemplateEdit() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); - const confirm = useConfirm(); const updateFormTemplateMutation = useMutation({ - mutationFn: ({ - formTemplate, - }: { - formTemplate: UpdateFormTemplateRequest; - shouldNavigateAwayAfterSubmit: boolean; - }) => { + mutationFn: (formTemplate: UpdateFormTemplateRequest) => { return authApi.put(`/form-templates/${formTemplate.id}`, { ...formTemplate, }); }, - onSuccess: async (_, { shouldNavigateAwayAfterSubmit }) => { - toast({ - title: 'Success', - description: 'Form template updated successfully', - }); - + onSuccess: async () => { + toast('Form template updated successfully'); await queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all(), type: 'all' }); router.invalidate(); - - if (shouldNavigateAwayAfterSubmit) { - if ( - await confirm({ - title: 'Changes made to form template in base language', - body: 'Please note that changes have been made to the form in base language, which can impact the translation(s). All new questions or response options which you have added have been copied to translations but in the base language. Access each translation of the form and manually translate each of the changes.', - }) - ) { - await navigate({ to: '/form-templates' }); - } - } }, onError: () => { - toast({ - title: 'Error saving form template', + toast.error('Error saving form template',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); const saveFormTemplate = useCallback( - (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + async (formData: EditFormType) => { const updatedForm: UpdateFormTemplateRequest = { id: formTemplateId, code: formData.code, @@ -80,7 +55,7 @@ function FormTemplateEdit() { questions: formData.questions.map(mapToQuestionRequest), }; - updateFormTemplateMutation.mutate({ formTemplate: updatedForm, shouldNavigateAwayAfterSubmit }); + await updateFormTemplateMutation.mutateAsync(updatedForm); }, [formTemplateId] ); @@ -97,9 +72,12 @@ function FormTemplateEdit() { title={`${formTemplate.code} - ${formTemplate.name[formTemplate.defaultLanguage] ?? ''}`}> - saveFormTemplate(formData, shouldNavigateAwayAfterSubmit) + onSaveForm={(formData: EditFormType) => + saveFormTemplate(formData) } + onNavigateAway={() => { + navigate({ to: '/form-templates' }); + }} hasCitizenReportingOption={true} /> diff --git a/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx b/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx index d99d90613..cf0fd3bc6 100644 --- a/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx +++ b/web/src/features/form-templates/components/FormTemplateNew/FormTemplateNew.tsx @@ -3,27 +3,21 @@ import { mapToQuestionRequest } from '@/common/form-requests'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useToast } from '@/components/ui/use-toast'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { FormTemplateFull, NewFormTemplateRequest } from '../../models'; import { formTemlatesKeys } from '../../queries'; function FormTemplateNew() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); const newFormTemplateMutation = useMutation({ - mutationFn: async ({ - formTemplate, - }: { - shouldNavigateAwayAfterSubmit: boolean; - formTemplate: NewFormTemplateRequest; - }) => { + mutationFn: async (formTemplate: NewFormTemplateRequest) => { return await authApi .post(`/form-templates`, { ...formTemplate, @@ -31,31 +25,22 @@ function FormTemplateNew() { .then((response) => response.data); }, - onSuccess: ({ id }, { shouldNavigateAwayAfterSubmit }) => { - toast({ - title: 'Success', - description: 'Form template created successfully', - }); - + onSuccess: ({ id }) => { + toast('Form template created successfully'); queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all(), type: 'all' }); router.invalidate(); - if (shouldNavigateAwayAfterSubmit) { - navigate({ to: '/form-templates' }); - } else { - navigate({ to: `/form-templates/$formTemplateId/edit`, params: { formTemplateId: id } }); - } + + return navigate({ to: `/form-templates/$formTemplateId/edit`, params: { formTemplateId: id } }); }, onError: () => { - toast({ - title: 'Error creating form template', + toast.error('Error creating form template',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); - const saveFormTemplate = useCallback((formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + const saveFormTemplate = useCallback(async (formData: EditFormType) => { const newFormTemplate: NewFormTemplateRequest = { code: formData.code, name: formData.name, @@ -67,15 +52,16 @@ function FormTemplateNew() { questions: formData.questions.map(mapToQuestionRequest), }; - newFormTemplateMutation.mutate({ formTemplate: newFormTemplate, shouldNavigateAwayAfterSubmit }); + await newFormTemplateMutation.mutateAsync(newFormTemplate); }, []); return ( } title={`Create new form template`}> - saveFormTemplate(formData, shouldNavigateAwayAfterSubmit) - } + onSaveForm={saveFormTemplate} + onNavigateAway={() => { + navigate({ to: '/form-templates' }); + }} hasCitizenReportingOption={true} /> diff --git a/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx b/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx index 7370589b2..078fcc4b1 100644 --- a/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx +++ b/web/src/features/form-templates/components/FormTemplateTranslationEdit/FormTemplateTranslationEdit.tsx @@ -1,20 +1,20 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; import { EditFormType } from '@/components/FormEditor/FormEditor'; +import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; import FormTranslationEditor from '@/components/FormTranslationEditor/FormTranslationEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; -import { useToast } from '@/components/ui/use-toast'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/form-templates/$formTemplateId_.edit-translation.$languageCode'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { UpdateFormTemplateRequest } from '../../models'; import { formTemlatesKeys, formTemplateDetailsQueryOptions } from '../../queries'; -import { FormTemplateDetailsBreadcrumbs } from '@/components/FormTemplateDetailsBreadcrumbs/FormTemplateDetailsBreadcrumbs'; function FormTemplateTranslationEdit() { const { formTemplateId, languageCode } = Route.useParams(); @@ -22,7 +22,6 @@ function FormTemplateTranslationEdit() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); const confirm = useConfirm(); const updateFormTemplateMutation = useMutation({ @@ -38,10 +37,7 @@ function FormTemplateTranslationEdit() { }, onSuccess: async (_, { shouldNavigateAwayAfterSubmit }) => { - toast({ - title: 'Success', - description: 'Form template updated successfully', - }); + toast('Form template updated successfully'); await queryClient.invalidateQueries({ queryKey: formTemlatesKeys.all(), type: 'all' }); router.invalidate(); @@ -59,10 +55,8 @@ function FormTemplateTranslationEdit() { }, onError: () => { - toast({ - title: 'Error saving form template', + toast.error('Error saving form template',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/forms/components/Dashboard/Dashboard.tsx b/web/src/features/forms/components/Dashboard/Dashboard.tsx index 3bc4430bd..0d77e1a50 100644 --- a/web/src/features/forms/components/Dashboard/Dashboard.tsx +++ b/web/src/features/forms/components/Dashboard/Dashboard.tsx @@ -21,7 +21,6 @@ import { Input } from '@/components/ui/input'; import { LanguageBadge } from '@/components/ui/language-badge'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -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 { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; @@ -45,6 +44,7 @@ import { useDebounce } from '@uidotdev/usehooks'; import { format } from 'date-fns'; import { difference } from 'lodash'; import { useMemo, useState, type ReactElement } from 'react'; +import { toast } from 'sonner'; import { NgoFormBase } from '../../models'; import { formsKeys, useForms } from '../../queries'; import EditFormAccessDialog, { useEditFormAccessDialog } from './EditFormAccessDialog'; @@ -598,11 +598,7 @@ export default function FormsDashboard(): ReactElement { }, onSuccess: (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Translation deleted', - }); - + toast('Translation deleted'); queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId) }); router.invalidate(); }, @@ -637,11 +633,7 @@ export default function FormsDashboard(): ReactElement { }, onSuccess: async (_, { electionRoundId, newLanguages, originalLanguages }) => { - toast({ - title: 'Success', - description: 'Translations added', - }); - + toast('Translations added'); addTranslationsDialog.dismiss(); const addedLanguages = difference(newLanguages, originalLanguages); @@ -673,10 +665,7 @@ export default function FormsDashboard(): ReactElement { }, onSuccess: (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form published', - }); + toast( 'Form published'); queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId) }); router.invalidate(); @@ -685,18 +674,14 @@ export default function FormsDashboard(): ReactElement { onError: (error) => { // @ts-ignore if (error.response.status === 400) { - toast({ - title: 'Error publishing form', + toast.error('Error publishing form',{ description: 'You are missing translations. Please translate all fields and try again', - variant: 'destructive', }); return; } - toast({ - title: 'Error publishing form', + toast.error('Error publishing form',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -707,20 +692,15 @@ export default function FormsDashboard(): ReactElement { }, onSuccess: (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form obsoleted', - }); + toast( 'Form obsoleted'); queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId) }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error obsoleting form', + toast.error('Error obsoleting form',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -731,20 +711,15 @@ export default function FormsDashboard(): ReactElement { }, onSuccess: (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form duplicated', - }); + toast('Form duplicated'); queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId) }); router.invalidate(); }, onError: (error) => { - toast({ - title: 'Error cloning form', + toast.error('Error cloning form',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -754,10 +729,7 @@ export default function FormsDashboard(): ReactElement { return authApi.delete(`/election-rounds/${electionRoundId}/forms/${formId}`); }, onSuccess: async (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form deleted', - }); + toast('Form deleted'); await queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId) }); router.invalidate(); }, diff --git a/web/src/features/forms/components/Dashboard/EditFormAccessDialog.tsx b/web/src/features/forms/components/Dashboard/EditFormAccessDialog.tsx index 59f192deb..55c37eb51 100644 --- a/web/src/features/forms/components/Dashboard/EditFormAccessDialog.tsx +++ b/web/src/features/forms/components/Dashboard/EditFormAccessDialog.tsx @@ -1,7 +1,3 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { create } from 'zustand'; -import { useMutation } from '@tanstack/react-query'; -import { useRouter } from '@tanstack/react-router'; import { authApi } from '@/common/auth-api'; import { Button } from '@/components/ui/button'; import { @@ -17,13 +13,17 @@ import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useCoalitionDetails } from '@/features/election-event/hooks/coalition-hooks'; import { queryClient } from '@/main'; -import { formsKeys } from '../../queries'; +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from '@tanstack/react-router'; import { sortBy } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { toast } from 'sonner'; +import { create } from 'zustand'; import { NgoFormBase } from '../../models'; +import { formsKeys } from '../../queries'; export interface EditFormAccessDialogProps { isOpen: boolean; @@ -92,7 +92,7 @@ function EditFormAccessDialog() { ngoMembersIds, }), onSuccess: async () => { - toast({ title: 'Success', description: 'Access modified' }); + toast('Access modified'); await queryClient.invalidateQueries({ queryKey: formsKeys.all(currentElectionRoundId) }); router.invalidate(); dismiss(); diff --git a/web/src/features/forms/components/FormEdit/FormEdit.tsx b/web/src/features/forms/components/FormEdit/FormEdit.tsx index fe782be2e..d77f37a67 100644 --- a/web/src/features/forms/components/FormEdit/FormEdit.tsx +++ b/web/src/features/forms/components/FormEdit/FormEdit.tsx @@ -4,8 +4,6 @@ import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/Form import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; -import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { isNilOrWhitespace } from '@/lib/utils'; @@ -14,6 +12,7 @@ import { Route } from '@/routes/forms/$formId_.edit'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { UpdateFormRequest } from '../../models'; import { formDetailsQueryOptions, formsKeys } from '../../queries'; @@ -26,8 +25,6 @@ function FormEdit() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); - const confirm = useConfirm(); const updateFormMutation = useMutation({ mutationFn: ({ @@ -42,27 +39,22 @@ function FormEdit() { }); }, - onSuccess: async (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form updated successfully', - }); + onSuccess: (_, { electionRoundId }) => { + toast('Form updated successfully'); - await queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); + queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error saving form template', + toast.error('Error saving form', { description: 'Please contact tech support', - variant: 'destructive', }); }, }); const saveForm = useCallback( - async (formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + async (formData: EditFormType) => { const updatedForm: UpdateFormRequest = { id: formId, code: formData.code, @@ -79,19 +71,8 @@ function FormEdit() { electionRoundId: currentElectionRoundId, form: updatedForm, }); - - if (shouldNavigateAwayAfterSubmit) { - if ( - await confirm({ - title: 'Changes made to form template in base language', - body: 'Please note that changes have been made to the form in base language, which can impact the translation(s). All new questions or response options which you have added have been copied to translations but in the base language. Access each translation of the form and manually translate each of the changes.', - }) - ) { - await navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); - } - } }, - [updateFormMutation, formId, currentElectionRoundId] + [currentElectionRoundId, formId, updateFormMutation] ); return ( @@ -101,9 +82,10 @@ function FormEdit() { title={`${form.code} - ${form.name[form.defaultLanguage] ?? ''}`}> - saveForm(formData, shouldNavigateAwayAfterSubmit) - } + onSaveForm={saveForm} + onNavigateAway={() => { + void navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); + }} hasCitizenReportingOption={electionEvent?.isMonitoringNgoForCitizenReporting ?? false} /> diff --git a/web/src/features/forms/components/FormNew/FormNew.tsx b/web/src/features/forms/components/FormNew/FormNew.tsx index 0ea6173ba..b9fe83cc8 100644 --- a/web/src/features/forms/components/FormNew/FormNew.tsx +++ b/web/src/features/forms/components/FormNew/FormNew.tsx @@ -3,21 +3,20 @@ import { mapToQuestionRequest } from '@/common/form-requests'; import FormEditor, { EditFormType } from '@/components/FormEditor/FormEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useToast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { useMutation } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from "sonner"; import { FormFull, NewFormRequest } from '../../models'; import { formsKeys } from '../../queries'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; function FormNew() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: electionEvent } = useElectionRoundDetails(currentElectionRoundId); @@ -28,7 +27,6 @@ function FormNew() { }: { electionRoundId: string; form: NewFormRequest; - shouldNavigateAwayAfterSubmit: boolean; }) => { return authApi .post(`/election-rounds/${electionRoundId}/forms`, { @@ -37,36 +35,24 @@ function FormNew() { .then((response) => response.data); }, - onSuccess: ({ id }, { electionRoundId, shouldNavigateAwayAfterSubmit }) => { - toast({ - title: 'Success', - description: 'Form created successfully', - }); + onSuccess: ({ id }, { electionRoundId }) => { + toast('Form created successfully'); queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); router.invalidate(); - if (shouldNavigateAwayAfterSubmit) { - navigate({ - to: '/election-event/$tab', - params: { tab: 'observer-forms' }, - }); - } else { - navigate({ to: '/forms/$formId/edit', params: { formId: id } }); - } + navigate({ to: `/election-rounds/${electionRoundId}/forms/$formId/edit`, params: { formId: id } }); }, onError: () => { - toast({ - title: 'Error creating form', + toast.error('Error creating form',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); const saveForm = useCallback( - (electionRoundId: string, formData: EditFormType, shouldNavigateAwayAfterSubmit: boolean) => { + async (formData: EditFormType) => { const newForm: NewFormRequest = { code: formData.code, name: formData.name, @@ -78,9 +64,9 @@ function FormNew() { questions: formData.questions.map(mapToQuestionRequest), }; - newFormMutation.mutate({ electionRoundId, form: newForm, shouldNavigateAwayAfterSubmit }); + await newFormMutation.mutateAsync({ electionRoundId: currentElectionRoundId, form: newForm }); }, - [] + [currentElectionRoundId, newFormMutation] ); return ( @@ -89,9 +75,10 @@ function FormNew() { title={`Create new form template`} breadcrumbs={<>}> - saveForm(currentElectionRoundId, formData, shouldNavigateAwayAfterSubmit) - } + onSaveForm={saveForm} + onNavigateAway={() => { + void navigate({ to: '/election-event/$tab', params: { tab: 'observer-forms' } }); + }} hasCitizenReportingOption={electionEvent?.isMonitoringNgoForCitizenReporting ?? false} /> diff --git a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx index 67cf65880..33c95e79c 100644 --- a/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx +++ b/web/src/features/forms/components/FormTranslationEdit/FormTranslationEdit.tsx @@ -1,22 +1,21 @@ import { authApi } from '@/common/auth-api'; import { mapToQuestionRequest } from '@/common/form-requests'; +import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; import { EditFormType } from '@/components/FormEditor/FormEditor'; import FormTranslationEditor from '@/components/FormTranslationEditor/FormTranslationEditor'; import Layout from '@/components/layout/Layout'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; -import { useToast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; import { isNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; +import { Route } from '@/routes/forms/$formId_.edit-translation.$languageCode'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useCallback } from 'react'; +import { toast } from 'sonner'; import { UpdateFormRequest } from '../../models'; -import { formsKeys, formDetailsQueryOptions } from '../../queries'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; -import { Route } from '@/routes/forms/$formId_.edit-translation.$languageCode'; -import { FormDetailsBreadcrumbs } from '@/components/FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; +import { formDetailsQueryOptions, formsKeys } from '../../queries'; function FormTranslationEdit() { const { formId, languageCode } = Route.useParams(); @@ -27,7 +26,6 @@ function FormTranslationEdit() { const navigate = useNavigate(); const router = useRouter(); - const { toast } = useToast(); const updateFormMutation = useMutation({ mutationFn: ({ @@ -43,20 +41,14 @@ function FormTranslationEdit() { }, onSuccess: async (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Form updated successfully', - }); - + toast('Form updated successfully'); await queryClient.invalidateQueries({ queryKey: formsKeys.all(electionRoundId), type: 'all' }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error saving form ', + toast.error('Error saving form ',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/forms/hooks.ts b/web/src/features/forms/hooks.ts index eae504ee7..0bb60b14c 100644 --- a/web/src/features/forms/hooks.ts +++ b/web/src/features/forms/hooks.ts @@ -1,8 +1,8 @@ import { authApi } from '@/common/auth-api'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; +import { toast } from 'sonner'; import { create } from 'zustand'; import { FormFull } from './models'; import { formsKeys } from './queries'; @@ -43,21 +43,15 @@ export const useCreateFormFromTemplate = () => { }); }, onSuccess: (response) => { - toast({ - title: 'Success', - description: 'Form created from template', - }); + toast('Form created from template'); queryClient.invalidateQueries({ queryKey: formsKeys.all(currentElectionRoundId) }); navigate({ to: '/forms/$formId/edit', params: { formId: response.data.id } }); }, - - onError: (err) => - toast({ - title: 'Error creating form from template', + onError: () => { + toast.error('Error creating form from template',{ description: 'Please contact tech support', - variant: 'destructive', - }), - + }); + }, onSettled: () => { if (isOpen) dismiss(); }, @@ -102,19 +96,14 @@ export const useCreateFormFromForm = () => { }); }, onSuccess: (response) => { - toast({ - title: 'Success', - description: 'Form created from template', - }); + toast('Form created from template'); queryClient.invalidateQueries({ queryKey: formsKeys.all(currentElectionRoundId) }); navigate({ to: '/forms/$formId/edit', params: { formId: response.data.id } }); }, onError: (err) => - toast({ - title: 'Error creating form from template', + toast.error('Error creating form from template',{ description: 'Please contact tech support', - variant: 'destructive', }), onSettled: () => { diff --git a/web/src/features/locations/LocationsImport/LocationsImport.tsx b/web/src/features/locations/LocationsImport/LocationsImport.tsx index 46fe7e59f..56bf1a0e9 100644 --- a/web/src/features/locations/LocationsImport/LocationsImport.tsx +++ b/web/src/features/locations/LocationsImport/LocationsImport.tsx @@ -9,7 +9,6 @@ import { z, ZodIssue } from 'zod'; import { authApi } from '@/common/auth-api'; import { Button } from '@/components/ui/button'; -import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { locationsKeys } from '@/hooks/locations-levels'; import { downloadImportExample, TemplateType } from '@/lib/utils'; @@ -19,6 +18,7 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { LoaderIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { ImportedLocationsDataTable } from './ImportedLocationsDataTable'; export type ImportLocationRow = z.infer & { errors?: ZodIssue[] }; @@ -28,7 +28,6 @@ export function LocationsImport(): FunctionComponent { const { t } = useTranslation('translation', { keyPrefix: 'electionEvent.locations.addLocation' }); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const navigate = useNavigate(); - const { toast } = useToast(); function deleteLocation(location: ImportLocationRow) { setLocations((prev) => [...prev.filter((obs) => obs.id !== location.id)]); @@ -55,19 +54,13 @@ export function LocationsImport(): FunctionComponent { }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: t('onSuccess'), - }); - + toast(t('onSuccess')); queryClient.invalidateQueries({ queryKey: locationsKeys.all(electionRoundId) }); navigate({ to: '/election-rounds/$electionRoundId', params: { electionRoundId } }); }, onError: () => { - toast({ - title: t('onError'), + toast.error(t('onError'),{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -115,10 +108,8 @@ export function LocationsImport(): FunctionComponent { async complete(results) { if (results.errors.length) { // Optionally show an error message to the user. - toast({ - title: 'Parsing errors', + toast.error('Parsing errors',{ description: 'Please check the file and try again', - variant: 'destructive', }); } diff --git a/web/src/features/monitoring-observers/components/EditMonitoringObserver/EditMonitoringObserver.tsx b/web/src/features/monitoring-observers/components/EditMonitoringObserver/EditMonitoringObserver.tsx index ebc7b8057..15832cf95 100644 --- a/web/src/features/monitoring-observers/components/EditMonitoringObserver/EditMonitoringObserver.tsx +++ b/web/src/features/monitoring-observers/components/EditMonitoringObserver/EditMonitoringObserver.tsx @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { updateMonitoringObserver } from '@/api/monitoring-observers/update-monitoring-observer'; import Layout from '@/components/layout/Layout'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -7,7 +7,6 @@ import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import TagsSelectFormField from '@/components/ui/tag-selector'; -import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useMonitoringObserversTags } from '@/hooks/tags-queries'; import { Route, monitoringObserverDetailsQueryOptions } from '@/routes/monitoring-observers/edit.$monitoringObserverId'; @@ -15,6 +14,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { z } from 'zod'; import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; import { targetedObserversKeys } from '../../hooks/push-messages-queries'; @@ -35,8 +35,6 @@ export default function EditObserver() { const { data: availableTags } = useMonitoringObserversTags(currentElectionRoundId); - const { toast } = useToast(); - const editObserverFormSchema = z.object({ status: z.string(), tags: z.any(), @@ -77,16 +75,11 @@ export default function EditObserver() { electionRoundId: string; request: UpdateMonitoringObserverRequest; }) => { - return authApi.post( - `/election-rounds/${electionRoundId}/monitoring-observers/${monitoringObserver.id}`, - request - ); + return updateMonitoringObserver(electionRoundId, monitoringObserver.id, request); }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Observer successfully updated', - }); + toast('Observer successfully updated'); + router.invalidate(); queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) }); queryClient.invalidateQueries({ queryKey: targetedObserversKeys.all(electionRoundId) }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx index 08bc6b6ff..aeda2af9d 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversImport/MonitoringObserversImport.tsx @@ -5,21 +5,21 @@ import { FileUploader } from '@/components/ui/file-uploader'; import { Separator } from '@/components/ui/separator'; import Papa from 'papaparse'; import { useMemo, useState } from 'react'; -import { ZodIssue, ZodIssueCode, z } from 'zod'; +import { z, ZodIssue, ZodIssueCode } from 'zod'; -import { authApi } from '@/common/auth-api'; +import { createMonitoringObservers } from '@/api/monitoring-observers/create-monitoring-observers'; import { Button } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { downloadImportExample, TemplateType } from '@/lib/utils'; import { queryClient } from '@/main'; import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { LoaderIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; import { ImportedObserversDataTable } from './ImportedObserversDataTable'; -import { downloadImportExample, TemplateType } from '@/lib/utils'; export const importObserversSchema = z.object({ firstName: z @@ -37,6 +37,7 @@ export const importObserversSchema = z.object({ message: 'Invalid email format.', }), phoneNumber: z.string().max(256, { message: 'Phone number cannot exceed 256 characters.' }).optional(), + tags: z.array(z.string()).default([]), }); export type ImportObserverRow = z.infer & { id: string; errors: ZodIssue[] }; @@ -68,23 +69,17 @@ export function MonitoringObserversImport(): FunctionComponent { const { mutate, isPending } = useMutation({ mutationFn: ({ electionRoundId, observers }: { electionRoundId: string; observers: ImportObserverRow[] }) => { - return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers }); + return createMonitoringObservers(electionRoundId, observers); }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: t('onSuccess'), - }); - + toast(t('onSuccess')); queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) }); navigate({ to: '/monitoring-observers' }); }, onError: () => { - toast({ - title: t('onError'), + toast.error(t('onError'), { description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx index 2490914d4..e1fcca1b6 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/CreateMonitoringObserverDialog.tsx @@ -1,16 +1,16 @@ -import { authApi } from '@/common/auth-api'; +import { createMonitoringObservers } from '@/api/monitoring-observers/create-monitoring-observers'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import TagsSelectFormField from '@/components/ui/tag-selector'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useMonitoringObserversTags } from '@/hooks/tags-queries'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { z } from 'zod'; import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries'; import { targetedObserversKeys } from '../../hooks/push-messages-queries'; @@ -30,7 +30,7 @@ function CreateMonitoringObserverDialog({ open, onOpenChange }: CreateMonitoring lastName: z.string(), email: z.string().email(), phoneNumber: z.string().optional().catch(''), - tags: z.any(), + tags: z.array(z.string()).default([]), }); type ObserverFormData = z.infer; @@ -46,14 +46,11 @@ function CreateMonitoringObserverDialog({ open, onOpenChange }: CreateMonitoring const newObserverMutation = useMutation({ mutationFn: ({ electionRoundId, values }: { electionRoundId: string; values: ObserverFormData }) => { - return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers: [values] }); + return createMonitoringObservers(electionRoundId, [values]); }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: t('onSuccess'), - }); + toast(t('onSuccess')); queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) }); queryClient.invalidateQueries({ queryKey: targetedObserversKeys.all(electionRoundId) }); @@ -62,10 +59,8 @@ function CreateMonitoringObserverDialog({ open, onOpenChange }: CreateMonitoring onOpenChange(false); }, onError: () => { - toast({ - title: t('onError'), - description: 'Please contact tech support', - variant: 'destructive', + toast.error(t('onError'), { + description: 'Please contact tech support' }); }, }); diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index 3fd92d2f7..25b25b732 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -1,4 +1,5 @@ -import { authApi } from '@/common/auth-api'; +import { exportMonitoringObservers as exportMonitoringObserversApi } from '@/api/monitoring-observers/export-monitoring-observers'; +import { resendMonitoringObserverInvites } from '@/api/monitoring-observers/resend-monitoring-observer-invites'; import TableTagList from '@/components/table-tag-list/TableTagList'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -29,7 +30,6 @@ 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'; @@ -40,6 +40,7 @@ import { Route } from '@/routes/monitoring-observers/$tab'; import { useDebounce } from '@uidotdev/usehooks'; import { format } from 'date-fns'; import { Plus } from 'lucide-react'; +import { toast } from 'sonner'; import { MonitoringObserversListFilters } from '../../filtering/MonitoringObserversListFilters'; import { monitoringObserversKeys, useMonitoringObservers } from '../../hooks/monitoring-observers-queries'; import { MonitoringObserver, MonitoringObserverStatus } from '../../models/monitoring-observer'; @@ -190,9 +191,7 @@ function MonitoringObserversList() { electionRoundId: string; monitoringObserverId: string | undefined; }) => { - return authApi.put(`/election-rounds/${electionRoundId}/monitoring-observers:resend-invites`, { - ids: [monitoringObserverId].filter((id) => !!id), - }); + return resendMonitoringObserverInvites(electionRoundId, [monitoringObserverId]); }, onSuccess: (_, { electionRoundId }) => { @@ -201,17 +200,11 @@ function MonitoringObserversList() { setMonitoringObserverId(undefined); - toast({ - title: 'Success', - description: 'Invitation sent', - }); + toast('Invitation sent'); }, - onError: () => { - toast({ - title: 'Error resending invitation', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error resending invitation', { + description: 'Please contact tech support', }); }, }); @@ -228,10 +221,7 @@ function MonitoringObserversList() { } const exportMonitoringObservers = async () => { - const res = await authApi.get(`/election-rounds/${currentElectionRoundId}/monitoring-observers:export`, { - responseType: 'blob', - }); - const csvData = res.data; + const csvData = await exportMonitoringObserversApi(currentElectionRoundId); const blob = new Blob([csvData], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); diff --git a/web/src/features/monitoring-observers/components/PushMessageForm/PushMessageForm.tsx b/web/src/features/monitoring-observers/components/PushMessageForm/PushMessageForm.tsx index b557e795a..df3504563 100644 --- a/web/src/features/monitoring-observers/components/PushMessageForm/PushMessageForm.tsx +++ b/web/src/features/monitoring-observers/components/PushMessageForm/PushMessageForm.tsx @@ -8,12 +8,11 @@ import { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { authApi } from '@/common/auth-api'; +import { sendPushNotification } from '@/api/monitoring-observers/send-push-notification'; import type { FunctionComponent } from '@/common/types'; import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; import { RichTextEditor } from '@/components/rich-text-editor'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; -import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; @@ -31,6 +30,7 @@ import { Route } from '@/routes/monitoring-observers/create-new-message'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { useDebounce } from '@uidotdev/usehooks'; +import { toast } from 'sonner'; import { MonitoringObserverStatusSelect } from '../../filtering/MonitoringObserverStatusSelect'; import { MonitoringObserverTagsSelect } from '../../filtering/MonitoringObserverTagsSelect'; import { pushMessagesKeys, useTargetedMonitoringObservers } from '../../hooks/push-messages-queries'; @@ -118,19 +118,12 @@ function PushMessageForm(): FunctionComponent { electionRoundId: string; request: SendPushNotificationRequest & { title: string; body: string }; }) => { - return authApi.post( - `/election-rounds/${electionRoundId}/notifications:send`, - request - ); + return sendPushNotification(electionRoundId, request); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: pushMessagesKeys.all(currentElectionRoundId) }); - toast({ - title: 'Success', - description: 'Notification sent', - }); - + toast('Notification sent'); router.invalidate(); navigate({ to: '/monitoring-observers/$tab', params: { tab: 'push-messages' } }); }, diff --git a/web/src/features/monitoring-observers/hooks/monitoring-observers-queries.ts b/web/src/features/monitoring-observers/hooks/monitoring-observers-queries.ts index 20f6a912d..be594de61 100644 --- a/web/src/features/monitoring-observers/hooks/monitoring-observers-queries.ts +++ b/web/src/features/monitoring-observers/hooks/monitoring-observers-queries.ts @@ -1,6 +1,5 @@ -import { authApi } from '@/common/auth-api'; +import { getMonitoringObservers } from '@/api/monitoring-observers/get-monitoring-observers'; import type { DataTableParameters, PageResponse } from '@/common/types'; -import { buildURLSearchParams, isQueryFiltered } from '@/lib/utils'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import { MonitoringObserver } from '../models/monitoring-observer'; @@ -27,30 +26,7 @@ export const useMonitoringObservers = ( return useQuery({ queryKey: monitoringObserversKeys.list(electionRoundId, queryParams), queryFn: async () => { - const params = { - ...queryParams.otherParams, - PageNumber: String(queryParams.pageNumber), - PageSize: String(queryParams.pageSize), - SortColumnName: queryParams.sortColumnName, - SortOrder: queryParams.sortOrder, - }; - const searchParams = buildURLSearchParams(params); - - const response = await authApi.get>( - `/election-rounds/${electionRoundId}/monitoring-observers`, - { - params: searchParams, - } - ); - - if (response.status !== 200) { - throw new Error('Failed to fetch monitoring observers'); - } - - return { - ...response.data, - isEmpty: !isQueryFiltered(queryParams.otherParams ?? {}) && response.data.items.length === 0, - }; + return getMonitoringObservers(electionRoundId, queryParams); }, enabled: !!electionRoundId, staleTime: STALE_TIME diff --git a/web/src/features/monitoring-observers/hooks/push-messages-queries.ts b/web/src/features/monitoring-observers/hooks/push-messages-queries.ts index 601abe2ac..1b74a9da7 100644 --- a/web/src/features/monitoring-observers/hooks/push-messages-queries.ts +++ b/web/src/features/monitoring-observers/hooks/push-messages-queries.ts @@ -1,6 +1,6 @@ -import { authApi } from '@/common/auth-api'; +import { getPushMessages } from '@/api/monitoring-observers/get-push-messages'; +import { getTargetedMonitoringObservers } from '@/api/monitoring-observers/get-targeted-monitoring-observers'; import type { DataTableParameters, PageResponse } from '@/common/types'; -import { buildURLSearchParams, isQueryFiltered } from '@/lib/utils'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; import type { TargetedMonitoringObserver } from '../models/targeted-monitoring-observer'; import type { PushMessageModel } from '../models/push-message'; @@ -31,26 +31,7 @@ export function usePushMessages(electionRoundId: string, queryParams: DataTableP return useQuery({ queryKey: pushMessagesKeys.list(electionRoundId, queryParams), queryFn: async () => { - const params = { - ...queryParams.otherParams, - PageNumber: String(queryParams.pageNumber), - PageSize: String(queryParams.pageSize), - SortColumnName: queryParams.sortColumnName, - SortOrder: queryParams.sortOrder, - }; - const searchParams = buildURLSearchParams(params); - - const response = await authApi.get( - `/election-rounds/${electionRoundId}/notifications:listSent`, - { - params: searchParams, - } - ); - - return { - ...response.data, - isEmpty: !isQueryFiltered(params) && response.data.items.length === 0, - }; + return getPushMessages(electionRoundId, queryParams); }, staleTime: STALE_TIME, enabled: !!electionRoundId, @@ -68,27 +49,7 @@ export const useTargetedMonitoringObservers = ( return useQuery({ queryKey: targetedObserversKeys.list(electionRoundId, queryParams), queryFn: async () => { - const params = { - ...queryParams.otherParams, - PageNumber: String(queryParams.pageNumber), - PageSize: String(queryParams.pageSize), - SortColumnName: queryParams.sortColumnName, - SortOrder: queryParams.sortOrder, - }; - const searchParams = buildURLSearchParams(params); - - const response = await authApi.get>( - `election-rounds/${electionRoundId}/notifications:listRecipients`, - { - params: searchParams, - } - ); - - if (response.status !== 200) { - throw new Error('Failed to fetch notification'); - } - - return response.data; + return getTargetedMonitoringObservers(electionRoundId, queryParams); }, staleTime: STALE_TIME, enabled: !!electionRoundId, diff --git a/web/src/features/ngo-admin-dashboard/hooks/statistics-queries.ts b/web/src/features/ngo-admin-dashboard/hooks/statistics-queries.ts index 3e5c2512a..aba758883 100644 --- a/web/src/features/ngo-admin-dashboard/hooks/statistics-queries.ts +++ b/web/src/features/ngo-admin-dashboard/hooks/statistics-queries.ts @@ -1,7 +1,7 @@ -import { authApi } from '@/common/auth-api'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { MonitoringNgoStats } from '../models/ngo-admin-statistics-models'; import { DataSources } from '@/common/types'; +import { getElectionRoundStatistics } from '@/api/election-rounds/get-election-round-statistics'; const STALE_TIME = 1000 * 10 * 60; // 10 minutes @@ -19,9 +19,7 @@ export function useElectionRoundStatistics( return useQuery({ queryKey: statisticsCacheKey.all(electionRoundId, dataSource), queryFn: async () => { - const response = await authApi.get(`/election-rounds/${electionRoundId}/statistics?dataSource=${dataSource}`); - - return response.data; + return getElectionRoundStatistics(electionRoundId, dataSource); }, refetchOnMount: false, staleTime: STALE_TIME, diff --git a/web/src/features/ngos/components/CreateNGODialog.tsx b/web/src/features/ngos/components/CreateNGODialog.tsx index 539abf42d..40f57562d 100644 --- a/web/src/features/ngos/components/CreateNGODialog.tsx +++ b/web/src/features/ngos/components/CreateNGODialog.tsx @@ -2,12 +2,12 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { toast } from '@/components/ui/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { useCreateNgo } from '../hooks/ngos-queries'; import { newNgoSchema, NgoCreationFormData } from '../models/NGO'; -import { useCallback, useEffect } from 'react'; export interface CreateNGODialogProps { open: boolean; @@ -48,20 +48,15 @@ function CreateNGODialog({ open, onOpenChange }: CreateNGODialogProps) { onMutationSuccess: () => { form.reset({}); internalOnOpenChange(false); - toast({ - title: 'Success', - description: 'New organization created', - }); + toast('New organization created'); }, onMutationError: (error) => { error?.errors?.forEach((error) => { form.setError(error.name as keyof NgoCreationFormData, { type: 'custom', message: error.reason }); }); - toast({ - title: 'Error adding NGO admin', + toast.error('Error adding NGO admin',{ description: 'Please contact Platform admins', - variant: 'destructive', }); }, }); diff --git a/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx b/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx index bae8561f0..3279d1ff8 100644 --- a/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx +++ b/web/src/features/ngos/components/admins/AddNgoAdminDialog.tsx @@ -1,15 +1,15 @@ +import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog'; import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; -import { toast } from '@/components/ui/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useBlocker } from '@tanstack/react-router'; +import { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; import { useCreateNgoAdmin } from '../../hooks/ngo-admin-queries'; import { NgoAdminFormData, ngoAdminSchema } from '../../models/NgoAdmin'; -import { useCallback, useEffect } from 'react'; -import { useBlocker } from '@tanstack/react-router'; -import { useConfirm } from '@/components/ui/alert-dialog-provider'; export interface AddNgoAdminDialogProps { ngoId: string; @@ -79,20 +79,15 @@ function AddNgoAdminDialog({ open, onOpenChange, ngoId }: AddNgoAdminDialogProps onMutationSuccess: () => { form.reset({}); internalOnOpenChange(false); - toast({ - title: 'Success', - description: 'New NGO admin added', - }); + toast('New NGO admin added'); }, - onMutationError: (error) => { - error?.errors?.forEach((error) => { + onMutationError: (error: any) => { + error?.errors?.forEach((error: any) => { form.setError(error.name as keyof NgoAdminFormData, { type: 'custom', message: error.reason }); }); - toast({ - title: 'Error adding NGO admin', - description: 'Please contact Platform admins', - variant: 'destructive', + toast.error('Error adding NGO admin',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/ngos/hooks/ngo-admin-queries.ts b/web/src/features/ngos/hooks/ngo-admin-queries.ts index e32cb84f3..dd41d628d 100644 --- a/web/src/features/ngos/hooks/ngo-admin-queries.ts +++ b/web/src/features/ngos/hooks/ngo-admin-queries.ts @@ -2,7 +2,6 @@ import { authApi } from '@/common/auth-api'; import { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; import { queryOptions, useMutation, @@ -13,6 +12,7 @@ import { } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import axios, { AxiosError } from 'axios'; +import { toast } from 'sonner'; import { EditNgoAdminFormData, NgoAdmin, NgoAdminFormData, NgoAdminGetRequestParams } from '../models/NgoAdmin'; import { ngosKeys } from './ngos-queries'; const STALE_TIME = 1000 * 10 * 60; // 10 minutes @@ -114,10 +114,8 @@ export const useNgoAdminMutations = (ngoId: string) => { navigate({ to: '/ngos/admin/$ngoId/$adminId/view', params: { ngoId, adminId } }); }, onError: () => { - toast({ - title: 'Error editing NGO admin', - description: '', - variant: 'destructive', + toast.error('Error editing NGO admin',{ + description: 'Please contact tech support', }); }, }); @@ -131,19 +129,14 @@ export const useNgoAdminMutations = (ngoId: string) => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO admin was deleted successfully', - }); + toast('NGO admin was deleted successfully'); if (onMutationSuccess) onMutationSuccess(); }, onError: () => { - toast({ - title: 'Error deleting NGO admin', - description: '', - variant: 'destructive', + toast.error('Error deleting NGO admin',{ + description: 'Please contact tech support', }); }, }); @@ -157,17 +150,12 @@ export const useNgoAdminMutations = (ngoId: string) => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO admin was deactivated successfully', - }); + toast('NGO admin was deactivated successfully'); }, onError: () => { - toast({ - title: 'Error deactivating the NGO admin', - description: '', - variant: 'destructive', + toast.error('Error deactivating the NGO admin',{ + description: 'Please contact tech support', }); }, }); @@ -181,17 +169,12 @@ export const useNgoAdminMutations = (ngoId: string) => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO admin was activated successfully', - }); + toast('NGO admin was activated successfully'); }, onError: () => { - toast({ - title: 'Error activating the NGO admin', - description: '', - variant: 'destructive', + toast.error('Error activating the NGO admin',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/ngos/hooks/ngos-queries.ts b/web/src/features/ngos/hooks/ngos-queries.ts index 5e62ca0e0..77e5795d7 100644 --- a/web/src/features/ngos/hooks/ngos-queries.ts +++ b/web/src/features/ngos/hooks/ngos-queries.ts @@ -2,12 +2,12 @@ import { authApi } from '@/common/auth-api'; import { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; import { queryClient } from '@/main'; import { queryOptions, useMutation, useQuery, UseQueryResult, useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; -import { EditNgoFormData, NGO, NgoCreationFormData } from '../models/NGO'; import axios, { AxiosError } from 'axios'; +import { toast } from 'sonner'; +import { EditNgoFormData, NGO, NgoCreationFormData } from '../models/NGO'; const STALE_TIME = 1000 * 10 * 60; // 10 minutes @@ -86,10 +86,8 @@ export const useCreateNgo = () => { // Handle non-Axios or unexpected errors console.error('Unexpected error:', error); onMutationError(); - toast({ - title: 'Error creating a new NGO', - description: '', - variant: 'destructive', + toast.error('Error creating a new NGO',{ + description: 'Please contact tech support', }); }, }); @@ -115,10 +113,8 @@ export const useNgoMutations = () => { navigate({ to: '/ngos/view/$ngoId/$tab', params: { ngoId: ngoId!, tab: 'details' } }); }, onError: () => { - toast({ - title: 'Error editing NGO', - description: '', - variant: 'destructive', + toast.error('Error editing NGO',{ + description: 'Please contact tech support', }); }, }); @@ -132,17 +128,11 @@ export const useNgoMutations = () => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO was deactivated successfully', - }); + toast('NGO was deactivated successfully'); }, - onError: () => { - toast({ - title: 'Error deactivating NGO', - description: '', - variant: 'destructive', + toast.error('Error deactivating NGO',{ + description: 'Please contact tech support', }); }, }); @@ -156,17 +146,12 @@ export const useNgoMutations = () => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO was activated successfully', - }); + toast('NGO was activated successfully'); }, onError: () => { - toast({ - title: 'Error activating NGO', - description: '', - variant: 'destructive', + toast.error('Error activating NGO',{ + description: 'Please contact tech support', }); }, }); @@ -180,19 +165,13 @@ export const useNgoMutations = () => { queryClient.invalidateQueries({ queryKey: ngosKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'NGO was deleted successfully', - }); - + toast('NGO was deleted successfully'); if (onMutationSuccess) onMutationSuccess(); }, onError: () => { - toast({ - title: 'Error deleting NGO', - description: '', - variant: 'destructive', + toast.error('Error deleting NGO',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/observers/components/EditObserver/EditObserver.tsx b/web/src/features/observers/components/EditObserver/EditObserver.tsx index 6e9f1134a..8d811c2fb 100644 --- a/web/src/features/observers/components/EditObserver/EditObserver.tsx +++ b/web/src/features/observers/components/EditObserver/EditObserver.tsx @@ -12,10 +12,10 @@ import { TrashIcon } from '@heroicons/react/24/outline'; import { zodResolver } from '@hookform/resolvers/zod'; import { useSuspenseQuery } from '@tanstack/react-query'; import { useBlocker, useNavigate } from '@tanstack/react-router'; +import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { useObserverMutations } from '../../hooks/observers-queries'; import { EditObserverFormData, editObserverFormSchema } from '../../models/observer'; -import { useEffect } from 'react'; export default function EditObserver() { const navigate = useNavigate(); diff --git a/web/src/features/observers/hooks/observers-queries.ts b/web/src/features/observers/hooks/observers-queries.ts index 5b1bed496..770710401 100644 --- a/web/src/features/observers/hooks/observers-queries.ts +++ b/web/src/features/observers/hooks/observers-queries.ts @@ -1,13 +1,17 @@ -import { authApi } from '@/common/auth-api'; +import { createObserver } from '@/api/observers/create-observer'; +import { deleteObserver } from '@/api/observers/delete-observer'; +import { getObservers } from '@/api/observers/get-observers'; +import { toggleObserverStatus as toggleObserverStatusApi } from '@/api/observers/toggle-observer-status'; +import { updateObserver } from '@/api/observers/update-observer'; import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation'; import type { DataTableParameters, PageResponse, ProblemDetails } from '@/common/types'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { buttonVariants } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; import { type UseQueryResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useRouter } from '@tanstack/react-router'; import { AxiosError } from 'axios'; import { UseFormReturn } from 'react-hook-form'; +import { toast } from 'sonner'; import { AddObserverFormData, EditObserverFormData, Observer } from '../models/observer'; const STALE_TIME = 1000 * 60 * 5; // five minutes @@ -28,17 +32,7 @@ export const useObservers = (queryParams: DataTableParameters): UseObserversResu return useQuery({ queryKey: observersKeys.list(queryParams), queryFn: async () => { - const response = await authApi.get>('/observers', { - params: { - ...queryParams.otherParams, - status: (queryParams.otherParams as any)?.observerStatus, - }, - }); - if (response.status !== 200) { - throw new Error('Failed to fetch observers'); - } - - return response.data; + return getObservers(queryParams); }, staleTime: STALE_TIME, }); @@ -58,14 +52,11 @@ export const useObserverMutations = () => { form: UseFormReturn; onOpenChange: (isOpen: boolean) => void; }) => { - return authApi.post(`/observers`, values); + return createObserver(values); }, onSuccess: (_, { form, onOpenChange }) => { - toast({ - title: 'Success', - description: 'Observer added', - }); + toast('Observer added'); queryClient.invalidateQueries({ queryKey: observersKeys.all() }); router.invalidate(); form.reset({}); @@ -74,10 +65,8 @@ export const useObserverMutations = () => { onError: (error: AxiosError, { form }) => { console.error(error); addFormValidationErrorsFromBackend(form, error); - toast({ - title: 'Error adding observer', - description: '', - variant: 'destructive', + toast.error('Error adding observer',{ + description: 'Please contact tech support', }); }, }); @@ -91,7 +80,7 @@ export const useObserverMutations = () => { values: EditObserverFormData; form: UseFormReturn; }) => { - return authApi.put(`/observers/${observerId}`, values); + return updateObserver(observerId, values); }, onSuccess: (_, { observerId }) => { @@ -103,63 +92,48 @@ export const useObserverMutations = () => { console.error(error); addFormValidationErrorsFromBackend(form, error); - toast({ - title: 'Error editing observer', - description: '', - variant: 'destructive', + toast.error('Error editing observer',{ + description: 'Please contact tech support', }); }, }); const toggleObserverStatus = useMutation({ mutationFn: ({ observerId, isObserverActive }: { observerId: string; isObserverActive: boolean }) => { - const ACTION = isObserverActive ? 'deactivate' : 'activate'; - - return authApi.put(`/observers/${observerId}:${ACTION}`, {}); + return toggleObserverStatusApi(observerId, isObserverActive); }, onSuccess: (_, { isObserverActive }) => { queryClient.invalidateQueries({ queryKey: observersKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: `Observer was ${isObserverActive ? 'deactivated' : 'activated'}`, - }); + toast(`Observer was ${isObserverActive ? 'deactivated' : 'activated'}`); }, onError: (err, { isObserverActive }) => { console.error(err); - toast({ - title: `Error`, - description: `Error ${isObserverActive ? 'deactivating' : 'activating'} observer`, - variant: 'destructive', + toast.error(`Error ${isObserverActive ? 'deactivating' : 'activating'} observer`,{ + description: 'Please contact tech support', }); }, }); const deleteObserverMutation = useMutation({ mutationFn: ({ observerId }: { observerId: string; onMutationSuccess?: () => void }) => { - return authApi.delete(`/observers/${observerId}`); + return deleteObserver(observerId); }, onSuccess: (_, { onMutationSuccess }) => { queryClient.invalidateQueries({ queryKey: observersKeys.all() }); router.invalidate(); - toast({ - title: 'Success', - description: 'Observer was deleted successfully', - }); - + toast('Observer was deleted successfully'); if (onMutationSuccess) onMutationSuccess(); }, onError: () => { - toast({ - title: 'Error deleting observer', - description: '', - variant: 'destructive', + toast.error('Error deleting observer',{ + description: 'Please contact tech support', }); }, }); diff --git a/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx b/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx index 2d00d01b4..a31a56423 100644 --- a/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx +++ b/web/src/features/polling-stations/PollingStationsImport/PollingStationsImport.tsx @@ -1,25 +1,25 @@ import { FunctionComponent, importPollingStationSchema } from '@/common/types'; import Layout from '@/components/layout/Layout'; +import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { FileUploader } from '@/components/ui/file-uploader'; import { Separator } from '@/components/ui/separator'; -import { Badge } from '@/components/ui/badge'; import Papa from 'papaparse'; import { useCallback, useMemo, useState } from 'react'; import { z, ZodIssue } from 'zod'; import { authApi } from '@/common/auth-api'; import { Button } from '@/components/ui/button'; -import { useToast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { pollingStationsKeys } from '@/hooks/polling-stations-levels'; import { downloadImportExample, TemplateType } from '@/lib/utils'; import { queryClient } from '@/main'; -import { ArrowDownTrayIcon, DocumentTextIcon, CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline'; +import { ArrowDownTrayIcon, CheckCircleIcon, DocumentTextIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { LoaderIcon, Upload } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; import { ImportedPollingStationsDataTable } from './ImportedPollingStationsDataTable'; export type ImportPollingStationRow = z.infer & { errors?: ZodIssue[] }; @@ -29,7 +29,6 @@ export function PollingStationsImport(): FunctionComponent { const { t } = useTranslation('translation', { keyPrefix: 'electionEvent.pollingStations.addPollingStation' }); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const navigate = useNavigate(); - const { toast } = useToast(); function deletePollingStation(pollingStation: ImportPollingStationRow) { setPollingStations((prev) => [...prev.filter((obs) => obs.id !== pollingStation.id)]); @@ -72,19 +71,13 @@ export function PollingStationsImport(): FunctionComponent { }, onSuccess: (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: t('onSuccess'), - }); - + toast(t('onSuccess')); queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(electionRoundId) }); navigate({ to: '/election-rounds/$electionRoundId', params: { electionRoundId } }); }, onError: () => { - toast({ - title: t('onError'), + toast.error(t('onError'),{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); @@ -149,10 +142,8 @@ export function PollingStationsImport(): FunctionComponent { transformHeader: (header) => header.charAt(0).toLowerCase() + header.slice(1), async complete(results) { if (results.errors.length) { - toast({ - title: 'Parsing errors', + toast.error('Parsing errors', { description: 'Please check the file and try again', - variant: 'destructive', }); } diff --git a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx index 041491c2b..12cd28b84 100644 --- a/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx +++ b/web/src/features/responses/components/CitizenReportDetails/CitizenReportDetails.tsx @@ -1,4 +1,6 @@ import { authApi } from '@/common/auth-api'; +import { DateTimeFormat } from '@/common/formats'; +import { usePrevSearch } from '@/common/prev-search-store'; import { CitizenReportFollowUpStatus, ElectionRoundStatus, @@ -6,24 +8,22 @@ import { type FunctionComponent, } from '@/common/types'; import Layout from '@/components/layout/Layout'; +import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; -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 { citizenReportDetailsQueryOptions, Route } from '@/routes/responses/citizen-reports/$citizenReportId'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; +import { format } from 'date-fns'; +import { toast } from 'sonner'; import { citizenReportKeys } from '../../hooks/citizen-reports'; -import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; import { SubmissionType } from '../../models/common'; import { mapCitizenReportFollowUpStatus } from '../../utils/helpers'; -import { usePrevSearch } from '@/common/prev-search-store'; -import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; -import { DateTimeFormat } from '@/common/formats'; -import { format } from 'date-fns'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; export default function CitizenReportDetails(): FunctionComponent { const { citizenReportId } = Route.useParams(); @@ -52,20 +52,15 @@ export default function CitizenReportDetails(): FunctionComponent { }, onSuccess: async (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Follow-up status updated', - }); + toast('Follow-up status updated'); await queryClient.invalidateQueries({ queryKey: citizenReportKeys.all(electionRoundId) }); router.invalidate(); }, onError: () => { - toast({ - title: 'Error updating follow up status', + toast.error('Error updating follow up status',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx b/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx index f5de1f32a..b500b1af7 100644 --- a/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx +++ b/web/src/features/responses/components/ExportDataButton/ExportDataButton.tsx @@ -1,12 +1,12 @@ -import { useCallback, useEffect, useState } from 'react'; import { authApi } from '@/common/auth-api'; import type { FunctionComponent } from '@/common/types'; import { CsvFileIcon } from '@/components/icons/CsvFileIcon'; import { Button } from '@/components/ui/button'; -import { toast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useCallback, useEffect, useState } from 'react'; +import { toast } from 'sonner'; import { useExportedDataDetails, useStartDataExport } from '../../hooks/data-export'; import { ExportStatus, type ExportedDataType } from '../../models/data-export'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; interface ExportDataButtonProps { exportedDataType: ExportedDataType; @@ -28,7 +28,9 @@ export function ExportDataButton({ exportedDataType, filterParams }: ExportDataB setExportedDataId(data.exportedDataId); }, onError: () => { - toast({ title: 'Export failed, please try again later', variant: 'default' }); + toast.error('Export failed, please try again later',{ + description: 'Please contact tech support', + }); }, } ); @@ -73,7 +75,9 @@ export function ExportDataButton({ exportedDataType, filterParams }: ExportDataB useEffect(() => { if (exportStatus === ExportStatus.Failed) { - toast({ title: 'Export failed, please try again later', variant: 'default' }); + toast.error('Export failed, please try again later',{ + description: 'Please contact tech support', + }); } if (exportStatus === ExportStatus.Completed) { diff --git a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx index af954cf9a..bdc0831a2 100644 --- a/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx +++ b/web/src/features/responses/components/FormSubmissionDetails/FormSubmissionDetails.tsx @@ -1,11 +1,13 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; -import { ElectionRoundStatus, FormSubmissionFollowUpStatus, FunctionComponent, FormType } from '@/common/types'; +import { usePrevSearch } from '@/common/prev-search-store'; +import { ElectionRoundStatus, FormSubmissionFollowUpStatus, FormType, FunctionComponent } from '@/common/types'; import Layout from '@/components/layout/Layout'; +import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { LanguageBadge } from '@/components/ui/language-badge'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; -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'; @@ -14,14 +16,12 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; import { format } from 'date-fns'; +import { useState } from 'react'; +import { toast } from 'sonner'; import { formSubmissionsByEntryKeys, formSubmissionsByObserverKeys } from '../../hooks/form-submissions-queries'; 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'; -import { useState } from 'react'; -import { LanguageBadge } from '@/components/ui/language-badge'; export default function FormSubmissionDetails(): FunctionComponent { const { submissionId } = Route.useParams(); @@ -49,10 +49,7 @@ export default function FormSubmissionDetails(): FunctionComponent { }, onSuccess: async (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Follow-up status updated', - }); + toast('Follow-up status updated'); await queryClient.invalidateQueries({ queryKey: formSubmissionsByEntryKeys.all(electionRoundId) }); await queryClient.invalidateQueries({ queryKey: formSubmissionsByObserverKeys.all(electionRoundId) }); @@ -60,10 +57,8 @@ export default function FormSubmissionDetails(): FunctionComponent { }, onError: () => { - toast({ - title: 'Error updating follow up status', + toast.error('Error updating follow up status',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx b/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx index e85459ff5..60807a6db 100644 --- a/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx +++ b/web/src/features/responses/components/IncidentReportDetails/IncidentReportDetails.tsx @@ -1,23 +1,23 @@ import { authApi } from '@/common/auth-api'; -import { IncidentReportFollowUpStatus, type FunctionComponent, ElectionRoundStatus } from '@/common/types'; +import { DateTimeFormat } from '@/common/formats'; +import { ElectionRoundStatus, IncidentReportFollowUpStatus, type FunctionComponent } from '@/common/types'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; -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 { incidentReportDetailsQueryOptions, Route } from '@/routes/responses/incident-reports/$incidentReportId'; import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; +import { format } from 'date-fns'; +import { toast } from 'sonner'; import { incidentReportsByEntryKeys, incidentReportsByObserverKeys } from '../../hooks/incident-reports-queries'; import { SubmissionType } from '../../models/common'; import { mapIncidentReportFollowUpStatus, mapIncidentReportLocationType } from '../../utils/helpers'; import PreviewAnswer from '../PreviewAnswer/PreviewAnswer'; -import { format } from 'date-fns'; -import { DateTimeFormat } from '@/common/formats'; -import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; export default function IncidentReportDetails(): FunctionComponent { const { incidentReportId } = Route.useParams(); @@ -45,10 +45,7 @@ export default function IncidentReportDetails(): FunctionComponent { }, onSuccess: async (_, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Follow-up status updated', - }); + toast('Follow-up status updated'); await queryClient.invalidateQueries({ queryKey: incidentReportsByEntryKeys.all(electionRoundId) }); await queryClient.invalidateQueries({ queryKey: incidentReportsByObserverKeys.all(electionRoundId) }); @@ -57,10 +54,8 @@ export default function IncidentReportDetails(): FunctionComponent { }, onError: () => { - toast({ - title: 'Error updating follow up status', + toast.error('Error updating follow up status',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx index 70b690734..19a799d19 100644 --- a/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx +++ b/web/src/features/responses/components/QuickReportDetails/QuickReportDetails.tsx @@ -1,13 +1,12 @@ import { authApi } from '@/common/auth-api'; import { DateTimeFormat } from '@/common/formats'; import { usePrevSearch } from '@/common/prev-search-store'; -import { QuickReportFollowUpStatus, type FunctionComponent, ElectionRoundStatus } from '@/common/types'; +import { ElectionRoundStatus, QuickReportFollowUpStatus, type FunctionComponent } from '@/common/types'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import Layout from '@/components/layout/Layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; -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'; @@ -16,6 +15,7 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; import { useMutation, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useRouter } from '@tanstack/react-router'; import { format } from 'date-fns'; +import { toast } from 'sonner'; import { quickReportKeys } from '../../hooks/quick-reports'; import { SubmissionType } from '../../models/common'; import { mapIncidentCategory, mapQuickReportFollowUpStatus, mapQuickReportLocationType } from '../../utils/helpers'; @@ -44,20 +44,15 @@ export default function QuickReportDetails(): FunctionComponent { }, onSuccess: (_data, { electionRoundId }) => { - toast({ - title: 'Success', - description: 'Follow-up status updated', - }); + toast('Follow-up status updated successfully'); invalidate(); void queryClient.invalidateQueries({ queryKey: quickReportKeys.all(electionRoundId) }); }, onError: () => { - toast({ - title: 'Error updating follow up status', + toast.error('Error updating follow up status',{ description: 'Please contact tech support', - variant: 'destructive', }); }, }); diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index c5eabfca1..42a588ba0 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -1,5 +1,5 @@ import Header from '@/components/layout/Header/Header'; -import { Toaster } from '@/components/ui/toaster'; +import { Toaster } from '@/components/ui/sonner'; import { AuthContext } from '@/context/auth.context'; import { RouterContext } from '@/routerContext'; import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; @@ -13,7 +13,7 @@ function RootComponent() { const { isAuthenticated } = useContext(AuthContext); return ( - +
{isAuthenticated &&
} diff --git a/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx b/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx index b7cdc0431..ff4cd5eaa 100644 --- a/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx +++ b/web/src/routes/monitoring-observers/edit.$monitoringObserverId.tsx @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { getMonitoringObserverDetails } from '@/api/monitoring-observers/get-monitoring-observer-details'; 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'; @@ -10,15 +10,7 @@ export const monitoringObserverDetailsQueryOptions = (electionRoundId: string, m return queryOptions({ queryKey: monitoringObserversKeys.detail(electionRoundId, monitoringObserverId), queryFn: async () => { - const response = await authApi.get( - `/election-rounds/${electionRoundId}/monitoring-observers/${monitoringObserverId}` - ); - - if (response.status !== 200) { - throw new Error('Failed to fetch monitoring observer details'); - } - - return response.data; + return getMonitoringObserverDetails(electionRoundId, monitoringObserverId); }, enabled: !!electionRoundId, }); diff --git a/web/src/routes/monitoring-observers/push-messages.$id_.view.tsx b/web/src/routes/monitoring-observers/push-messages.$id_.view.tsx index 4ec11e10f..89227ce65 100644 --- a/web/src/routes/monitoring-observers/push-messages.$id_.view.tsx +++ b/web/src/routes/monitoring-observers/push-messages.$id_.view.tsx @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api' +import { getPushMessageDetails } from '@/api/monitoring-observers/get-push-message-details' import PushMessageDetails from '@/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails' import { redirectIfNotAuth } from '@/lib/utils' import { queryOptions } from '@tanstack/react-query' @@ -15,15 +15,7 @@ export const pushMessageDetailsQueryOptions = ( return queryOptions({ queryKey: pushMessagesKeys.detail(electionRoundId, pushMessageId), queryFn: async () => { - const response = await authApi.get( - `/election-rounds/${electionRoundId}/notifications/${pushMessageId}`, - ) - - if (response.status !== 200) { - throw new Error('Failed to fetch notification details') - } - - return response.data + return getPushMessageDetails(electionRoundId, pushMessageId) }, enabled: !!electionRoundId, }) diff --git a/web/src/routes/observers/$observerId.tsx b/web/src/routes/observers/$observerId.tsx index 57a361648..b2b5d14a6 100644 --- a/web/src/routes/observers/$observerId.tsx +++ b/web/src/routes/observers/$observerId.tsx @@ -1,4 +1,4 @@ -import { authApi } from '@/common/auth-api'; +import { getObserverDetails } from '@/api/observers/get-observer-details'; import ObserverProfile from '@/features/observers/components/ObserverProfile/ObserverProfile'; import { observersKeys } from '@/features/observers/hooks/observers-queries'; import { Observer } from '@/features/observers/models/observer'; @@ -10,13 +10,7 @@ export const observerDetailsQueryOptions = (observerId: string) => queryOptions({ queryKey: observersKeys.detail(observerId), queryFn: async () => { - const response = await authApi.get(`/observers/${observerId}`); - - if (response.status !== 200) { - throw new Error('Failed to fetch observer details'); - } - - return response.data; + return getObserverDetails(observerId); }, }); diff --git a/web/src/styles/tailwind.css b/web/src/styles/tailwind.css index 0447c541d..2ff6fc2f9 100644 --- a/web/src/styles/tailwind.css +++ b/web/src/styles/tailwind.css @@ -66,4 +66,8 @@ .breadcrumbs .crumb:last-child:after { display: none; +} + +body { + @apply bg-background text-foreground; } \ No newline at end of file