Skip to content
Merged
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
31 changes: 28 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,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: `
<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 };
}
}
31 changes: 21 additions & 10 deletions src/actions/register/registerActions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

'use server';

import { revalidatePath } from 'next/cache';
Expand All @@ -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[];}
Expand All @@ -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<RegisterResult> {
try {
if (!data.email || !data.password || !data.firstName || !data.lastName) {
Expand Down Expand Up @@ -43,7 +42,8 @@ export async function registerUser(data: RegisterUserData): Promise<RegisterResu
: 'PATIENT'; // Default to patient

// Create the user with all provided fields
const user = await prisma.user.create({ data: { email: data.email, hashedPassword, firstName: data.firstName, lastName: data.lastName, dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : undefined, address: data.address, phoneNumber: data.phoneNumber,hospitalNumber: data.hospitalNumber, profession: data.profession, registrationNumber: data.registrationNumber, institution: data.institution, role, status: 'PENDING', }});
const initialStatus = role === 'PATIENT' ? 'ACTIVE' : 'PENDING';
const user = await prisma.user.create({ data: { email: data.email, hashedPassword, firstName: data.firstName, lastName: data.lastName, dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : undefined, address: data.address, phoneNumber: data.phoneNumber,hospitalNumber: data.hospitalNumber, profession: data.profession, registrationNumber: data.registrationNumber, institution: data.institution, role, status: initialStatus, }});


return { success: true, userId: user.id};
Expand All @@ -55,26 +55,35 @@ return { success: false, error: 'Failed to register user. Please try again.'
}
}


export async function saveDataPrivacyPreferences(
userId: string,
data: DataPrivacyFormData
) {
try {

const user = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
});

const isPatient = user?.role === 'PATIENT';

// Update the user's research consent preference
await prisma.user.update({ where: { id: userId }, data: { agreedForResearch: data.researchConsent,},});

// Create clinician relationships if clinician access is granted
if (data.clinicianAccess && data.selectedClinicians.length > 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,},
})
)
);
Expand All @@ -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);
Expand All @@ -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') {
}

Expand All @@ -129,7 +140,6 @@ return {
}
}


export async function searchClinicians(searchParams: Record<string, string>) {
try {
const { firstName, lastName, institution, email } = searchParams;
Expand Down Expand Up @@ -181,3 +191,4 @@ return { emailExists: false, phoneExists: false, registrationNumberExists: false
}
}


50 changes: 27 additions & 23 deletions src/views/ClinicianSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

'use client'

import React, { useState, useRef, useEffect } from 'react'

import Image from 'next/image'

import {
Box,
Expand All @@ -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 @@ -199,13 +204,7 @@ return
})}
onClick={() => !alreadySaved && handleSelectClinician(clinician)}
>
<Image
src='/images/pages/savedclinician-logo.png'
alt='Hospital Logo'
width={40}
height={40}
style={{ marginRight: 8 }}
/>
<i className='ri-hospital-line' style={{ color: 'orange', fontSize: '24px', marginRight: 8 }}></i>
<Box sx={{ flexGrow: 1 }}>
<Typography sx={theme => ({ fontWeight: 'bold', fontSize: '14px', color: theme.palette.text.primary })}>
{clinician.firstName} {clinician.lastName}
Expand Down Expand Up @@ -459,3 +458,8 @@ 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
11 changes: 6 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 Down
Loading