From 40316ae361102ad2683888f15b44e8fdf1d7c0e5 Mon Sep 17 00:00:00 2001 From: Guts088737 Date: Wed, 9 Apr 2025 19:30:47 +0100 Subject: [PATCH 1/2] add send email (registration); fix dataofbirthpicker issues; fix errors with pop-up when repeat to click the submit button; add profession selections; fix email or password error messages --- src/actions/email/sendInvite.ts | 31 +++++++++- src/actions/register/registerActions.ts | 31 ++++++---- src/views/ClinicianSearch.tsx | 50 +++++++++-------- src/views/DateOfBirthPicker.tsx | 5 +- src/views/Login.tsx | 11 ++-- src/views/RegisterClinician.tsx | 48 ++++++++++------ src/views/RegisterPatient.tsx | 24 ++++++-- src/views/RegisterResearcher.tsx | 31 +++++++--- src/views/RegisterUser.tsx | 75 +++++++++++++++---------- src/views/SavedClinicians.tsx | 14 ++--- 10 files changed, 209 insertions(+), 111 deletions(-) diff --git a/src/actions/email/sendInvite.ts b/src/actions/email/sendInvite.ts index a180e11..3c969a8 100644 --- a/src/actions/email/sendInvite.ts +++ b/src/actions/email/sendInvite.ts @@ -5,7 +5,6 @@ import sgMail from '@sendgrid/mail'; import { prisma } from '@/prisma/client' - export interface User { firstName: string lastName: string @@ -21,10 +20,9 @@ const getPatientName = async (id: string): Promise => { }) } - - sgMail.setApiKey(process.env.SENDGRID_API_KEY!); +// Send invite email after login (with uid) export async function sendInviteEmail(name: string, email: string, patientId: string) { const patient = await getPatientName(patientId); const patientName = patient ? `${patient.firstName} ${patient.lastName}` : ''; @@ -53,3 +51,30 @@ return { success: true }; return { success: false, error: error.message }; } } + +// Send invite email before login (without uid) +export async function sendInviteEmailDuringRegistration(clinicianName: string, email: string) { + try { + const msg = { + to: email, + from: process.env.SENDGRID_SENDER_EMAIL!, + subject: 'You\'ve been invited to join our platform!', + html: ` +

Dear ${clinicianName},

+

A new patient would like to share their symptoms data with you on our platform.

+

Register your account to view their spidergrams and track their data.

+

Here is a link to our website: https://team3.uksouth.cloudapp.azure.com

+

Kind regards,

+

The Spider team

+ `, + }; + + await sgMail.send(msg); + + return { success: true }; + } catch (error: any) { + console.error('[Invite Email Error]', error); + + return { success: false, error: error.message }; + } +} diff --git a/src/actions/register/registerActions.ts b/src/actions/register/registerActions.ts index 0412ba1..95e54d3 100644 --- a/src/actions/register/registerActions.ts +++ b/src/actions/register/registerActions.ts @@ -1,3 +1,4 @@ + 'use server'; import { revalidatePath } from 'next/cache'; @@ -6,7 +7,6 @@ import { hash } from 'bcryptjs'; import { prisma } from '@/prisma/client'; - export interface Clinician { id: string;firstName: string;lastName: string;institution: string;email: string;} interface DataPrivacyFormData {researchConsent: boolean;clinicianAccess: boolean;selectedClinicians: Clinician[];} @@ -15,7 +15,6 @@ export interface RegisterUserData { firstName: string; lastName: string; email: export interface RegisterResult { success: boolean; userId?: string; error?: string; } - export async function registerUser(data: RegisterUserData): Promise { try { if (!data.email || !data.password || !data.firstName || !data.lastName) { @@ -43,7 +42,8 @@ export async function registerUser(data: RegisterUserData): Promise 0) { + const relationshipStatus = isPatient ? 'PENDING' : 'CONNECTED'; + // Create an array of clinician relationship objects - const clinicianRelationships = data.selectedClinicians.map((clinician) => ({ patient: { connect: { id: userId } }, clinician: { connect: { id: clinician.id } }, agreedToShareData: true, status: 'CONNECTED', })); + const clinicianRelationships = data.selectedClinicians.map((clinician) => ({ patient: { connect: { id: userId } }, clinician: { connect: { id: clinician.id } }, agreedToShareData: true, status: relationshipStatus, })); await prisma.$transaction( clinicianRelationships.map((relationship) => prisma.clinicianPatient.upsert({ where: { patientId_clinicianId: {patientId: userId, clinicianId: relationship.clinician.connect!.id, },}, - update: { agreedToShareData: relationship.agreedToShareData, status: 'CONNECTED',}, - create: { patient: { connect: { id: userId } }, clinician: { connect: { id: relationship.clinician.connect!.id } }, agreedToShareData: relationship.agreedToShareData, status: 'CONNECTED',}, + update: { agreedToShareData: relationship.agreedToShareData, status: relationshipStatus,}, + create: { patient: { connect: { id: userId } }, clinician: { connect: { id: relationship.clinician.connect!.id } }, agreedToShareData: relationship.agreedToShareData, status: relationshipStatus,}, }) ) ); @@ -97,7 +106,6 @@ return { success: false, error: 'Failed to save data privacy preferences. Please } } - export async function completeRegistration( userId: string, data: DataPrivacyFormData, accountType: string) { try { const result = await saveDataPrivacyPreferences(userId, data); @@ -111,7 +119,10 @@ export async function completeRegistration( userId: string, data: DataPrivacyFor // upadate status or not based on accounttype if (accountType === 'Clinician') { - await prisma.user.update({ where: { id: userId }, data: { status: 'ACTIVE', },}); + await prisma.user.update({ + where: { id: userId }, + data: { status: 'ACTIVE', }, + }); } else if (accountType === 'Researcher') { } @@ -129,7 +140,6 @@ return { } } - export async function searchClinicians(searchParams: Record) { try { const { firstName, lastName, institution, email } = searchParams; @@ -181,3 +191,4 @@ return { emailExists: false, phoneExists: false, registrationNumberExists: false } } + diff --git a/src/views/ClinicianSearch.tsx b/src/views/ClinicianSearch.tsx index 91fbf03..869fda8 100644 --- a/src/views/ClinicianSearch.tsx +++ b/src/views/ClinicianSearch.tsx @@ -1,8 +1,8 @@ + 'use client' import React, { useState, useRef, useEffect } from 'react' -import Image from 'next/image' import { Box, @@ -16,12 +16,14 @@ import { DialogActions, IconButton } from '@mui/material' + import { alpha } from '@mui/material/styles' import { toast } from 'react-toastify' import { searchClinicians } from '@/actions/register/registerActions' -import { sendInvitation } from '@/actions/patientSettings/userActions' + +import { sendInviteEmailDuringRegistration } from '@/actions/email/sendInvite'; export interface Clinician { id: string @@ -74,7 +76,6 @@ export const ClinicianSearch = ({ onSaveClinician, savedClinicians }: ClinicianS return () => observer.disconnect() }, []) - const handleOpenInviteModal = () => { setInviteModalOpen(true) } @@ -85,24 +86,28 @@ export const ClinicianSearch = ({ onSaveClinician, savedClinicians }: ClinicianS const handleSendInvitation = async () => { if (!inviteEmail) { - toast?.error("Please enter the clinician's email") || console.error("Please enter the clinician's email") + toast?.error("Please enter the clinician's email"); -return +return; } - + try { - const invite = await sendInvitation(inviteEmail, inviteMessage) - - if (!invite.success) { - throw new Error((invite as any).message || 'Failed to send the invitation') + const name = searchFirstName + ? `${searchFirstName} ${searchLastName || ''}`.trim() + : 'Clinician'; + + const invitation = await sendInviteEmailDuringRegistration(name, inviteEmail); + + if (invitation.success) { + toast?.success('Invitation sent successfully'); + handleCloseInviteModal(); + setInviteEmail(''); + } else { + throw new Error(invitation.error || 'Failed to send the invitation'); } - - toast?.success('Invitation sent successfully') || console.log('Invitation sent successfully') - handleCloseInviteModal() } catch (error) { - console.error('Failed to send invitation:', error) - toast?.error((error as any).message || 'Failed to send the invitation') - console.error((error as any).message || 'Failed to send the invitation') + console.error('Failed to send invitation:', error); + toast?.error((error as any).message || 'Failed to send the invitation'); } } @@ -199,13 +204,7 @@ return })} onClick={() => !alreadySaved && handleSelectClinician(clinician)} > - Hospital Logo + ({ fontWeight: 'bold', fontSize: '14px', color: theme.palette.text.primary })}> {clinician.firstName} {clinician.lastName} @@ -459,3 +458,8 @@ return ) } + + + + + diff --git a/src/views/DateOfBirthPicker.tsx b/src/views/DateOfBirthPicker.tsx index 431c0fc..2578f8b 100644 --- a/src/views/DateOfBirthPicker.tsx +++ b/src/views/DateOfBirthPicker.tsx @@ -48,7 +48,10 @@ export const DateOfBirthPicker = ({ error: error, helperText: helperText, placeholder: 'dd/mm/yyyy', - inputProps: { 'aria-label': label } + inputProps: { + readOnly: true, + 'aria-label': label + } } }} format='dd/MM/yyyy' diff --git a/src/views/Login.tsx b/src/views/Login.tsx index 71f7e74..568f484 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -107,11 +107,6 @@ const LoginV2 = ({ mode }: { mode: string }) => {
{`Log in`} - {error && ( - - {error} - - )}
{ Log In + {error && ( + + {error} + + )} + Don't have an account? diff --git a/src/views/RegisterClinician.tsx b/src/views/RegisterClinician.tsx index 8334242..9ab3202 100644 --- a/src/views/RegisterClinician.tsx +++ b/src/views/RegisterClinician.tsx @@ -1,3 +1,4 @@ + 'use client' import React, { useState, useEffect } from 'react' @@ -49,7 +50,7 @@ export const ClinicianRegister: React.FC = ({ onBack, ac } }, []) - // Handle form submission + // handleSubmit const handleSubmit = async () => { try { setIsLoading(true) @@ -73,11 +74,17 @@ export const ClinicianRegister: React.FC = ({ onBack, ac }) if (!registerResult.success) { - throw new Error(registerResult.error || 'Failed to register user') + setError(registerResult.error || 'Failed to register user'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } - + if (!registerResult.userId) { - throw new Error('User ID is missing after successful registration') + setError('User ID is missing after successful registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } const newUserId: string = registerResult.userId @@ -89,7 +96,10 @@ export const ClinicianRegister: React.FC = ({ onBack, ac ) if (!completionResult.success) { - throw new Error(completionResult.error || 'Failed to complete registration') + setError(completionResult.error || 'Failed to complete registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } } else { const completionResult = await completeRegistration( @@ -99,7 +109,10 @@ export const ClinicianRegister: React.FC = ({ onBack, ac ) if (!completionResult.success) { - throw new Error(completionResult.error || 'Failed to complete registration') + setError(completionResult.error || 'Failed to complete registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } } @@ -122,22 +135,21 @@ export const ClinicianRegister: React.FC = ({ onBack, ac return ( {/* Error dialog */} - setOpenErrorDialog(false)} aria-labelledby='error-dialog-title'> + setOpenErrorDialog(false)} aria-labelledby='error-dialog-title' maxWidth="sm" fullWidth > Error {' '} {error} - {' '} - - + + {/* Success dialog */} @@ -145,6 +157,8 @@ export const ClinicianRegister: React.FC = ({ onBack, ac open={openSuccessDialog} onClose={() => setOpenSuccessDialog(false)} aria-labelledby='success-dialog-title' + maxWidth="sm" + fullWidth > Success @@ -210,3 +224,5 @@ export const ClinicianRegister: React.FC = ({ onBack, ac ) } + + diff --git a/src/views/RegisterPatient.tsx b/src/views/RegisterPatient.tsx index 0aab896..4a08049 100644 --- a/src/views/RegisterPatient.tsx +++ b/src/views/RegisterPatient.tsx @@ -1,3 +1,4 @@ + 'use client' import React, { useState, useEffect } from 'react' @@ -88,14 +89,21 @@ export const PatientRegister: React.FC = ({ onBack, accoun console.log('Register Result:', registerResult) if (!registerResult.success) { - throw new Error(registerResult.error || 'Failed to register user') + setError(registerResult.error || 'Failed to register user'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } - + if (!registerResult.userId) { - throw new Error('User ID is missing after successful registration') - } - finalUserId = registerResult.userId + setError('User ID is missing after successful registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; + } + + finalUserId = registerResult.userId; } // Complete registration with privacy settings @@ -133,7 +141,7 @@ export const PatientRegister: React.FC = ({ onBack, accoun return ( {/* Error and Success dialog */} - setOpenErrorDialog(false)} aria-labelledby='error-dialog-title'> + setOpenErrorDialog(false)} aria-labelledby='error-dialog-title' maxWidth="sm" fullWidth > Error {error} @@ -153,6 +161,8 @@ export const PatientRegister: React.FC = ({ onBack, accoun open={openSuccessDialog} onClose={() => setOpenSuccessDialog(false)} aria-labelledby='success-dialog-title' + maxWidth="sm" + fullWidth > Success @@ -259,3 +269,5 @@ export const PatientRegister: React.FC = ({ onBack, accoun ) } + + diff --git a/src/views/RegisterResearcher.tsx b/src/views/RegisterResearcher.tsx index 5e73b5b..0cb0aa7 100644 --- a/src/views/RegisterResearcher.tsx +++ b/src/views/RegisterResearcher.tsx @@ -1,3 +1,4 @@ + 'use client' import React, { useState, useEffect } from 'react' @@ -132,11 +133,17 @@ export const ResearcherRegister: React.FC = ({ onBack, }) if (!registerResult.success) { - throw new Error(registerResult.error || 'Failed to register user') + setError(registerResult.error || 'Failed to register user'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } - + if (!registerResult.userId) { - throw new Error('User ID is missing after successful registration') + setError('User ID is missing after successful registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } newUserId = registerResult.userId @@ -147,9 +154,12 @@ export const ResearcherRegister: React.FC = ({ onBack, { researchConsent: false, clinicianAccess: false, selectedClinicians: [] }, accountType ) - + if (!completionResult.success) { - throw new Error(completionResult.error || 'Failed to complete registration') + setError(completionResult.error || 'Failed to complete registration'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } const formDataApp = new FormData() @@ -186,7 +196,10 @@ export const ResearcherRegister: React.FC = ({ onBack, const applicationSuccess = await createApplication(formDataApp, newUserId) if (!applicationSuccess) { - throw new Error('Application creation failed') + setError('Application creation failed'); + setOpenErrorDialog(true); + setIsLoading(false); + return; } setSuccess('Congratulations! Your account has been created successfully!') @@ -208,7 +221,7 @@ export const ResearcherRegister: React.FC = ({ onBack, return ( {/* Error and success notifications */} - setOpenErrorDialog(false)} aria-labelledby='error-dialog-title'> + setOpenErrorDialog(false)} aria-labelledby='error-dialog-title' maxWidth="sm" fullWidth > Error {' '} @@ -229,6 +242,8 @@ export const ResearcherRegister: React.FC = ({ onBack, open={openSuccessDialog} onClose={() => setOpenSuccessDialog(false)} aria-labelledby='success-dialog-title' + maxWidth="sm" + fullWidth > Success @@ -305,3 +320,5 @@ export const ResearcherRegister: React.FC = ({ onBack, ) } + + diff --git a/src/views/RegisterUser.tsx b/src/views/RegisterUser.tsx index e3b5041..98504b5 100644 --- a/src/views/RegisterUser.tsx +++ b/src/views/RegisterUser.tsx @@ -329,10 +329,8 @@ return parsed?.number || phone.trim().replace(/\s/g, '') } if (shouldValidateField('hospitalNumber') && accountType === 'patient') { - if (!formData.hospitalNumber?.trim()) { - errors.hospitalNumber = 'The hospital number is required'; - } else if (!validateHospitalNumber(formData.hospitalNumber)) { - errors.hospitalNumber = 'Please enter the valid format.)'; + if (formData.hospitalNumber?.trim() && !validateHospitalNumber(formData.hospitalNumber)) { + errors.hospitalNumber = 'Please enter the valid format.'; } else if (errors.hospitalNumber && !errors.hospitalNumber.includes('already exists')) { errors.hospitalNumber = ''; } @@ -360,7 +358,7 @@ return parsed?.number || phone.trim().replace(/\s/g, '') } if (shouldValidateField('profession') && accountType === 'clinician') { - errors.profession = !formData.profession?.trim() ? 'Profession is required' : '' + errors.profession = !formData.profession?.trim() ? 'Please select a profession' : '' isValid = isValid && !errors.profession } else { errors.profession = '' @@ -473,10 +471,8 @@ return parsed?.number || phone.trim().replace(/\s/g, '') } if (fieldName === 'hospitalNumber' && accountType === 'patient') { - if (!value.trim()) { - newErrors.hospitalNumber = 'The hospital number is required'; - } else if (!validateHospitalNumber(value)) { - newErrors.hospitalNumber = 'Please enter the valid format)'; + if (value.trim() && !validateHospitalNumber(value)) { + newErrors.hospitalNumber = 'Please enter the valid format.'; } else if (newErrors.hospitalNumber && !newErrors.hospitalNumber.includes('already exists')) { newErrors.hospitalNumber = ''; } @@ -884,8 +880,8 @@ return parsed?.number || phone.trim().replace(/\s/g, '') {accountType === 'patient' && ( <> - Institution ) => + handleInputChange({ + target: { name: 'profession', value: event.target.value } + } as React.ChangeEvent) + } + onBlur={() => { + setTouchedFields(prev => ({ ...prev, profession: true })) + validateForm('profession') + setIsProfessionFocused(!!formData.profession) + }} + onOpen={() => setIsProfessionFocused(true)} + onClose={() => setIsProfessionFocused(!!formData.profession)} + error={!!formErrors.profession} + > + Rheumatologist + General Practitioner + Geneticist + Paediatrician + Physiotherapist + Orthopaedic Consultant + Other + + {formErrors.profession && ( + + {formErrors.profession} + + )} + )} @@ -1010,6 +1022,7 @@ return parsed?.number || phone.trim().replace(/\s/g, '') Institution {formErrors.institution && ( - + {formErrors.institution} )} diff --git a/src/views/SavedClinicians.tsx b/src/views/SavedClinicians.tsx index 464976d..6e82d43 100644 --- a/src/views/SavedClinicians.tsx +++ b/src/views/SavedClinicians.tsx @@ -1,10 +1,10 @@ + 'use client' import React from 'react' -import Image from 'next/image' - import { Box, Typography, IconButton, Card, CardContent } from '@mui/material' + import { alpha } from '@mui/material/styles' import type { Clinician } from './ClinicianSearch' @@ -48,13 +48,7 @@ export const SavedClinicians: React.FC = ({ clinicians, on })} > - Hospital Logo + ({ fontWeight: 'bold', fontSize: '14px', color: theme.palette.text.primary })}> {clinician.firstName} {clinician.lastName} @@ -86,3 +80,5 @@ export const SavedClinicians: React.FC = ({ clinicians, on ) } + + From 71609fe81ee2876b42a079324fe158e85b4b0df6 Mon Sep 17 00:00:00 2001 From: Guts088737 Date: Wed, 9 Apr 2025 19:34:52 +0100 Subject: [PATCH 2/2] add send email (registration); fix dataofbirthpicker issues; fix errors with pop-up when repeat to click the submit button; add profession selections; fix email or password error messages --- src/views/RegisterClinician.tsx | 12 ++++++++---- src/views/RegisterPatient.tsx | 6 ++++-- src/views/RegisterResearcher.tsx | 12 ++++++++---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/views/RegisterClinician.tsx b/src/views/RegisterClinician.tsx index 9ab3202..d576072 100644 --- a/src/views/RegisterClinician.tsx +++ b/src/views/RegisterClinician.tsx @@ -77,14 +77,16 @@ export const ClinicianRegister: React.FC = ({ onBack, ac setError(registerResult.error || 'Failed to register user'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } if (!registerResult.userId) { setError('User ID is missing after successful registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } const newUserId: string = registerResult.userId @@ -99,7 +101,8 @@ export const ClinicianRegister: React.FC = ({ onBack, ac setError(completionResult.error || 'Failed to complete registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } } else { const completionResult = await completeRegistration( @@ -112,7 +115,8 @@ export const ClinicianRegister: React.FC = ({ onBack, ac setError(completionResult.error || 'Failed to complete registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } } diff --git a/src/views/RegisterPatient.tsx b/src/views/RegisterPatient.tsx index 4a08049..1d69e4a 100644 --- a/src/views/RegisterPatient.tsx +++ b/src/views/RegisterPatient.tsx @@ -92,7 +92,8 @@ export const PatientRegister: React.FC = ({ onBack, accoun setError(registerResult.error || 'Failed to register user'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } if (!registerResult.userId) { @@ -100,7 +101,8 @@ export const PatientRegister: React.FC = ({ onBack, accoun setError('User ID is missing after successful registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } finalUserId = registerResult.userId; diff --git a/src/views/RegisterResearcher.tsx b/src/views/RegisterResearcher.tsx index 0cb0aa7..cbf6486 100644 --- a/src/views/RegisterResearcher.tsx +++ b/src/views/RegisterResearcher.tsx @@ -136,14 +136,16 @@ export const ResearcherRegister: React.FC = ({ onBack, setError(registerResult.error || 'Failed to register user'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } if (!registerResult.userId) { setError('User ID is missing after successful registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } newUserId = registerResult.userId @@ -159,7 +161,8 @@ export const ResearcherRegister: React.FC = ({ onBack, setError(completionResult.error || 'Failed to complete registration'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } const formDataApp = new FormData() @@ -199,7 +202,8 @@ export const ResearcherRegister: React.FC = ({ onBack, setError('Application creation failed'); setOpenErrorDialog(true); setIsLoading(false); - return; + +return; } setSuccess('Congratulations! Your account has been created successfully!')