Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/actions/email/sendInvite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import sgMail from '@sendgrid/mail';
import { prisma } from '@/prisma/client'



export interface User {
firstName: string
lastName: string
Expand All @@ -21,10 +20,9 @@ const getPatientName = async (id: string): Promise<User | null> => {
})
}



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}` : '';
Expand Down Expand Up @@ -53,3 +51,32 @@ 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: `
<p>Dear ${clinicianName},</p>
<p>A new patient would like to share their symptoms data with you on our platform.</p>
<p>Register your account to view their spidergrams and track their data.</p>
<p>Here is a link to our website: https://team3.uksouth.cloudapp.azure.com</p>
<p>Kind regards,</p>
<p>The Spider team</p>
`,
};

await sgMail.send(msg);

return { success: true };
} catch (error: any) {
console.error('[Invite Email Error]', error);

return { success: false, error: error.message };
}
}


39 changes: 24 additions & 15 deletions src/views/ClinicianSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,7 +76,6 @@ export const ClinicianSearch = ({ onSaveClinician, savedClinicians }: ClinicianS
return () => observer.disconnect()
}, [])


const handleOpenInviteModal = () => {
setInviteModalOpen(true)
}
Expand All @@ -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');
}
}

Expand Down Expand Up @@ -459,3 +464,7 @@ return
</>
)
}




5 changes: 4 additions & 1 deletion src/views/DateOfBirthPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 7 additions & 5 deletions src/views/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,6 @@ const LoginV2 = ({ mode }: { mode: string }) => {
</Link>
<div className='flex flex-col gap-5 is-full sm:is-auto md:is-full sm:max-is-[400px] md:max-is-[unset] border border-divider rounded-lg p-6 bg-backgroundPaper'>
<Typography variant='h4' className='text-center'>{`Log in`}</Typography>
{error && (
<Typography variant='caption' className='text-center mt-3' color='var(--mui-palette-error-main)'>
{error}
</Typography>
)}
<form onSubmit={handleLogin} className='flex flex-col gap-5'>
<TextField
autoFocus
Expand Down Expand Up @@ -170,6 +165,12 @@ const LoginV2 = ({ mode }: { mode: string }) => {
Log In
</Button>

{error && (
<Typography variant='caption' className='text-center mt-3' color='var(--mui-palette-error-main)'>
{error}
</Typography>
)}

<Typography className='text-center mt-4'>
Don&apos;t have an account?
<Link href='/register' className='text-primary underline ml-2'>
Expand All @@ -186,3 +187,4 @@ const LoginV2 = ({ mode }: { mode: string }) => {
export default LoginV2



78 changes: 46 additions & 32 deletions src/views/RegisterUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
}
Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -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 = '';
}
Expand Down Expand Up @@ -884,8 +880,8 @@ return parsed?.number || phone.trim().replace(/\s/g, '')
{accountType === 'patient' && (
<>
<Grid item xs={12}>
<TextField
label='Hospital Number'
<TextField
label='Hospital Number (Optional)'
name='hospitalNumber'
autoComplete='off'
fullWidth
Expand Down Expand Up @@ -951,6 +947,7 @@ return parsed?.number || phone.trim().replace(/\s/g, '')
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel shrink={isInstitutionFocused || !!formData.institution}>Institution</InputLabel>
<Select
label='Institution'
name='institution'
value={formData.institution}
onChange={(event: SelectChangeEvent<string>) =>
Expand All @@ -974,33 +971,48 @@ return parsed?.number || phone.trim().replace(/\s/g, '')
<MenuItem value='Other'>Other</MenuItem>
</Select>
{formErrors.institution && (
<Typography variant='caption' color='error'>
<Typography variant='caption' sx={{ color: '#f44336' }}>
{formErrors.institution}
</Typography>
)}
</FormControl>
</Grid>
<Grid item xs={12}>
{' '}
<TextField
label='Profession'
name='profession'
fullWidth
variant='outlined'
value={formData.profession}
onChange={handleInputChange}
onBlur={() => {
setTouchedFields(prev => ({ ...prev, profession: true }))
validateForm('profession')
setIsFirstNameFocused(!!formData.profession)
}}
error={!!formErrors.profession}
helperText={formErrors.profession}
InputLabelProps={{ shrink: isProfessionFocused || !!formData.profession }}
onFocus={() => setIsProfessionFocused(true)}
sx={{ mb: 2, '& .MuiOutlinedInput-root': { borderRadius: '8px' } }}
/>{' '}
</Grid>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel shrink={isProfessionFocused || !!formData.profession}>Profession</InputLabel>
<Select
label='Profession'
name='profession'
value={formData.profession || ''}
onChange={(event: SelectChangeEvent<string>) =>
handleInputChange({
target: { name: 'profession', value: event.target.value }
} as React.ChangeEvent<HTMLInputElement>)
}
onBlur={() => {
setTouchedFields(prev => ({ ...prev, profession: true }))
validateForm('profession')
setIsProfessionFocused(!!formData.profession)
}}
onOpen={() => setIsProfessionFocused(true)}
onClose={() => setIsProfessionFocused(!!formData.profession)}
error={!!formErrors.profession}
>
<MenuItem value='Rheumatologist'>Rheumatologist</MenuItem>
<MenuItem value='General Practitioner'>General Practitioner</MenuItem>
<MenuItem value='Geneticist'>Geneticist</MenuItem>
<MenuItem value='Paediatrician'>Paediatrician</MenuItem>
<MenuItem value='Physiotherapist'>Physiotherapist</MenuItem>
<MenuItem value='Orthopaedic Consultant'>Orthopaedic Consultant</MenuItem>
<MenuItem value='Other'>Other</MenuItem>
</Select>
{formErrors.profession && (
<Typography variant='caption' sx={{ color: '#f44336' }}>
{formErrors.profession}
</Typography>
)}
</FormControl>
</Grid>
</>
)}

Expand All @@ -1010,6 +1022,7 @@ return parsed?.number || phone.trim().replace(/\s/g, '')
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel shrink={isInstitutionFocused || !!formData.institution}>Institution</InputLabel>
<Select
label='Profession'
name='institution'
value={formData.institution}
onChange={(event: SelectChangeEvent<string>) =>
Expand Down Expand Up @@ -1210,3 +1223,4 @@ return parsed?.number || phone.trim().replace(/\s/g, '')