diff --git a/frontend/src/apiClient.ts b/frontend/src/apiClient.ts new file mode 100644 index 00000000..645c9fa2 --- /dev/null +++ b/frontend/src/apiClient.ts @@ -0,0 +1,44 @@ +import axios from "axios"; +import { FormValues } from "./pages/Feedback/FeedbackForm"; +const baseURL = import.meta.env.VITE_API_BASE_URL; + +const api = axios.create({ + baseURL, + headers: { + Authorization: `JWT ${localStorage.getItem("access")}`, + }, +}); + +// Request interceptor to set the Authorization header +api.interceptors.request.use( + (configuration) => { + const token = localStorage.getItem("access"); + if (token) { + configuration.headers.Authorization = `JWT ${token}`; + } + return configuration; + }, + (error) => Promise.reject(error) +); + +const handleSubmitFeedback = async ( + feedbackType: FormValues["feedbackType"], + name: FormValues["name"], + email: FormValues["email"], + message: FormValues["message"] +) => { + try { + const response = await api.post(`/jira/feedback/`, { + feedbacktype: feedbackType, + name, + email, + message, + }); + return response.data; + } catch (error) { + console.error("Error(s) during handleSubmitFeeedback: ", error); + throw error; + } +}; + +export { handleSubmitFeedback }; diff --git a/frontend/src/pages/Feedback/FeedbackForm.tsx b/frontend/src/pages/Feedback/FeedbackForm.tsx index 68a3b2a1..a1a04c84 100644 --- a/frontend/src/pages/Feedback/FeedbackForm.tsx +++ b/frontend/src/pages/Feedback/FeedbackForm.tsx @@ -2,22 +2,39 @@ import { useEffect, useState } from "react"; import { useFormik } from "formik"; import { useMutation } from "react-query"; import { object, string } from "yup"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; +import { handleSubmitFeedback } from "../../apiClient"; -interface FormValues { +export interface FormValues { + feedbackType: "new_feature" | "issue" | "general" | ""; name: string; email: string; message: string; image: string; } +// Error Message interface and validation +interface ErrorMessages { + [key: string]: string[]; +} +const isValidErrorMessages = (data: unknown): data is ErrorMessages => { + if (typeof data !== "object" || data === null) return false; + return Object.entries(data).every( + ([key, value]) => + typeof key === "string" && + Array.isArray(value) && + value.every((item) => typeof item === "string") + ); +}; + const FeedbackForm = () => { const [feedback, setFeedback] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); + const [errorMessages, setErrorMessages] = useState({}); const [isPressed, setIsPressed] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const feedbackValidation = object().shape({ + feedbackType: string().required("You must select a feedback type"), name: string().required("Name is a required field"), email: string() .email("You have entered an invalid email") @@ -33,18 +50,19 @@ const FeedbackForm = () => { feedbackMessage.innerText = feedback; } + // TO-DO: Code below may not be necessary // Update an error message div after Submit - const errorMessageDiv = document.getElementById("error-message"); - if (errorMessageDiv) { - errorMessageDiv.innerText = errorMessage; - } - }, [feedback, errorMessage]); + // const errorMessageDiv = document.getElementById("error-message"); + // if (errorMessageDiv) { + // errorMessageDiv.innerText = errorMessage; + // } + }, [feedback, errorMessages]); //reset the form fields and states when clicking cancel const handleCancel = () => { resetForm(); setFeedback(""); - setErrorMessage(""); + setErrorMessages({}); }; const handleMouseDown = () => { @@ -79,85 +97,99 @@ const FeedbackForm = () => { const { errors, handleChange, handleSubmit, resetForm, touched, values } = useFormik({ initialValues: { + feedbackType: "", name: "", email: "", message: "", image: "", }, + validationSchema: feedbackValidation, onSubmit: async (values) => { setFeedback(""); try { // Call 1: Create Feedback request - const response = await axios.post( - "http://localhost:8000/api/jira/create_new_feedback/", - { - name: values.name, - email: values.email, - message: values.message, - }, - { - headers: { - "Content-Type": "application/json", - }, - } + await handleSubmitFeedback( + values.feedbackType, + values.name, + values.email, + values.message ); + setFeedback("Feedback submitted successfully!"); + resetForm(); + setErrorMessages({}); + + // TO-DO: Commented code below needs to be updated for image submission // check to see if request was successful and get the issue key - if (response.data.status === 201) { - const issueKey = response.data.issueKey; + // if (response.data.status === 201) { + // const issueKey = response.data.issueKey; - if (values.image) { - // Call 2: Upload Image - const formData = new FormData(); - formData.append("issueKey", issueKey); - formData.append("attachment", values.image); + // if (values.image) { + // // Call 2: Upload Image + // const formData = new FormData(); + // formData.append("issueKey", issueKey); + // formData.append("attachment", values.image); - const response2 = await axios.post( - "http://localhost:8000/api/jira/upload_servicedesk_attachment/", - formData, - { - headers: { - "Content-Type": "multipart/form-data", - }, - } - ); + // const response2 = await axios.post( + // "http://localhost:8000/api/jira/upload_servicedesk_attachment/", + // formData, + // { + // headers: { + // "Content-Type": "multipart/form-data", + // }, + // } + // ); - // Check if attachment upload was successful - if (response2.data.status === 200) { - const attachmentId = response2.data.tempAttachmentId; + // // Check if attachment upload was successful + // if (response2.data.status === 200) { + // const attachmentId = response2.data.tempAttachmentId; - // Step 3: Attach upload image to feedback request - const response3 = await axios.post( - "http://localhost:8000/api/jira/attach_feedback_attachment/", - { - issueKey: issueKey, - tempAttachmentId: attachmentId, - } - ); + // // Step 3: Attach upload image to feedback request + // const response3 = await axios.post( + // "http://localhost:8000/api/jira/attach_feedback_attachment/", + // { + // issueKey: issueKey, + // tempAttachmentId: attachmentId, + // } + // ); - // Check if the attachment was successfully attached - if (response3.status === 200) { - setFeedback("Feedback and image submitted successfully!"); - resetForm(); - } else { - setErrorMessage("Error attaching image"); - } - } else { - setErrorMessage("Error uploading the image."); - console.log(response2); - } + // // Check if the attachment was successfully attached + // if (response3.status === 200) { + // setFeedback("Feedback and image submitted successfully!"); + // resetForm(); + // } else { + // setErrorMessage("Error attaching image"); + // } + // } else { + // setErrorMessage("Error uploading the image."); + // console.log(response2); + // } + // } else { + // setFeedback("Feedback submitted successfully!"); + // resetForm(); + // } + // } else { + // setErrorMessage(`Error(s): ${response.data.message}`); + // } + } catch (error) { + console.error(error); + if (error instanceof AxiosError && error.response) { + const data = error.response.data; + if (isValidErrorMessages(data)) { + // Handle expected error such as missing/invalid form field + setErrorMessages(data); } else { - setFeedback("Feedback submitted successfully!"); - resetForm(); + // Handle unexpected error such as invalid JWT token + setErrorMessages({ "Axios Error": [error.message] }); } + } else if (error instanceof Error) { + setErrorMessages({ Error: [error.message] }); } else { - setErrorMessage("Error creating a new feedback request."); + // Handle unknown error + setErrorMessages({ Error: ["Unknown Error"] }); } - } catch (error) { - setErrorMessage("An error occurred while submitting the form"); } }, - validationSchema: feedbackValidation, }); return ( @@ -178,44 +210,57 @@ const FeedbackForm = () => {
+
+ {touched.feedbackType && errors.feedbackType && ( +

+ {errors.feedbackType} +

+ )} +
@@ -404,7 +449,16 @@ const FeedbackForm = () => {
{feedback}
-
{errorMessage}
+ {Object.entries(errorMessages).map(([field, messages], index) => ( +
+ {field}: +
    + {messages.map((message, idx) => ( +
  • {message}
  • + ))} +
+
+ ))} diff --git a/frontend/src/pages/PatientManager/PatientManager.tsx b/frontend/src/pages/PatientManager/PatientManager.tsx index 56b72b8a..94770b97 100644 --- a/frontend/src/pages/PatientManager/PatientManager.tsx +++ b/frontend/src/pages/PatientManager/PatientManager.tsx @@ -4,60 +4,60 @@ import NewPatientForm from "./NewPatientForm.tsx"; import PatientHistory from "./PatientHistory.tsx"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import PatientSummary from "./PatientSummary.tsx"; -import { PatientInfo } from "./PatientTypes.ts"; -import { copy } from "../../assets/index.js"; -import Welcome from "../../components/Welcome/Welcome.tsx"; +import PatientSummary from './PatientSummary.tsx'; +import { Diagnosis, PatientInfo } from './PatientTypes.ts'; +import { copy } from '../../assets/index.js'; +import Welcome from '../../components/Welcome/Welcome.tsx'; const PatientManager = () => { const [isPatientDeleted, setIsPatientDeleted] = useState(false); - const [patientInfo, setPatientInfo] = useState({ - ID: "", - Diagnosis: "" || undefined, - OtherDiagnosis: "", - Description: "", - CurrentMedications: "", - PriorMedications: "", - Depression: "", - Hypomania: "", - Mania: "", - Psychotic: "", - Suicide: "", - Kidney: "", - Liver: "", - blood_pressure: "", - weight_gain: "", - Reproductive: "", - risk_pregnancy: "", - PossibleMedications: { - first: "", - second: "", - third: "", - }, - }); + const [patientInfo, setPatientInfo] = useState({ + ID: '', + Diagnosis: Diagnosis.Manic, + OtherDiagnosis: '', + Description: '', + CurrentMedications: '', + PriorMedications: '', + Depression: '', + Hypomania: '', + Mania: '', + Psychotic: '', + Suicide: '', + Kidney: '', + Liver: '', + blood_pressure: '', + weight_gain: '', + Reproductive: '', + risk_pregnancy: '', + PossibleMedications: { + first: '', + second: '', + third: '', + }, + }); - const handlePatientDeleted = (deletedId: string) => { - if (patientInfo.ID === deletedId) { - setPatientInfo({ - ID: "", - Diagnosis: "" || undefined, - OtherDiagnosis: "", - Description: "", - CurrentMedications: "", - PriorMedications: "", - Depression: "", - Hypomania: "", - Mania: "", - Psychotic: "", - Suicide: "", - Kidney: "", - Liver: "", - blood_pressure: "", - weight_gain: "", - Reproductive: "", - risk_pregnancy: "", - }); + const handlePatientDeleted = (deletedId: string) => { + if (patientInfo.ID === deletedId) { + setPatientInfo({ + ID: '', + Diagnosis: Diagnosis.Manic, + OtherDiagnosis: '', + Description: '', + CurrentMedications: '', + PriorMedications: '', + Depression: '', + Hypomania: '', + Mania: '', + Psychotic: '', + Suicide: '', + Kidney: '', + Liver: '', + blood_pressure: '', + weight_gain: '', + Reproductive: '', + risk_pregnancy: '', + }); setIsPatientDeleted(true); } diff --git a/server/api/admin.py b/server/api/admin.py index c7a44b4c..be1266ae 100644 --- a/server/api/admin.py +++ b/server/api/admin.py @@ -46,4 +46,4 @@ class AI_PromptStorage(admin.ModelAdmin): @admin.register(Feedback) class Feedback(admin.ModelAdmin): - list_display = ['feedbacktype'] + list_display = ['feedbacktype', 'name', 'email', 'message'] diff --git a/server/api/migrations/0005_feedback_email_feedback_message_feedback_name_and_more.py b/server/api/migrations/0005_feedback_email_feedback_message_feedback_name_and_more.py new file mode 100644 index 00000000..2e5bc469 --- /dev/null +++ b/server/api/migrations/0005_feedback_email_feedback_message_feedback_name_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2024-07-17 17:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_feedback'), + ] + + operations = [ + migrations.AddField( + model_name='feedback', + name='email', + field=models.EmailField(default='', max_length=254), + ), + migrations.AddField( + model_name='feedback', + name='message', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='feedback', + name='name', + field=models.CharField(default='', max_length=100), + ), + migrations.AlterField( + model_name='feedback', + name='feedbacktype', + field=models.CharField(choices=[('issue', 'Issue'), ('new_feature', 'New Feature'), ('general', 'General')], default='general', max_length=100), + ), + ] diff --git a/server/api/views/jira/models.py b/server/api/views/jira/models.py index 7684899d..d618b9ff 100644 --- a/server/api/views/jira/models.py +++ b/server/api/views/jira/models.py @@ -2,7 +2,20 @@ class Feedback(models.Model): - feedbacktype = models.CharField(max_length=100) + ISSUE = 'issue' + NEW_FEATURE = 'new_feature' + GENERAL = 'general' + + FEEDBACK_TYPE_CHOICES = [ + (ISSUE, 'Issue'), + (NEW_FEATURE, 'New Feature'), + (GENERAL, 'General'), + ] + + feedbacktype = models.CharField(max_length=100, choices=FEEDBACK_TYPE_CHOICES, default=GENERAL) + name = models.CharField(max_length=100, default='') + email = models.EmailField(default='') + message = models.TextField(default='') def __str__(self): - return self.feedbacktype + return self.name diff --git a/server/api/views/jira/serializers.py b/server/api/views/jira/serializers.py index cb5ce0ed..4538029e 100644 --- a/server/api/views/jira/serializers.py +++ b/server/api/views/jira/serializers.py @@ -5,4 +5,4 @@ class FeedbackSerializer(serializers.ModelSerializer): class Meta: model = Feedback - fields = ['feedbacktype'] + fields = ['feedbacktype', 'name', 'email', 'message'] diff --git a/server/api/views/jira/views.py b/server/api/views/jira/views.py index 77d7b98f..c604586d 100644 --- a/server/api/views/jira/views.py +++ b/server/api/views/jira/views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.decorators import api_view from rest_framework import status -from django.http import JsonResponse +from django.http import JsonResponse, HttpRequest from django import forms import requests import json @@ -16,21 +16,31 @@ class FeedbackView(APIView): - def post(self, request): + def post(self, request, *args, **kwargs): serializer = FeedbackSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - +# TO-DO: Edit/Delete all functions below @csrf_exempt -def create_new_feedback(request: str) -> JsonResponse: +def create_new_feedback(request: HttpRequest) -> JsonResponse: """ Create a new feedback request in Jira Service Desk. """ token: str = os.environ.get("JIRA_API_KEY") + try: + data: dict[str, str] = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"status": 400, "message": "Invalid JSON payload"}) + + required_fields = ["name", "email", "message", "feedbackType"] + missing_fields = [field for field in required_fields if field not in data] + if missing_fields: + return JsonResponse({"status": 400, "message": f"Missing fields: {', '.join(missing_fields)}"}) + data: dict[str, str] = json.loads(request.body) name: str = data["name"] email: str = data["email"] @@ -47,7 +57,7 @@ def create_new_feedback(request: str) -> JsonResponse: feedback_type_id = 33 case _: return JsonResponse( - {"status": 500, "message": "Internal server error"} + {"status": 400, "message": "Invalid feedback type"} ) url: str = "https://balancer.atlassian.net/rest/servicedeskapi/request"