From bd905852622bf3c8e7d97e3dbb6ba0bd13dd484f Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Tue, 29 Apr 2025 15:48:36 +0300 Subject: [PATCH 1/2] add ConfirmationModal component and integrate it into ActionButtons for discard confirmation --- .../components/Modal/ConfirmationModal.scss | 84 +++++++++++++++++++ .../components/Modal/ConfirmationModal.tsx | 77 +++++++++++++++++ .../utils/i18n/resources/en/common.json | 4 +- .../utils/i18n/resources/es/common.json | 4 +- .../utils/i18n/resources/fr/common.json | 4 +- .../src/pages/Reports/ReportDetailPage.tsx | 35 +++++++- .../Reports/components/ActionButtons.tsx | 65 ++++++++++---- 7 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 frontend/src/common/components/Modal/ConfirmationModal.scss create mode 100644 frontend/src/common/components/Modal/ConfirmationModal.tsx diff --git a/frontend/src/common/components/Modal/ConfirmationModal.scss b/frontend/src/common/components/Modal/ConfirmationModal.scss new file mode 100644 index 00000000..6f7b3cbc --- /dev/null +++ b/frontend/src/common/components/Modal/ConfirmationModal.scss @@ -0,0 +1,84 @@ +.confirmation-modal { + --height: auto; + --border-radius: 16px 16px 0 0; + --box-shadow: none; + align-items: flex-end; + --background: #ffffff; + + &__container { + padding: 24px; + width: 100%; + } + + &__title { + font-family: 'Inter', sans-serif; + font-size: 22px; + font-weight: 600; + color: #313e4c; + margin: 0 0 16px; + text-align: left; + } + + &__message { + font-family: 'Inter', sans-serif; + font-size: 16px; + font-weight: 400; + color: #313e4c; + margin: 0 0 24px; + text-align: left; + line-height: 1.5; + } + + &__actions { + display: flex; + gap: 16px; + } + + &__button { + flex: 1; + min-height: 56px; + margin: 0; + --border-radius: 12px; + font-size: 20px; + font-weight: 600; + text-transform: none; + + &--cancel { + --background: #ffffff; + --background-hover: rgba(67, 95, 240, 0.04); + --background-activated: rgba(67, 95, 240, 0.08); + --color: #435ff0; /* Blue color for No button */ + --border-color: #435ff0; + --border-width: 1px; + --border-style: solid; + --box-shadow: none; + } + + &--confirm { + --background: #ffffff; /* White background for Yes button */ + --background-hover: rgba(175, 27, 63, 0.04); + --background-activated: rgba(175, 27, 63, 0.08); + --color: #af1b3f; /* Red color for Yes button */ + --border-color: #af1b3f; /* Red border for Yes button */ + --border-width: 1px; + --border-style: solid; + } + } + + // Media queries for larger screens + @media screen and (min-width: 768px) { + &__container { + max-width: 500px; + margin: 0 auto; + } + } + + // Handle the pull indicator + &::part(handle) { + background: #D9D9D9; + width: 36px; + height: 4px; + border-radius: 2px; + margin-top: 12px; + } +} \ No newline at end of file diff --git a/frontend/src/common/components/Modal/ConfirmationModal.tsx b/frontend/src/common/components/Modal/ConfirmationModal.tsx new file mode 100644 index 00000000..88956d7f --- /dev/null +++ b/frontend/src/common/components/Modal/ConfirmationModal.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { IonButton, IonModal } from '@ionic/react'; +import './ConfirmationModal.scss'; + +interface ConfirmationModalProps { + isOpen: boolean; + title: string; + message: string; + confirmText: string; + cancelText: string; + onConfirm: () => void; + onCancel: () => void; + itemName?: string; + testid?: string; +} + +/** + * A reusable confirmation modal component that presents as a bottom sheet + * Used for confirming destructive actions like deleting or discarding items + */ +const ConfirmationModal: React.FC = ({ + isOpen, + title, + message, + confirmText, + cancelText, + onConfirm, + onCancel, + itemName, + testid = 'confirmation-modal', +}) => { + // Replace placeholder with actual item name if provided + const formattedMessage = itemName + ? message.replace('{itemName}', `"${itemName}"`) + : message; + + return ( + +
+

{title}

+ +

+ {formattedMessage} +

+ +
+ + {cancelText} + + + + {confirmText} + +
+
+
+ ); +}; + +export default ConfirmationModal; \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/en/common.json b/frontend/src/common/utils/i18n/resources/en/common.json index 8407824e..df17ccd5 100644 --- a/frontend/src/common/utils/i18n/resources/en/common.json +++ b/frontend/src/common/utils/i18n/resources/en/common.json @@ -73,10 +73,10 @@ "loading": { "report": "Loading report..." }, - "no": "no", + "no": "No", "updated": "updated", "welcome": "Welcome", - "yes": "yes", + "yes": "Yes", "pages": { "chat": { "title": "AI Assistant" diff --git a/frontend/src/common/utils/i18n/resources/es/common.json b/frontend/src/common/utils/i18n/resources/es/common.json index edb1338a..eea19df0 100644 --- a/frontend/src/common/utils/i18n/resources/es/common.json +++ b/frontend/src/common/utils/i18n/resources/es/common.json @@ -70,10 +70,10 @@ "loading": { "report": "Cargando informe..." }, - "no": "no", + "no": "No", "updated": "actualizado", "welcome": "Bienvenido", - "yes": "sí", + "yes": "Sí", "pages": { "chat": { "title": "Asistente IA" diff --git a/frontend/src/common/utils/i18n/resources/fr/common.json b/frontend/src/common/utils/i18n/resources/fr/common.json index fd2bf766..f59e009d 100644 --- a/frontend/src/common/utils/i18n/resources/fr/common.json +++ b/frontend/src/common/utils/i18n/resources/fr/common.json @@ -70,10 +70,10 @@ "loading": { "report": "Chargement du rapport..." }, - "no": "non", + "no": "Non", "updated": "mis à jour", "welcome": "Bienvenu", - "yes": "oui", + "yes": "Oui", "pages": { "chat": { "title": "Assistant IA" diff --git a/frontend/src/pages/Reports/ReportDetailPage.tsx b/frontend/src/pages/Reports/ReportDetailPage.tsx index c6e95ae0..5d04f664 100644 --- a/frontend/src/pages/Reports/ReportDetailPage.tsx +++ b/frontend/src/pages/Reports/ReportDetailPage.tsx @@ -7,6 +7,7 @@ import axios from 'axios'; import { MedicalReport } from '../../common/models/medicalReport'; import { useTranslation } from 'react-i18next'; import { getAuthConfig } from 'common/api/reportService'; +import { useToasts } from 'common/hooks/useToasts'; // Import components import ReportHeader from './components/ReportHeader'; @@ -35,6 +36,7 @@ const ReportDetailPage: React.FC = () => { const { reportId } = useParams<{ reportId: string }>(); const history = useHistory(); const { t } = useTranslation(); + const { createToast } = useToasts(); // Fetch report data using react-query const { data, isLoading, error } = useQuery({ @@ -85,8 +87,31 @@ const ReportDetailPage: React.FC = () => { // Handle action buttons const handleDiscard = async () => { - await axios.delete(`${API_URL}/api/reports/${reportId}`, await getAuthConfig()); - history.push('/tabs/home'); + try { + await axios.delete(`${API_URL}/api/reports/${reportId}`, await getAuthConfig()); + + // Show toast notification + createToast({ + message: t('report.discard.success', { + ns: 'reportDetail', + defaultValue: 'Report deleted successfully' + }), + duration: 2000, + }); + + // Navigate back + history.push('/tabs/home'); + } catch (error) { + console.error('Error discarding report:', error); + createToast({ + message: t('report.discard.error', { + ns: 'reportDetail', + defaultValue: 'Failed to delete report' + }), + duration: 2000, + color: 'danger', + }); + } }; const handleNewUpload = () => { @@ -114,7 +139,11 @@ const ReportDetailPage: React.FC = () => { {/* Action buttons at the bottom */} - + ); diff --git a/frontend/src/pages/Reports/components/ActionButtons.tsx b/frontend/src/pages/Reports/components/ActionButtons.tsx index 0e4c93db..93eaf10b 100644 --- a/frontend/src/pages/Reports/components/ActionButtons.tsx +++ b/frontend/src/pages/Reports/components/ActionButtons.tsx @@ -1,29 +1,62 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import ConfirmationModal from '../../../common/components/Modal/ConfirmationModal'; interface ActionButtonsProps { onDiscard: () => void; onNewUpload: () => void; + reportTitle?: string; } -const ActionButtons: React.FC = ({ onDiscard, onNewUpload }) => { +const ActionButtons: React.FC = ({ onDiscard, onNewUpload, reportTitle = "" }) => { const { t } = useTranslation(); + const [showConfirmDiscard, setShowConfirmDiscard] = useState(false); + + const handleDiscardClick = () => { + setShowConfirmDiscard(true); + }; + + const handleConfirmDiscard = () => { + setShowConfirmDiscard(false); + onDiscard(); + }; + + const handleCancelDiscard = () => { + setShowConfirmDiscard(false); + }; return ( -
- - -
+ <> +
+ + +
+ + + ); }; From 92746cbc8718009c5683a1f8905681ab143a1c1c Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Tue, 29 Apr 2025 16:06:43 +0300 Subject: [PATCH 2/2] feat: add confirmation modal for new upload and enhance discard functionality --- .../src/pages/Reports/ReportDetailPage.tsx | 21 +++--- .../Reports/components/ActionButtons.tsx | 74 +++++++++++++++++-- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/Reports/ReportDetailPage.tsx b/frontend/src/pages/Reports/ReportDetailPage.tsx index 5d04f664..d30f0dbe 100644 --- a/frontend/src/pages/Reports/ReportDetailPage.tsx +++ b/frontend/src/pages/Reports/ReportDetailPage.tsx @@ -89,24 +89,24 @@ const ReportDetailPage: React.FC = () => { const handleDiscard = async () => { try { await axios.delete(`${API_URL}/api/reports/${reportId}`, await getAuthConfig()); - + // Show toast notification createToast({ - message: t('report.discard.success', { - ns: 'reportDetail', - defaultValue: 'Report deleted successfully' + message: t('report.discard.success', { + ns: 'reportDetail', + defaultValue: 'Report deleted successfully', }), duration: 2000, }); - + // Navigate back history.push('/tabs/home'); } catch (error) { console.error('Error discarding report:', error); createToast({ - message: t('report.discard.error', { - ns: 'reportDetail', - defaultValue: 'Failed to delete report' + message: t('report.discard.error', { + ns: 'reportDetail', + defaultValue: 'Failed to delete report', }), duration: 2000, color: 'danger', @@ -139,10 +139,11 @@ const ReportDetailPage: React.FC = () => { {/* Action buttons at the bottom */} - diff --git a/frontend/src/pages/Reports/components/ActionButtons.tsx b/frontend/src/pages/Reports/components/ActionButtons.tsx index 93eaf10b..1141097f 100644 --- a/frontend/src/pages/Reports/components/ActionButtons.tsx +++ b/frontend/src/pages/Reports/components/ActionButtons.tsx @@ -1,16 +1,28 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import axios from 'axios'; +import { getAuthConfig } from 'common/api/reportService'; import ConfirmationModal from '../../../common/components/Modal/ConfirmationModal'; +const API_URL = import.meta.env.VITE_BASE_URL_API || ''; + interface ActionButtonsProps { onDiscard: () => void; onNewUpload: () => void; reportTitle?: string; + reportId?: string; } -const ActionButtons: React.FC = ({ onDiscard, onNewUpload, reportTitle = "" }) => { +const ActionButtons: React.FC = ({ + onDiscard, + onNewUpload, + reportTitle = '', + reportId, +}) => { const { t } = useTranslation(); const [showConfirmDiscard, setShowConfirmDiscard] = useState(false); + const [showConfirmNewUpload, setShowConfirmNewUpload] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const handleDiscardClick = () => { setShowConfirmDiscard(true); @@ -25,29 +37,61 @@ const ActionButtons: React.FC = ({ onDiscard, onNewUpload, r setShowConfirmDiscard(false); }; + const handleNewUploadClick = () => { + setShowConfirmNewUpload(true); + }; + + const handleConfirmNewUpload = async () => { + setShowConfirmNewUpload(false); + + // If we have a reportId, delete the current report before going to upload screen + if (reportId) { + try { + setIsDeleting(true); + await axios.delete(`${API_URL}/api/reports/${reportId}`, await getAuthConfig()); + } catch (error) { + console.error('Error deleting report before new upload:', error); + } finally { + setIsDeleting(false); + // Even if delete failed, still go to upload screen + onNewUpload(); + } + } else { + // If no reportId, just navigate to upload + onNewUpload(); + } + }; + + const handleCancelNewUpload = () => { + setShowConfirmNewUpload(false); + }; + return ( <>
- = ({ onDiscard, onNewUpload, r itemName={reportTitle} testid="discard-report-confirmation" /> + + ); };