Repository: City-of-Helsinki/haitaton-ui
Pull request #1119: HAI-3433 Change Send dialog behaviour
Author: login: umeetiusbaar
Created: 2025-03-11T12:56:50Z | Merged: 2025-03-19T09:06:11Z

# Description

Change Send dialog behaviour to keep the dialog open until sending is finished and disable the controls on the dialog while sending is processing. In case of an error, an error message is shown in the dialog.

### Jira Issue: https://helsinkisolutionoffice.atlassian.net/browse/HAI-3433

## Type of change

- [x] Bug fix
- [ ] New feature
- [ ] Other

# Instructions for testing
For a thorough testing both johtoselvityshakemus and kaivuilmoitus should be tested (for täydennys also).

1. Fill an application
2. Send it from the form
4. Send dialog should behave as described in Jira ticket
5. Fill another application
6. Send it from the application view
7. Send dialog should behave as described in Jira ticket
8. Create and fill a täydennys for an application
9. Send it from the form
10. Send dialog should behave as described in Jira ticket
11. Create and fill another täydennys for an application
12. Send it from the application view
13. Send dialog should behave as described in Jira ticket

# Checklist:

- [x] I have written new tests (if applicable)
- [x] I have ran the tests myself (if applicable)
- [ ] I have made necessary changes to the documentation, link to confluence
      or other location:

# Other relevant info

Please describe here if there is e.g. some requirements for this change or
other info that the tester/user needs to know.



Diff:
diff --git a/src/common/components/HDSConfirmationDialog/ConfirmationDialog.tsx b/src/common/components/HDSConfirmationDialog/ConfirmationDialog.tsx
index ef56f2c14..4377bc5e0 100644
--- a/src/common/components/HDSConfirmationDialog/ConfirmationDialog.tsx
+++ b/src/common/components/HDSConfirmationDialog/ConfirmationDialog.tsx
@@ -18,7 +18,9 @@ type Props = {
   showCloseButton?: boolean;
   showSecondaryButton?: boolean;
   isLoading?: boolean;
+  loadingText?: string;
   headerIcon?: React.ReactNode;
+  disabled?: boolean;
 };
 
 const ConfirmationDialog: React.FC<React.PropsWithChildren<Props>> = ({
@@ -34,12 +36,14 @@ const ConfirmationDialog: React.FC<React.PropsWithChildren<Props>> = ({
   showCloseButton = false,
   showSecondaryButton = true,
   isLoading = false,
+  loadingText = mainBtnLabel,
   headerIcon = (
     <IconAlertCircleFill
       aria-hidden="true"
       color={variant === 'primary' ? 'var(--color-bus)' : 'var(--color-brick)'}
     />
   ),
+  disabled = false,
 }) => {
   const { t } = useTranslation();
 
@@ -50,6 +54,8 @@ const ConfirmationDialog: React.FC<React.PropsWithChildren<Props>> = ({
       variant={variant}
       iconLeft={mainBtnIcon}
       isLoading={isLoading}
+      loadingText={loadingText}
+      disabled={disabled}
     >
       {mainBtnLabel ?? t('common:confirmationDialog:confirmButton')}
     </Button>
@@ -61,6 +67,7 @@ const ConfirmationDialog: React.FC<React.PropsWithChildren<Props>> = ({
       onClick={close}
       theme={variant === 'danger' ? 'black' : 'default'}
       data-testid="dialog-cancel-test"
+      disabled={disabled}
     >
       {t('common:confirmationDialog:cancelButton')}
     </Button>
diff --git a/src/domain/application/applicationView/ApplicationView.test.tsx b/src/domain/application/applicationView/ApplicationView.test.tsx
index 49b81fda1..ec3e9d4c9 100644
--- a/src/domain/application/applicationView/ApplicationView.test.tsx
+++ b/src/domain/application/applicationView/ApplicationView.test.tsx
@@ -242,10 +242,10 @@ describe('Cable report application view', () => {
     cancelApplication.mockRestore();
   });
 
-  test('Should not send multiple requests if clicking Send button many times', async () => {
+  test('Confirming send disables dialog buttons', async () => {
     server.use(
       http.post('/api/hakemukset/:id/laheta', async () => {
-        await delay(200);
+        await delay(500);
         return new HttpResponse();
       }),
     );
@@ -260,10 +260,9 @@ describe('Cable report application view', () => {
     const confirmButton = screen.getByRole('button', { name: 'Vahvista' });
     expect(confirmButton).toBeEnabled();
     await user.click(confirmButton);
-    await user.click(confirmButton);
-    await user.click(confirmButton);
-
-    await screen.findByText('Hakemus on lähetetty käsiteltäväksi.');
+    expect(confirmButton).toBeDisabled();
+    expect(screen.getByRole('button', { name: 'Lähetetään' })).toBeDisabled();
+    expect(screen.getByRole('button', { name: 'Peruuta' })).toBeDisabled();
 
     expect(sendApplication).toHaveBeenCalledTimes(1);
 
diff --git a/src/domain/application/applicationView/ApplicationView.tsx b/src/domain/application/applicationView/ApplicationView.tsx
index f7b05d21d..e72f0edd3 100644
--- a/src/domain/application/applicationView/ApplicationView.tsx
+++ b/src/domain/application/applicationView/ApplicationView.tsx
@@ -71,7 +71,6 @@ import KaivuilmoitusAttachmentSummary from '../components/summary/KaivuilmoitusA
 import InvoicingCustomerSummary from '../components/summary/InvoicingCustomerSummary';
 import React, { useState } from 'react';
 import { SignedInUser } from '../../hanke/hankeUsers/hankeUser';
-import useSendApplication from '../hooks/useSendApplication';
 import { validationSchema as johtoselvitysValidationSchema } from '../../johtoselvitys/validationSchema';
 import { validationSchema as kaivuilmoitusValidationSchema } from '../../kaivuilmoitus/validationSchema';
 import ApplicationReportCompletionDateDialog from '../../kaivuilmoitus/components/ApplicationReportCompletionDateDialog';
@@ -688,7 +687,6 @@ function ApplicationView({
 }: Readonly<Props>) {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
-  const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false);
   const [showSendDialog, setShowSendDialog] = useState(false);
   const [showReportOperationalConditionDialog, setShowReportOperationalConditionDialog] =
     useState(false);
@@ -762,8 +760,6 @@ function ApplicationView({
     applicationType,
     alluStatus,
   );
-  const applicationSendMutation = useSendApplication();
-
   const informationRequestFeatureEnabled = useIsInformationRequestFeatureEnabled();
 
   const { sendTaydennysButton, sendTaydennysDialog } = useSendTaydennys(application, signedInUser);
@@ -773,15 +769,6 @@ function ApplicationView({
     (alluStatus === AlluStatus.DECISION || alluStatus === AlluStatus.OPERATIONAL_CONDITION) &&
     !muutosilmoitus?.sent;
 
-  async function onSendApplication(pdr: PaperDecisionReceiver | undefined | null) {
-    applicationSendMutation.mutate({
-      id: id as number,
-      paperDecisionReceiver: pdr,
-    });
-    setIsSendButtonDisabled(true);
-    setShowSendDialog(false);
-  }
-
   function openSendDialog() {
     setShowSendDialog(true);
   }
@@ -920,7 +907,7 @@ function ApplicationView({
                 theme="coat"
                 iconLeft={<IconEnvelope aria-hidden="true" />}
                 onClick={openSendDialog}
-                disabled={disableSendButton || isSendButtonDisabled}
+                disabled={disableSendButton}
               >
                 {t('hakemus:buttons:sendApplication')}
               </Button>
@@ -1245,10 +1232,9 @@ function ApplicationView({
       )}
       <ApplicationSendDialog
         type={applicationType}
+        id={application.id}
         isOpen={showSendDialog}
-        isLoading={applicationSendMutation.isLoading}
         onClose={closeSendDialog}
-        onSend={onSendApplication}
       />
       {sendTaydennysDialog}
     </InformationViewContainer>
diff --git a/src/domain/application/applicationView/hooks/useSendTaydennys.tsx b/src/domain/application/applicationView/hooks/useSendTaydennys.tsx
index 2a834115a..f9fbadb68 100644
--- a/src/domain/application/applicationView/hooks/useSendTaydennys.tsx
+++ b/src/domain/application/applicationView/hooks/useSendTaydennys.tsx
@@ -1,6 +1,13 @@
-import { useState } from 'react';
-import { Button, IconEnvelope, IconQuestionCircle, Notification } from 'hds-react';
-import { useTranslation } from 'react-i18next';
+import React, { useState } from 'react';
+import {
+  Button,
+  IconEnvelope,
+  IconQuestionCircle,
+  Link,
+  LoadingSpinner,
+  Notification,
+} from 'hds-react';
+import { Trans, useTranslation } from 'react-i18next';
 import { AlluStatus, Application } from '../../types/application';
 import { validationSchema as johtoselvitysValidationSchema } from '../../../johtoselvitysTaydennys/validationSchema';
 import { validationSchema as kaivuilmoitusValidationSchema } from '../../../kaivuilmoitusTaydennys/validationSchema';
@@ -9,6 +16,8 @@ import useIsInformationRequestFeatureEnabled from '../../taydennys/hooks/useIsIn
 import useSendTaydennysMutation from '../../taydennys/hooks/useSendTaydennys';
 import { isContactIn } from '../../utils';
 import { SignedInUser } from '../../../hanke/hankeUsers/hankeUser';
+import { Box } from '@chakra-ui/react';
+import useTaydennysSendNotification from '../../taydennys/hooks/useTaydennysSendNotification';
 
 const validationSchemas = {
   CABLE_REPORT: johtoselvitysValidationSchema,
@@ -31,38 +40,51 @@ export default function useSendTaydennys(
     (application.taydennys.muutokset.length > 0 || application.taydennys.liitteet.length > 0);
   const [showSendTaydennysDialog, setShowSendTaydennysDialog] = useState(false);
   const sendTaydennysMutation = useSendTaydennysMutation();
+  const { showSendSuccess } = useTaydennysSendNotification();
   const isContact =
     application.taydennys && isContactIn(signedInUser, application.taydennys.applicationData);
-  const disableSendButton = Boolean(!isContact);
 
   function openSendTaydennysDialog() {
     setShowSendTaydennysDialog(true);
   }
 
   function closeSendTaydennysDialog() {
-    setShowSendTaydennysDialog(false);
+    if (!sendTaydennysMutation.isLoading) {
+      sendTaydennysMutation.reset();
+      setShowSendTaydennysDialog(false);
+    }
   }
 
   function sendTaydennysHakemus() {
     if (application.taydennys?.id) {
-      sendTaydennysMutation.mutate(application.taydennys.id);
-      closeSendTaydennysDialog();
+      sendTaydennysMutation.mutate(application.taydennys.id, {
+        onSuccess() {
+          showSendSuccess();
+          closeSendTaydennysDialog();
+        },
+      });
     }
   }
 
+  const sendButtonIcon = sendTaydennysMutation.isLoading ? (
+    <LoadingSpinner small />
+  ) : (
+    <IconEnvelope />
+  );
+
   const sendTaydennysButton = showSendTaydennysButton ? (
     <>
       <Button
         theme="coat"
-        iconLeft={<IconEnvelope />}
+        iconLeft={sendButtonIcon}
         onClick={openSendTaydennysDialog}
-        loadingText={t('common:buttons:sendingText')}
-        isLoading={sendTaydennysMutation.isLoading}
-        disabled={disableSendButton}
+        disabled={sendTaydennysMutation.isLoading}
       >
-        {t('taydennys:buttons:sendTaydennys')}
+        {sendTaydennysMutation.isLoading
+          ? t('common:buttons:sendingText')
+          : t('taydennys:buttons:sendTaydennys')}
       </Button>
-      {disableSendButton && (
+      {!isContact && (
         <Notification
           size="small"
           style={{ marginTop: 'var(--spacing-xs)' }}
@@ -78,13 +100,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        sendTaydennysMutation.isError ? (
+          <>
+            {t('taydennys:sendDialog:description')}
+            <Box paddingTop="var(--spacing-s)">
+              <Notification
+                type="error"
+                size="small"
+                label={t('taydennys:notifications:sendErrorLabel')}
+              >
+                <Trans i18nKey="taydennys:notifications:sendErrorText">
+                  <p>
+                    Lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä
+                    Haitattoman tekniseen tukeen sähköpostiosoitteessa
+                    <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
+                  </p>
+                </Trans>
+              </Notification>
+            </Box>
+          </>
+        ) : (
+          t('taydennys:sendDialog:description')
+        )
+      }
       showCloseButton
       isOpen={showSendTaydennysDialog}
       close={closeSendTaydennysDialog}
       mainAction={sendTaydennysHakemus}
       variant="primary"
+      isLoading={sendTaydennysMutation.isLoading}
+      loadingText={t('common:buttons:sendingText')}
       headerIcon={<IconQuestionCircle />}
+      disabled={sendTaydennysMutation.isLoading}
     />
   );
 
diff --git a/src/domain/application/components/ApplicationSendDialog.test.tsx b/src/domain/application/components/ApplicationSendDialog.test.tsx
index fe66a2a02..e7855a499 100644
--- a/src/domain/application/components/ApplicationSendDialog.test.tsx
+++ b/src/domain/application/components/ApplicationSendDialog.test.tsx
@@ -1,15 +1,18 @@
 import { render, screen } from '../../../testUtils/render';
 import ApplicationSendDialog from './ApplicationSendDialog';
 import { waitFor } from '@testing-library/react';
+import { server } from '../../mocks/test-server';
+import { http, HttpResponse } from 'msw';
+import { cloneDeep } from 'lodash';
+import hakemukset from '../../mocks/data/hakemukset-data';
 
 test('Shows correct information when opened for excavation notification', async () => {
   render(
     <ApplicationSendDialog
       type="EXCAVATION_NOTIFICATION"
+      id={1}
       isOpen={true}
-      isLoading={false}
       onClose={() => {}}
-      onSend={() => {}}
     />,
   );
 
@@ -33,15 +36,7 @@ test('Shows correct information when opened for excavation notification', async
 });
 
 test('Shows correct information when opened for cable report', async () => {
-  render(
-    <ApplicationSendDialog
-      type="CABLE_REPORT"
-      isOpen={true}
-      isLoading={false}
-      onClose={() => {}}
-      onSend={() => {}}
-    />,
-  );
+  render(<ApplicationSendDialog type="CABLE_REPORT" id={1} isOpen={true} onClose={() => {}} />);
 
   expect(screen.getByText(/lähetä hakemus\?/i)).toBeInTheDocument();
   expect(
@@ -65,15 +60,7 @@ test('Shows correct information when opened for cable report', async () => {
 test('Shows correct information when opened for cable report when paper decision feature is disabled', async () => {
   const OLD_ENV = { ...window._env_ };
   window._env_ = { ...OLD_ENV, REACT_APP_FEATURE_CABLE_REPORT_PAPER_DECISION: 0 };
-  render(
-    <ApplicationSendDialog
-      type="CABLE_REPORT"
-      isOpen={true}
-      isLoading={false}
-      onClose={() => {}}
-      onSend={() => {}}
-    />,
-  );
+  render(<ApplicationSendDialog type="CABLE_REPORT" id={1} isOpen={true} onClose={() => {}} />);
 
   expect(screen.getByText(/lähetä hakemus\?/i)).toBeInTheDocument();
   expect(
@@ -100,10 +87,9 @@ test('Shows correct information when ordering paper decision', async () => {
   const { user } = render(
     <ApplicationSendDialog
       type="EXCAVATION_NOTIFICATION"
+      id={1}
       isOpen={true}
-      isLoading={false}
       onClose={() => {}}
-      onSend={() => {}}
     />,
   );
 
@@ -139,10 +125,9 @@ test('Enables confirmation button when form is filled', async () => {
   const { user } = render(
     <ApplicationSendDialog
       type="EXCAVATION_NOTIFICATION"
+      id={1}
       isOpen={true}
-      isLoading={false}
       onClose={() => {}}
-      onSend={() => {}}
     />,
   );
 
@@ -167,15 +152,23 @@ test('Enables confirmation button when form is filled', async () => {
   expect(confirmButton).toBeEnabled();
 });
 
-test('Confirm calls onSend', async () => {
-  const onSend = jest.fn();
+test('Sends the application when confirmed', async () => {
+  const hakemus = cloneDeep(hakemukset[0]);
+  let sent = false;
+  server.use(
+    http.post(`/api/hakemukset/:id/laheta`, async () => {
+      hakemus.alluStatus = 'PENDING';
+      sent = true;
+      return HttpResponse.json(hakemus);
+    }),
+  );
+
   const { user } = render(
     <ApplicationSendDialog
       type="EXCAVATION_NOTIFICATION"
+      id={1}
       isOpen={true}
-      isLoading={false}
       onClose={() => {}}
-      onSend={onSend}
     />,
   );
 
@@ -194,21 +187,53 @@ test('Confirm calls onSend', async () => {
   await user.type(cityInput, 'Helsinki');
   const confirmButton = screen.getByRole('button', { name: 'Vahvista' });
   await user.click(confirmButton);
-  expect(onSend).toHaveBeenCalled();
+  expect(sent).toBe(true);
 });
 
-test('Cancel calls onClose', async () => {
-  const onClose = jest.fn();
+test('Shows error message when sending fails', async () => {
+  server.use(
+    http.post(`/api/hakemukset/:id/laheta`, async () => {
+      return HttpResponse.json({ errorMessage: 'Failed for testing purposes' }, { status: 500 });
+    }),
+  );
+
   const { user } = render(
     <ApplicationSendDialog
       type="EXCAVATION_NOTIFICATION"
+      id={1}
       isOpen={true}
-      isLoading={false}
-      onClose={onClose}
-      onSend={() => {}}
+      onClose={() => {}}
     />,
   );
 
+  const orderPaperDecisionButton = screen.getByRole('button', {
+    name: 'Tilaan päätöksen myös paperisena',
+  });
+  await user.click(orderPaperDecisionButton);
+  await screen.findByText('Täytä vastaanottajan tiedot');
+  const nameInput = screen.getByText('Nimi');
+  await user.type(nameInput, 'Pekka Paperinen');
+  const streetAddressInput = screen.getByText('Katuosoite');
+  await user.type(streetAddressInput, 'Paperitie 1');
+  const postalCodeInput = screen.getByText('Postinumero');
+  await user.type(postalCodeInput, '00100');
+  const cityInput = screen.getByText('Postitoimipaikka');
+  await user.type(cityInput, 'Helsinki');
+  const confirmButton = screen.getByRole('button', { name: 'Vahvista' });
+  await user.click(confirmButton);
+  expect(
+    await screen.findByText(
+      'Hakemuksen lähettäminen käsittelyyn epäonnistui. Yritä lähettämistä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa',
+    ),
+  ).toBeInTheDocument();
+});
+
+test('Calls onClose when cancelled', async () => {
+  const onClose = jest.fn();
+  const { user } = render(
+    <ApplicationSendDialog type="EXCAVATION_NOTIFICATION" id={1} isOpen={true} onClose={onClose} />,
+  );
+
   const cancelButton = screen.getByRole('button', { name: 'Peruuta' });
   await user.click(cancelButton);
   expect(onClose).toHaveBeenCalled();
diff --git a/src/domain/application/components/ApplicationSendDialog.tsx b/src/domain/application/components/ApplicationSendDialog.tsx
index fefa4be5e..2fff75c30 100644
--- a/src/domain/application/components/ApplicationSendDialog.tsx
+++ b/src/domain/application/components/ApplicationSendDialog.tsx
@@ -3,11 +3,13 @@ import {
   Dialog,
   IconCheck,
   IconQuestionCircle,
+  Link,
+  LoadingSpinner,
   Notification,
   ToggleButton,
 } from 'hds-react';
 import React from 'react';
-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
 import { ApplicationSendData, ApplicationType, PaperDecisionReceiver } from '../types/application';
 import { FormProvider, useForm } from 'react-hook-form';
 import { yupResolver } from '@hookform/resolvers/yup';
@@ -16,16 +18,17 @@ import { Box, Grid, GridItem } from '@chakra-ui/react';
 import Text from '../../../common/components/text/Text';
 import TextInput from '../../../common/components/textInput/TextInput';
 import { useFeatureFlags } from '../../../common/components/featureFlags/FeatureFlagsContext';
+import useSendApplication from '../hooks/useSendApplication';
+import useApplicationSendNotification from '../hooks/useApplicationSendNotification';
 
 type Props = {
   type: ApplicationType;
+  id?: number | null;
   isOpen: boolean;
-  isLoading: boolean;
-  onClose: () => void;
-  onSend: (paperDecisionReceiver?: PaperDecisionReceiver | null) => void;
+  onClose: (id?: number | null) => void;
 };
 
-const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClose, onSend }) => {
+const ApplicationSendDialog: React.FC<Props> = ({ type, id, isOpen, onClose }) => {
   const { t } = useTranslation();
   const features = useFeatureFlags();
   const formContext = useForm<ApplicationSendData>({
@@ -43,12 +46,25 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
   const paperDecisionFeatureEnabled =
     type === 'EXCAVATION_NOTIFICATION' || features.cableReportPaperDecision;
 
+  const applicationSendMutation = useSendApplication();
+  const { showSendSuccess } = useApplicationSendNotification();
+
   async function submitForm(data: ApplicationSendData) {
     const paperDecisionReceiver = data.orderPaperDecision
       ? (data.paperDecisionReceiver as PaperDecisionReceiver)
       : null;
-    onSend(paperDecisionReceiver);
-    onClose();
+    applicationSendMutation.mutate(
+      {
+        id: id as number,
+        paperDecisionReceiver: paperDecisionReceiver,
+      },
+      {
+        onSuccess(applicationData) {
+          showSendSuccess();
+          onClose(applicationData.id);
+        },
+      },
+    );
   }
 
   function handleOrderPaperDecisionChange() {
@@ -62,17 +78,26 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
   }
 
   function handleClose() {
-    reset();
-    onClose();
+    if (!applicationSendMutation.isLoading) {
+      reset();
+      applicationSendMutation.reset();
+      onClose();
+    }
   }
 
+  const submitButtonIcon = applicationSendMutation.isLoading ? (
+    <LoadingSpinner small />
+  ) : (
+    <IconCheck />
+  );
+
   return (
     <Dialog
       id="application-send"
       isOpen={isOpen}
       aria-labelledby={dialogTitle}
       variant="primary"
-      close={onClose}
+      close={handleClose}
       closeButtonLabelText={t('common:ariaLabels:closeButtonLabelText')}
     >
       <Dialog.Header
@@ -97,6 +122,7 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                   label={t('hakemus:sendDialog:orderPaperDecision')}
                   checked={orderPaperDecisionChecked}
                   onChange={handleOrderPaperDecisionChange}
+                  disabled={applicationSendMutation.isLoading}
                 />
               </Box>
             )}
@@ -118,6 +144,7 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.name"
                       label={t('hakemus:sendDialog:name')}
                       required={orderPaperDecisionChecked}
+                      disabled={applicationSendMutation.isLoading}
                     />
                   </GridItem>
                   <GridItem colSpan={2}>
@@ -126,6 +153,7 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.streetAddress"
                       label={t('hakemus:sendDialog:streetAddress')}
                       required={orderPaperDecisionChecked}
+                      disabled={applicationSendMutation.isLoading}
                     />
                   </GridItem>
                   <GridItem colSpan={{ sm: 1, xs: 2 }} colStart={{ sm: 1, xs: 1 }}>
@@ -134,6 +162,7 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.postalCode"
                       label={t('hakemus:sendDialog:postalCode')}
                       required={orderPaperDecisionChecked}
+                      disabled={applicationSendMutation.isLoading}
                     />
                   </GridItem>
                   <GridItem colSpan={2} colStart={{ sm: 2, xs: 1 }}>
@@ -142,24 +171,44 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.city"
                       label={t('hakemus:sendDialog:city')}
                       required={orderPaperDecisionChecked}
+                      disabled={applicationSendMutation.isLoading}
                     />
                   </GridItem>
                 </Grid>
               </Box>
             )}
+            {applicationSendMutation.isError && (
+              <Notification
+                type="error"
+                size="small"
+                label={t('hakemus:notifications:sendErrorLabel')}
+              >
+                <Trans i18nKey="hakemus:notifications:sendErrorText">
+                  <p>
+                    Hakemuksen lähettäminen käsittelyyn epäonnistui. Yritä lähettämistä myöhemmin
+                    uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa
+                    <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
+                  </p>
+                </Trans>
+              </Notification>
+            )}
           </Dialog.Content>
 
           <Dialog.ActionButtons>
             <Button
               type="submit"
-              iconLeft={<IconCheck />}
-              isLoading={isLoading}
-              loadingText={t('common:buttons:sendingText')}
-              disabled={!isConfirmButtonEnabled}
+              iconLeft={submitButtonIcon}
+              disabled={!isConfirmButtonEnabled || applicationSendMutation.isLoading}
             >
-              {t('common:confirmationDialog:confirmButton')}
+              {applicationSendMutation.isLoading
+                ? t('common:buttons:sendingText')
+                : t('common:confirmationDialog:confirmButton')}
             </Button>
-            <Button variant="secondary" onClick={handleClose}>
+            <Button
+              variant="secondary"
+              onClick={handleClose}
+              disabled={applicationSendMutation.isLoading}
+            >
               {t('common:confirmationDialog:cancelButton')}
             </Button>
           </Dialog.ActionButtons>
diff --git a/src/domain/application/hooks/useApplicationSendNotification.tsx b/src/domain/application/hooks/useApplicationSendNotification.tsx
index 6c6004559..e14b4e3c9 100644
--- a/src/domain/application/hooks/useApplicationSendNotification.tsx
+++ b/src/domain/application/hooks/useApplicationSendNotification.tsx
@@ -1,14 +1,10 @@
-import React from 'react';
-import { Link } from 'hds-react';
-import { Trans, useTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { useGlobalNotification } from '../../../common/components/globalNotification/GlobalNotificationContext';
 
 /**
- * Returns functions for showing success and error notifications for sending application
+ * Returns functions for showing success notification for sending application
  */
-export default function useApplicationSendNotification(
-  errorMessageKey: string = 'hakemus:notifications:sendErrorText',
-) {
+export default function useApplicationSendNotification() {
   const { t } = useTranslation();
   const { setNotification } = useGlobalNotification();
 
@@ -25,27 +21,5 @@ export default function useApplicationSendNotification(
     });
   }
 
-  function showSendError() {
-    const sendErrorMessage = (
-      <Trans i18nKey={errorMessageKey}>
-        <p>
-          Hakemus on tallennettu luonnoksena, koska hakemuksen lähettäminen käsittelyyn epäonnistui.
-          Yritä lähettämistä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen
-          sähköpostiosoitteessa
-          <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
-        </p>
-      </Trans>
-    );
-
-    setNotification(true, {
-      position: 'top-right',
-      dismissible: true,
-      label: t('hakemus:notifications:sendErrorLabel'),
-      message: sendErrorMessage,
-      type: 'error',
-      closeButtonLabelText: t('common:components:notification:closeButtonLabelText'),
-    });
-  }
-
-  return { showSendSuccess, showSendError };
+  return { showSendSuccess };
 }
diff --git a/src/domain/application/hooks/useSendApplication.ts b/src/domain/application/hooks/useSendApplication.ts
index 33aec88bd..bda7b3f2e 100644
--- a/src/domain/application/hooks/useSendApplication.ts
+++ b/src/domain/application/hooks/useSendApplication.ts
@@ -1,6 +1,5 @@
 import { useQueryClient } from 'react-query';
 import { sendApplication } from '../utils';
-import useApplicationSendNotification from './useApplicationSendNotification';
 import { Application, PaperDecisionReceiver } from '../types/application';
 import useDebouncedMutation from '../../../common/hooks/useDebouncedMutation';
 
@@ -11,17 +10,12 @@ type Options = {
 export default function useSendApplication(options?: Options) {
   const { onSuccess } = options || {};
   const queryClient = useQueryClient();
-  const { showSendSuccess, showSendError } = useApplicationSendNotification();
 
   return useDebouncedMutation(
     (variables: { id: number; paperDecisionReceiver: PaperDecisionReceiver | undefined | null }) =>
       sendApplication(variables.id, variables.paperDecisionReceiver),
     {
-      onError() {
-        showSendError();
-      },
       async onSuccess(data) {
-        showSendSuccess();
         await queryClient.invalidateQueries(['application', data.id], { refetchInactive: true });
         onSuccess && onSuccess(data);
       },
diff --git a/src/domain/application/taydennys/hooks/useSendTaydennys.ts b/src/domain/application/taydennys/hooks/useSendTaydennys.ts
index 0a36d308c..912df1599 100644
--- a/src/domain/application/taydennys/hooks/useSendTaydennys.ts
+++ b/src/domain/application/taydennys/hooks/useSendTaydennys.ts
@@ -1,22 +1,22 @@
 import { useQueryClient } from 'react-query';
-import { JohtoselvitysData, KaivuilmoitusData } from '../../types/application';
-import useTaydennysSendNotification from './useTaydennysSendNotification';
+import { Application, JohtoselvitysData, KaivuilmoitusData } from '../../types/application';
 import useDebouncedMutation from '../../../../common/hooks/useDebouncedMutation';
 import { sendTaydennys } from '../taydennysApi';
 
-export default function useSendTaydennys() {
+type Options = {
+  onSuccess: (data: Application) => void;
+};
+
+export default function useSendTaydennys(options?: Options) {
+  const { onSuccess } = options || {};
   const queryClient = useQueryClient();
-  const { showSendSuccess, showSendError } = useTaydennysSendNotification();
 
   return useDebouncedMutation(
     (id: string) => sendTaydennys<JohtoselvitysData | KaivuilmoitusData>(id),
     {
-      onError() {
-        showSendError();
-      },
       async onSuccess(data) {
-        showSendSuccess();
         await queryClient.invalidateQueries(['application', data.id], { refetchInactive: true });
+        onSuccess && onSuccess(data);
       },
     },
   );
diff --git a/src/domain/application/taydennys/hooks/useTaydennysSendNotification.tsx b/src/domain/application/taydennys/hooks/useTaydennysSendNotification.tsx
index 8ce407c22..e694fb8dd 100644
--- a/src/domain/application/taydennys/hooks/useTaydennysSendNotification.tsx
+++ b/src/domain/application/taydennys/hooks/useTaydennysSendNotification.tsx
@@ -1,5 +1,4 @@
-import { Trans, useTranslation } from 'react-i18next';
-import { Link } from 'hds-react';
+import { useTranslation } from 'react-i18next';
 import { useGlobalNotification } from '../../../../common/components/globalNotification/GlobalNotificationContext';
 
 /**
@@ -22,26 +21,5 @@ export default function useTaydennysSendNotification() {
     });
   }
 
-  function showSendError() {
-    const sendErrorMessage = (
-      <Trans i18nKey="taydennys:notifications:sendErrorText">
-        <p>
-          Lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman
-          tekniseen tukeen sähköpostiosoitteessa
-          <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
-        </p>
-      </Trans>
-    );
-
-    setNotification(true, {
-      position: 'top-right',
-      dismissible: true,
-      label: t('taydennys:notifications:sendErrorLabel'),
-      message: sendErrorMessage,
-      type: 'error',
-      closeButtonLabelText: t('common:components:notification:closeButtonLabelText'),
-    });
-  }
-
-  return { showSendSuccess, showSendError };
+  return { showSendSuccess };
 }
diff --git a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx
index 29e5de447..d8a5647be 100644
--- a/src/domain/johtoselvitys/JohtoselvitysContainer.tsx
+++ b/src/domain/johtoselvitys/JohtoselvitysContainer.tsx
@@ -37,7 +37,6 @@ import {
   JohtoselvitysCreateData,
   JohtoselvitysData,
   JohtoselvitysUpdateData,
-  PaperDecisionReceiver,
 } from '../application/types/application';
 import Attachments from './Attachments';
 import ConfirmationDialog from '../../common/components/HDSConfirmationDialog/ConfirmationDialog';
@@ -46,7 +45,6 @@ import { APPLICATION_ID_STORAGE_KEY } from '../application/constants';
 import { usePermissionsForHanke } from '../hanke/hankeUsers/hooks/useUserRightsForHanke';
 import useSaveApplication from '../application/hooks/useSaveApplication';
 import useNavigateToApplicationView from '../application/hooks/useNavigateToApplicationView';
-import useSendApplication from '../application/hooks/useSendApplication';
 import ApplicationSendDialog from '../application/components/ApplicationSendDialog';
 
 type Props = {
@@ -134,7 +132,6 @@ const JohtoselvitysContainer: React.FC<React.PropsWithChildren<Props>> = ({
 
   const navigateToApplicationView = useNavigateToApplicationView();
 
-  const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false);
   const [showSendDialog, setShowSendDialog] = useState(false);
 
   const {
@@ -183,28 +180,13 @@ const JohtoselvitysContainer: React.FC<React.PropsWithChildren<Props>> = ({
     },
   });
 
-  const applicationSendMutation = useSendApplication({
-    onSuccess(data) {
-      navigateToApplicationView(data.id?.toString());
-    },
-  });
-
-  async function onSendApplication(pdr: PaperDecisionReceiver | undefined | null) {
-    const data = getValues();
-    applicationSendMutation.mutate({
-      id: data.id as number,
-      paperDecisionReceiver: pdr,
-    });
-    setIsSendButtonDisabled(true);
-    setShowSendDialog(false);
-  }
-
   function openSendDialog() {
     setShowSendDialog(true);
   }
 
-  function closeSendDialog() {
+  function closeSendDialog(id?: number | null) {
     setShowSendDialog(false);
+    navigateToApplicationView(id?.toString());
   }
 
   function saveCableApplication(handleSuccess?: (data: Application<JohtoselvitysData>) => void) {
@@ -442,7 +424,7 @@ const JohtoselvitysContainer: React.FC<React.PropsWithChildren<Props>> = ({
                   type="submit"
                   iconLeft={<IconEnvelope aria-hidden="true" />}
                   loadingText={t('common:buttons:sendingText')}
-                  disabled={disableSendButton || isSendButtonDisabled}
+                  disabled={disableSendButton}
                 >
                   {t('hakemus:buttons:sendApplication')}
                 </Button>
@@ -477,10 +459,9 @@ const JohtoselvitysContainer: React.FC<React.PropsWithChildren<Props>> = ({
 
       <ApplicationSendDialog
         type="CABLE_REPORT"
+        id={getValues('id')}
         isOpen={showSendDialog}
-        isLoading={applicationSendMutation.isLoading}
         onClose={closeSendDialog}
-        onSend={onSendApplication}
       />
     </FormProvider>
   );
diff --git a/src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx b/src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx
index 43795eb37..ac78c8f63 100644
--- a/src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx
+++ b/src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx
@@ -1,6 +1,6 @@
-import { useState } from 'react';
+import React, { useState } from 'react';
 import { FieldPath, FormProvider, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
 import {
   Button,
   IconEnvelope,
@@ -9,6 +9,7 @@ import {
   Link,
   StepState,
   Notification,
+  LoadingSpinner,
 } from 'hds-react';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { Box } from '@chakra-ui/react';
@@ -52,6 +53,7 @@ import Attachments from './Attachments';
 import useAttachments from '../application/hooks/useAttachments';
 import { updateTaydennys } from '../application/taydennys/taydennysApi';
 import useUpdateHakemus from '../application/taydennysAndMuutosilmoitusCommon/hooks/useUpdateHakemus';
+import useTaydennysSendNotification from '../application/taydennys/hooks/useTaydennysSendNotification';
 
 type Props = {
   taydennys: Taydennys<JohtoselvitysData>;
@@ -68,6 +70,7 @@ export default function JohtoselvitysTaydennysContainer({
   const { setNotification } = useGlobalNotification();
   const navigateToApplicationView = useNavigateToApplicationView();
   const sendTaydennysMutation = useSendTaydennys();
+  const { showSendSuccess } = useTaydennysSendNotification();
   const [showSendDialog, setShowSendDialog] = useState(false);
   const { data: signedInUser } = usePermissionsForHanke(hankeData.hankeTunnus);
   const { data: originalAttachments } = useAttachments(originalApplication.id);
@@ -271,16 +274,20 @@ export default function JohtoselvitysTaydennysContainer({
   }
 
   function closeSendDialog() {
-    setShowSendDialog(false);
+    if (!sendTaydennysMutation.isLoading) {
+      sendTaydennysMutation.reset();
+      setShowSendDialog(false);
+    }
   }
 
   function sendTaydennys() {
     sendTaydennysMutation.mutate(taydennys.id, {
       onSuccess(data) {
+        showSendSuccess();
+        closeSendDialog();
         navigateToApplicationView(data.id?.toString());
       },
     });
-    closeSendDialog();
   }
 
   function validateStepChange(changeStep: () => void, stepIndex: number) {
@@ -329,7 +336,21 @@ export default function JohtoselvitysTaydennysContainer({
           const disableSendButton = showSendButton && !isContact;
 
           const saveAndQuitIsLoading = hakemusUpdateMutation.isLoading;
-          const saveAndQuitLoadingText = t('common:buttons:savingText');
+          const saveAndQuiteButtonIcon = saveAndQuitIsLoading ? (
+            <LoadingSpinner small />
+          ) : (
+            <IconSaveDiskette />
+          );
+          const saveAndQuitButtonText = saveAndQuitIsLoading
+            ? t('common:buttons:savingText')
+            : t('hankeForm:saveDraftButton');
+
+          const sendIsLoading = sendTaydennysMutation.isLoading;
+          const sendButtonIcon = sendIsLoading ? <LoadingSpinner small /> : <IconEnvelope />;
+          const sendButtonText = sendIsLoading
+            ? t('common:buttons:sendingText')
+            : t('taydennys:buttons:sendTaydennys');
+
           return (
             <FormActions
               activeStepIndex={activeStep}
@@ -342,27 +363,19 @@ export default function JohtoselvitysTaydennysContainer({
                 navigateToApplicationViewOnSuccess
                 buttonVariant="danger"
                 buttonIsLoading={saveAndQuitIsLoading}
-                buttonIsLoadingText={saveAndQuitLoadingText}
+                buttonIsLoadingText={t('common:buttons:savingText')}
               />
               <Button
                 variant="secondary"
-                iconLeft={<IconSaveDiskette aria-hidden="true" />}
+                iconLeft={saveAndQuiteButtonIcon}
                 data-testid="save-form-btn"
                 onClick={handleSaveAndQuit}
-                isLoading={saveAndQuitIsLoading}
-                loadingText={saveAndQuitLoadingText}
               >
-                {t('hankeForm:saveDraftButton')}
+                {saveAndQuitButtonText}
               </Button>
               {showSendButton && (
-                <Button
-                  type="submit"
-                  iconLeft={<IconEnvelope aria-hidden="true" />}
-                  loadingText={t('common:buttons:sendingText')}
-                  isLoading={sendTaydennysMutation.isLoading}
-                  disabled={disableSendButton}
-                >
-                  {t('taydennys:buttons:sendTaydennys')}
+                <Button type="submit" iconLeft={sendButtonIcon} disabled={disableSendButton}>
+                  {sendButtonText}
                 </Button>
               )}
               {disableSendButton && (
@@ -389,13 +402,39 @@ export default function JohtoselvitysTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          sendTaydennysMutation.isError ? (
+            <>
+              {t('taydennys:sendDialog:description')}
+              <Box paddingTop="var(--spacing-s)">
+                <Notification
+                  type="error"
+                  size="small"
+                  label={t('taydennys:notifications:sendErrorLabel')}
+                >
+                  <Trans i18nKey="taydennys:notifications:sendErrorText">
+                    <p>
+                      Lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä
+                      Haitattoman tekniseen tukeen sähköpostiosoitteessa
+                      <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
+                    </p>
+                  </Trans>
+                </Notification>
+              </Box>
+            </>
+          ) : (
+            t('taydennys:sendDialog:description')
+          )
+        }
         showCloseButton
         isOpen={showSendDialog}
         close={closeSendDialog}
         mainAction={sendTaydennys}
         variant="primary"
+        isLoading={sendTaydennysMutation.isLoading}
+        loadingText={t('common:buttons:sendingText')}
         headerIcon={<IconQuestionCircle />}
+        disabled={sendTaydennysMutation.isLoading}
       />
     </FormProvider>
   );
diff --git a/src/domain/kaivuilmoitus/KaivuilmoitusContainer.tsx b/src/domain/kaivuilmoitus/KaivuilmoitusContainer.tsx
index a4043b769..98566406a 100644
--- a/src/domain/kaivuilmoitus/KaivuilmoitusContainer.tsx
+++ b/src/domain/kaivuilmoitus/KaivuilmoitusContainer.tsx
@@ -35,7 +35,6 @@ import {
   KaivuilmoitusCreateData,
   KaivuilmoitusData,
   KaivuilmoitusUpdateData,
-  PaperDecisionReceiver,
 } from '../application/types/application';
 import { useGlobalNotification } from '../../common/components/globalNotification/GlobalNotificationContext';
 import {
@@ -51,7 +50,6 @@ import Areas from './Areas';
 import useNavigateToApplicationView from '../application/hooks/useNavigateToApplicationView';
 import { getJohtoselvitysIdentifiers, isApplicationDraft, isContactIn } from '../application/utils';
 import { usePermissionsForHanke } from '../hanke/hankeUsers/hooks/useUserRightsForHanke';
-import useSendApplication from '../application/hooks/useSendApplication';
 import ApplicationSendDialog from '../application/components/ApplicationSendDialog';
 import HaittojenHallinta from './HaittojenHallinta';
 import FormErrorsNotification from './components/FormErrorsNotification';
@@ -115,7 +113,6 @@ export default function KaivuilmoitusContainer({ hankeData, application }: Reado
     getValues('id'),
   );
 
-  const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false);
   const [showSendDialog, setShowSendDialog] = useState(false);
 
   const {
@@ -209,28 +206,13 @@ export default function KaivuilmoitusContainer({ hankeData, application }: Reado
     }
   }
 
-  const applicationSendMutation = useSendApplication({
-    onSuccess(data) {
-      navigateToApplicationView(data.id?.toString());
-    },
-  });
-
-  async function onSendApplication(pdr: PaperDecisionReceiver | undefined | null) {
-    const data = getValues();
-    applicationSendMutation.mutate({
-      id: data.id as number,
-      paperDecisionReceiver: pdr,
-    });
-    setIsSendButtonDisabled(true);
-    setShowSendDialog(false);
-  }
-
   function openSendDialog() {
     setShowSendDialog(true);
   }
 
-  function closeSendDialog() {
+  function closeSendDialog(id?: number | null) {
     setShowSendDialog(false);
+    navigateToApplicationView(id?.toString());
   }
 
   function saveAndQuit() {
@@ -410,7 +392,7 @@ export default function KaivuilmoitusContainer({ hankeData, application }: Reado
                   type="submit"
                   iconLeft={<IconEnvelope />}
                   loadingText={t('common:buttons:sendingText')}
-                  disabled={disableSendButton || isSendButtonDisabled}
+                  disabled={disableSendButton}
                 >
                   {t('hakemus:buttons:sendApplication')}
                 </Button>
@@ -458,10 +440,9 @@ export default function KaivuilmoitusContainer({ hankeData, application }: Reado
 
       <ApplicationSendDialog
         type="EXCAVATION_NOTIFICATION"
+        id={getValues('id')}
         isOpen={showSendDialog}
-        isLoading={applicationSendMutation.isLoading}
         onClose={closeSendDialog}
-        onSend={onSendApplication}
       />
     </FormProvider>
   );
diff --git a/src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx b/src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx
index 7d75c5f6f..322d18c50 100644
--- a/src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx
+++ b/src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx
@@ -1,12 +1,14 @@
 import { useState } from 'react';
 import { yupResolver } from '@hookform/resolvers/yup';
 import { FieldPath, FormProvider, useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
 import {
   Button,
   IconEnvelope,
   IconQuestionCircle,
   IconSaveDiskette,
+  Link,
+  LoadingSpinner,
   Notification,
   StepState,
 } from 'hds-react';
@@ -51,6 +53,7 @@ import ConfirmationDialog from '../../common/components/HDSConfirmationDialog/Co
 import FormErrorsNotification from '../kaivuilmoitus/components/FormErrorsNotification';
 import { updateTaydennys } from '../application/taydennys/taydennysApi';
 import useUpdateHakemus from '../application/taydennysAndMuutosilmoitusCommon/hooks/useUpdateHakemus';
+import useTaydennysSendNotification from '../application/taydennys/hooks/useTaydennysSendNotification';
 
 type Props = {
   taydennys: Taydennys<KaivuilmoitusData>;
@@ -69,6 +72,7 @@ export default function KaivuilmoitusTaydennysContainer({
   const { data: hankkeenHakemukset } = useApplicationsForHanke(hankeData.hankeTunnus, true);
   const johtoselvitysIds = getJohtoselvitysIdentifiers(hankkeenHakemukset?.applications);
   const sendTaydennysMutation = useSendTaydennys();
+  const { showSendSuccess } = useTaydennysSendNotification();
   const [showSendDialog, setShowSendDialog] = useState(false);
   const { data: signedInUser } = usePermissionsForHanke(hankeData.hankeTunnus);
   const { data: originalAttachments } = useAttachments(originalApplication.id);
@@ -246,16 +250,20 @@ export default function KaivuilmoitusTaydennysContainer({
   }
 
   function closeSendDialog() {
-    setShowSendDialog(false);
+    if (!sendTaydennysMutation.isLoading) {
+      sendTaydennysMutation.reset();
+      setShowSendDialog(false);
+    }
   }
 
   function sendTaydennys() {
     sendTaydennysMutation.mutate(taydennys.id, {
       onSuccess(data) {
+        showSendSuccess();
+        closeSendDialog();
         navigateToApplicationView(data.id?.toString());
       },
     });
-    closeSendDialog();
   }
 
   function validateStepChange(changeStep: () => void, stepIndex: number) {
@@ -309,7 +317,18 @@ export default function KaivuilmoitusTaydennysContainer({
           const disableSendButton = showSendButton && !isContact;
 
           const saveAndQuitIsLoading = hakemusUpdateMutation.isLoading;
-          const saveAndQuitLoadingText = t('common:buttons:savingText');
+          const saveAndQuiteButtonIcon = saveAndQuitIsLoading ? (
+            <LoadingSpinner small />
+          ) : (
+            <IconSaveDiskette />
+          );
+          const saveAndQuitButtonText = saveAndQuitIsLoading
+            ? t('common:buttons:savingText')
+            : t('hankeForm:saveDraftButton');
+
+          const sendIsLoading = sendTaydennysMutation.isLoading;
+          const sendButtonIcon = sendIsLoading ? <LoadingSpinner small /> : <IconEnvelope />;
+          const sendButtonText = t('common:buttons:sendingText');
 
           return (
             <FormActions
@@ -323,26 +342,24 @@ export default function KaivuilmoitusTaydennysContainer({
                 navigateToApplicationViewOnSuccess
                 buttonVariant="danger"
                 buttonIsLoading={saveAndQuitIsLoading}
-                buttonIsLoadingText={saveAndQuitLoadingText}
+                buttonIsLoadingText={saveAndQuitButtonText}
               />
               <Button
                 variant="secondary"
                 onClick={handleSaveAndQuit}
-                iconLeft={<IconSaveDiskette />}
-                isLoading={saveAndQuitIsLoading}
-                loadingText={saveAndQuitLoadingText}
+                iconLeft={saveAndQuiteButtonIcon}
               >
-                {t('hankeForm:saveDraftButton')}
+                {saveAndQuitButtonText}
               </Button>
               {showSendButton && (
                 <Button
                   type="submit"
-                  iconLeft={<IconEnvelope aria-hidden="true" />}
+                  iconLeft={sendButtonIcon}
                   loadingText={t('common:buttons:sendingText')}
                   isLoading={sendTaydennysMutation.isLoading}
                   disabled={disableSendButton}
                 >
-                  {t('taydennys:buttons:sendTaydennys')}
+                  {sendIsLoading ? sendButtonText : t('taydennys:buttons:sendTaydennys')}
                 </Button>
               )}
               {disableSendButton && (
@@ -369,13 +386,39 @@ export default function KaivuilmoitusTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          sendTaydennysMutation.isError ? (
+            <>
+              {t('taydennys:sendDialog:description')}
+              <Box paddingTop="var(--spacing-s)">
+                <Notification
+                  type="error"
+                  size="small"
+                  label={t('taydennys:notifications:sendErrorLabel')}
+                >
+                  <Trans i18nKey="taydennys:notifications:sendErrorText">
+                    <p>
+                      Lähettämisessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä
+                      Haitattoman tekniseen tukeen sähköpostiosoitteessa
+                      <Link href="mailto:haitatontuki@hel.fi">haitatontuki@hel.fi</Link>.
+                    </p>
+                  </Trans>
+                </Notification>
+              </Box>
+            </>
+          ) : (
+            t('taydennys:sendDialog:description')
+          )
+        }
         showCloseButton
         isOpen={showSendDialog}
         close={closeSendDialog}
         mainAction={sendTaydennys}
         variant="primary"
+        isLoading={sendTaydennysMutation.isLoading}
+        loadingText={t('common:buttons:sendingText')}
         headerIcon={<IconQuestionCircle />}
+        disabled={sendTaydennysMutation.isLoading}
       />
     </FormProvider>
   );
diff --git a/src/locales/en.json b/src/locales/en.json
index c36ebd4d5..624b57e3d 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1206,9 +1206,8 @@
       "saveErrorText": "<0>Failed to save the application. Please try again later or contact the Haitaton technical support by email <1>haitatontuki@hel.fi</1>.</0>",
       "sendSuccessLabel": "Application sent",
       "sendErrorLabel": "Sending failed",
-      "sendSuccessText": "The application was sent for processing.",
-      "sendErrorText": "<0>The application was saved as a draft because sending it failed. Please try again later or contact the Haitaton technical support by email <1>haitatontuki@hel.fi.</1></0>",
-      "sendErrorTextNoSave": "<0>Failed to submit the application for processing. Please try again later or contact the Haitaton technical support by email <1>haitatontuki@hel.fi.</1></0>",
+      "sendSuccessText": "The application was sent for processing. [TRANSLATION_PENDING]",
+      "sendErrorText": "<0>Failed to submit the application for processing. Please try again later or contact the Haitaton technical support by email <1>haitatontuki@hel.fi.</1></0>",
       "cancelSuccessLabel": "Application cancelled",
       "cancelSuccessText": "The application has been cancelled",
       "noApplications": "No applications have been added to the project",
diff --git a/src/locales/fi.json b/src/locales/fi.json
index 2c51cdd15..b9192f7c1 100644
--- a/src/locales/fi.json
+++ b/src/locales/fi.json
@@ -1296,9 +1296,8 @@
       "saveErrorText": "<0>Hakemuksen tallentaminen epäonnistui. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi</1>.</0>",
       "sendSuccessLabel": "Hakemus lähetetty",
       "sendErrorLabel": "Lähettäminen epäonnistui",
-      "sendSuccessText": "Hakemus on lähetetty käsiteltäväksi.",
-      "sendErrorText": "<0>Hakemus on tallennettu luonnoksena, koska hakemuksen lähettäminen käsittelyyn epäonnistui. Yritä lähettämistä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi.</1></0>",
-      "sendErrorTextNoSave": "<0>Hakemuksen lähettäminen käsittelyyn epäonnistui. Yritä lähettämistä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi.</1></0>",
+      "sendSuccessText": "Hakemus on lähetetty käsiteltäväksi. Ilmoitus hakemuksesta lähetettiin hakemukseen lisätyille yhteyshenkilöille.",
+      "sendErrorText": "<0>Hakemuksen lähettäminen käsittelyyn epäonnistui. Yritä lähettämistä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>haitatontuki@hel.fi.</1></0>",
       "cancelSuccessLabel": "Hakemus peruttiin",
       "cancelSuccessText": "Hakemus peruttiin onnistuneesti",
       "noApplications": "Hankkeella ei ole lisättyjä hakemuksia",
diff --git a/src/locales/sv.json b/src/locales/sv.json
index 282d5af2d..2485f5baa 100644
--- a/src/locales/sv.json
+++ b/src/locales/sv.json
@@ -1206,9 +1206,8 @@
       "saveErrorText": "<0>Ansökan kunde inte sparas. Försök igen senare eller kontakta Haitatons tekniska support med e-post till <1>haitatontuki@hel.fi</1>.</0>",
       "sendSuccessLabel": "Ansökan har skickats",
       "sendErrorLabel": "Ansökan kunde inte skickas",
-      "sendSuccessText": "Ansökan har skickats för handläggning.",
-      "sendErrorText": "<0>Ansökan har sparats som utkast emedan den inte kunde skickas för handläggning. Försök att skicka den igen senare eller kontakta Haitatons tekniska support med e-post till <1>haitatontuki@hel.fi.</1></0>",
-      "sendErrorTextNoSave": "<0>Det gick inte att skicka in ansökan för behandling. Försök skicka den igen senare eller kontakta Haitatons tekniska support på e-postadressen <1>haitatontuki@hel.fi.</1></0>",
+      "sendSuccessText": "Ansökan har skickats för handläggning. [TRANSLATION_PENDING]",
+      "sendErrorText": "<0>Det gick inte att skicka in ansökan för behandling. Försök skicka den igen senare eller kontakta Haitatons tekniska support på e-postadressen <1>haitatontuki@hel.fi.</1></0>",
       "cancelSuccessLabel": "Ansökan återtogs",
       "cancelSuccessText": "Ansökan kunde återtas",
       "noApplications": "Inga ansökningar har lagts till projektet",


Reviews:
user: login: markohaarni
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T07:37:41Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:34:03Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:34:09Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:34:13Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:34:16Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:16:46Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:19:46Z
user: login: umeetiusbaar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:24:10Z
user: login: markohaarni
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:12:00Z
user: login: markohaarni
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:14:53Z

Review comments:
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: markohaarni
body: `sendTaydennysMutation` has `isError` property which could be used here so there would be no need for separate `isError` variable.
created_at: 2025-03-14T07:19:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,24 +174,40 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.city"
                       label={t('hakemus:sendDialog:city')}
                       required={orderPaperDecisionChecked}
+                      disabled={isSending}
                     />
                   </GridItem>
                 </Grid>
               </Box>
             )}
+            {isError && (
path: src/domain/application/components/ApplicationSendDialog.tsx
user: login: markohaarni
body: Same here as above that mutations `isError` could be used.
created_at: 2025-03-14T07:22:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,13 +406,39 @@ export default function JohtoselvitysTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          isError ? (
path: src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx
user: login: markohaarni
body: Same here with `isError` as above.
created_at: 2025-03-14T07:28:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -369,13 +387,39 @@ export default function KaivuilmoitusTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          isError ? (
path: src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx
user: login: markohaarni
body: Same as above.
created_at: 2025-03-14T07:34:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: umeetiusbaar
body: Thank you for noticing! Changed it.
created_at: 2025-03-18T07:34:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,24 +174,40 @@ const ApplicationSendDialog: React.FC<Props> = ({ type, isOpen, isLoading, onClo
                       name="paperDecisionReceiver.city"
                       label={t('hakemus:sendDialog:city')}
                       required={orderPaperDecisionChecked}
+                      disabled={isSending}
                     />
                   </GridItem>
                 </Grid>
               </Box>
             )}
+            {isError && (
path: src/domain/application/components/ApplicationSendDialog.tsx
user: login: umeetiusbaar
body: Changed it.
created_at: 2025-03-18T07:34:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,13 +406,39 @@ export default function JohtoselvitysTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          isError ? (
path: src/domain/johtoselvitysTaydennys/JohtoselvitysTaydennysContainer.tsx
user: login: umeetiusbaar
body: Changed it.
created_at: 2025-03-18T07:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -369,13 +387,39 @@ export default function KaivuilmoitusTaydennysContainer({
 
       <ConfirmationDialog
         title={t('taydennys:sendDialog:title')}
-        description={t('taydennys:sendDialog:description')}
+        description={
+          isError ? (
path: src/domain/kaivuilmoitusTaydennys/KaivuilmoitusTaydennysContainer.tsx
user: login: umeetiusbaar
body: Changed it.
created_at: 2025-03-18T07:34:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: umeetiusbaar
body: Although, after this change, when there is an error in sending and the user closed the dialog after the error, then when opening the send dialog again the error message is shown right away. How can I reset mutation.isError so that when re-opening the dialog the error is not shown right away?
created_at: 2025-03-18T10:16:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: umeetiusbaar
body: Found it. Mutation has reset() function that should be called when closing the dialog.
created_at: 2025-03-18T10:19:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: umeetiusbaar
body: And it seems that I could also get rid off [isSending, setIsSending] and replace it with mutation.isLoading?
created_at: 2025-03-18T10:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,13 +98,39 @@ export default function useSendTaydennys(
   const sendTaydennysDialog = (
     <ConfirmationDialog
       title={t('taydennys:sendDialog:title')}
-      description={t('taydennys:sendDialog:description')}
+      description={
+        isError ? (
path: src/domain/application/applicationView/hooks/useSendTaydennys.tsx
user: login: markohaarni
body: Yes, seems like a good change!
created_at: 2025-03-19T07:12:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-19T08:18:26Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=City-of-Helsinki_haitaton-ui&pullRequest=1119) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [25.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=City-of-Helsinki_haitaton-ui&pullRequest=1119&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=City-of-Helsinki_haitaton-ui&pullRequest=1119)


author_association: NONE