diff --git a/public/locale/en.json b/public/locale/en.json index 11f81a52fbc..90127ca54b3 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -656,6 +656,7 @@ "create_position_preset": "Create a new position preset", "create_position_preset_description": "Creates a new position preset in Care from the current position of the camera for the given name", "create_preset_prerequisite": "To create presets for this bed, you'll need to link the camera to the bed first.", + "create_questionnaire": "Create Questionnaire", "create_resource_request": "Create Request", "create_schedule_template": "Create Schedule Template", "create_tag": "Create Tag", @@ -1389,6 +1390,7 @@ "name_of_shifting_approving_facility": "Name of shifting approving facility", "nationality": "Nationality", "nationality_is_required": "Nationality is required", + "navigation": "Navigation", "nearby_facilities": "Nearby Facilities", "network_failure": "Network Failure. Please check your internet connectivity.", "never": "never", @@ -1747,6 +1749,7 @@ "professional_info_note_view": "View user's professional information", "profile": "Profile", "profile_picture_deleted": "Profile picture deleted", + "properties": "Properties", "proposed": "Proposed", "provisional": "Provisional", "qualification": "Qualification", @@ -1882,6 +1885,7 @@ "resume": "Resume", "retake": "Retake", "retake_recording": "Retake Recording", + "retired": "Retired", "return_to_care": "Return to CARE", "return_to_login": "Return to Login", "return_to_password_reset": "Return to Password Reset", @@ -1962,8 +1966,10 @@ "search_medication": "Search Medication", "search_medications": "Search Medications", "search_medicine": "Search Medicine", + "search_organizations": "Search Organizations", "search_patient_page_text": "Search for existing patients using their phone number or create a new patient record", "search_patients": "Search Patients", + "search_questionnaires": "Search Questionnaires", "search_resource": "Search Resource", "search_tags": "Search tags...", "search_user": "Search User", @@ -2020,6 +2026,7 @@ "select_skills": "Select and add some skills", "select_status": "Select Status", "select_sub_department": "Select sub-department", + "select_subject_type": "Select Subject Type", "select_time": "Select time", "select_time_slot": "Select time slot", "select_user": "Select user", @@ -2128,6 +2135,7 @@ "stream_uuid": "Stream UUID", "sub_category": "Sub Category", "subject": "Subject", + "subject_type": "Subject Type", "submit": "Submit", "submitting": "Submitting", "subscribe": "Subscribe", @@ -2365,6 +2373,7 @@ "verify_patient_identifier": "Please verify the patient identifier", "verify_patient_identity": "Verify Patient Identity", "verify_using": "Verify Using", + "version": "Version", "video_call": "Video Call", "video_conference_link": "Video Conference Link", "view": "View", diff --git a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx index b3de7c4e7fb..2f53e2a9dfb 100644 --- a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx +++ b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { Building, Check, Loader2 } from "lucide-react"; import { useNavigate } from "raviger"; import { useState } from "react"; +import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -67,12 +68,11 @@ export default function CloneQuestionnaireSheet({ navigate(`/admin/questionnaire/${data.slug}`); setOpen(false); }, - onError: (error: any) => { - if (error.response?.status === 400) { - setError("This slug is already in use. Please choose a different one."); - } else { - setError("Failed to clone questionnaire. Please try again."); - } + onError: (error) => { + const errorData = error.cause as { errors: { msg: string }[] }; + errorData.errors.forEach((er) => { + toast.error(er.msg); + }); }, }); @@ -197,7 +197,7 @@ export default function CloneQuestionnaireSheet({ - +
- +
- +
+ } + /> + + ); + } + + return ( +
+
+ {selection.selectedIds.length > 0 ? ( + selection.available?.results + .filter((org) => selection.selectedIds.includes(org.id)) + .map((org) => ( + + {org.name} + + + )) + ) : ( +

+ {t("no_organizations_selected")} +

+ )} +
+ + + + + {t("no_organizations_found")} + + {selection.isLoading ? ( +
+ +
+ ) : ( + selection.available?.results.map((org) => ( + selection.onToggle(org.id)} + > +
+ + {org.name} + {org.description && ( + + - {org.description} + + )} +
+ {selection.selectedIds.includes(org.id) && ( + + )} +
+ )) + )} +
+
+
+
+ ); +} + +function TagSelector({ + id, + selection, + questionnaire, +}: { + id?: string; + selection: QuestionnairePropertiesProps["tagSelection"]; + questionnaire: QuestionnaireDetail; +}) { + if (id) { + return ( + <> +
+ {questionnaire.tags.map((tag) => ( + + + {tag.name} + + ))} + {questionnaire.tags.length === 0 && ( +

{t("no_tags_selected")}

+ )} +
+ + + {t("manage_tags")} + + } + /> + + ); + } + + return ( +
+
+ {selection.selectedIds.length > 0 ? ( + selection.available?.results + .filter((tag) => selection.selectedIds.includes(tag.id)) + .map((tag) => ( + + {tag.name} + + + )) + ) : ( +

{t("no_tags_selected")}

+ )} +
+ + + + + {t("no_tags_found")} + + {selection.isLoading ? ( +
+ +
+ ) : ( + selection.available?.results.map((tag) => ( + selection.onToggle(tag.id)} + > +
+ + {tag.name} +
+ {selection.selectedIds.includes(tag.id) && ( + + )} +
+ )) + )} +
+
+
+
+ ); +} + +function QuestionnaireProperties({ + questionnaire, + updateQuestionnaireField, + id, + organizations, + organizationSelection, + tagSelection, +}: QuestionnairePropertiesProps) { + return ( + + + {t("properties")} + + + updateQuestionnaireField("status", val)} + /> + + updateQuestionnaireField("subject_type", val)} + /> + +
+ + +
+
+ + +
+ + + Clone Questionnaire + + } + /> + +
+ + + updateQuestionnaireField("version", e.target.value) + } + /> +
+
+
+ ); +} + +const LAYOUT_OPTIONS = [ + { + id: "full-width", + value: "grid grid-cols-1", + label: "Full Width", + preview: ( +
+
+
+
+ ), + }, + { + id: "equal-split", + value: "grid grid-cols-2", + label: "Equal Split", + preview: ( +
+
+
+
+
+
+ ), + }, + { + id: "wide-start", + value: "grid grid-cols-[2fr,1fr]", + label: "Wide Start", + preview: ( +
+
+
+
+
+
+ ), + }, + { + id: "wide-end", + value: "grid grid-cols-[1fr,2fr]", + label: "Wide End", + preview: ( +
+
+
+
+
+
+ ), + }, +] as const; + +interface LayoutOptionProps { + option: (typeof LAYOUT_OPTIONS)[number]; + isSelected: boolean; + questionId: string; +} + +function LayoutOptionCard({ + option, + isSelected, + questionId, +}: LayoutOptionProps) { + const optionId = `${questionId}-${option.id}`; + return ( +
+ + +
+ ); +} + export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { const navigate = useNavigate(); const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit"); @@ -92,7 +603,9 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { new Set(), ); const [selectedOrgIds, setSelectedOrgIds] = useState([]); + const [selectedTagIds, setSelectedTagIds] = useState([]); const [orgSearchQuery, setOrgSearchQuery] = useState(""); + const [tagSearchQuery, setTagSearchQuery] = useState(""); const { data: initialQuestionnaire, @@ -106,16 +619,35 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { enabled: !!id, }); - const { data: availableOrganizations, isLoading: isLoadingOrganizations } = - useQuery({ - queryKey: ["organizations", orgSearchQuery], - queryFn: query(organizationApi.list, { - queryParams: { - org_type: "role", - name: orgSearchQuery || undefined, - }, - }), - }); + const { data: organizations } = useQuery({ + queryKey: ["questionnaire", id, "organizations"], + queryFn: query(questionnaireApi.getOrganizations, { + pathParams: { id: id! }, + }), + enabled: !!id, + }); + + const { + data: availableOrganizations, + isLoading: isLoadingAvailableOrganizations, + } = useQuery({ + queryKey: ["organizations", orgSearchQuery], + queryFn: query(organizationApi.list, { + queryParams: { + org_type: "role", + name: orgSearchQuery || undefined, + }, + }), + }); + + const { data: availableTags, isLoading: isLoadingAvailableTags } = useQuery({ + queryKey: ["tags", tagSearchQuery], + queryFn: query(questionnaireApi.tags.list, { + queryParams: { + name: tagSearchQuery || undefined, + }, + }), + }); const { mutate: createQuestionnaire, isPending: isCreating } = useMutation({ mutationFn: mutate(questionnaireApi.create), @@ -230,12 +762,22 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { ); }; + const handleToggleTag = (tagId: string) => { + setSelectedTagIds((current) => + current.includes(tagId) + ? current.filter((id) => id !== tagId) + : [...current, tagId], + ); + }; + return (

- {id ? "Edit Questionnaire" : "Create Questionnaire"} + {id + ? t("edit") + " " + questionnaire.title + : "Create Questionnaire"}

{questionnaire.description}

@@ -256,18 +798,24 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { onValueChange={(v) => setActiveTab(v as "edit" | "preview")} > - Edit - Preview + + + Edit form + + + + Preview form + -
-
- - - Navigation +
+
+ + + {t("navigation")} - + - - - - Properties - - -
- - -
- -
- - - updateQuestionnaireField("version", e.target.value) - } - /> -
- -
- - -
- -
- - {id ? ( - - - Manage Organizations - - } - /> - ) : ( -
-
- {selectedOrgIds.length > 0 ? ( - availableOrganizations?.results - .filter((org) => selectedOrgIds.includes(org.id)) - .map((org) => ( - - {org.name} - - - )) - ) : ( -

- No organizations selected -

- )} -
- - - - - No organizations found. - - {isLoadingOrganizations ? ( -
- -
- ) : ( - availableOrganizations?.results.map((org) => ( - - handleToggleOrganization(org.id) - } - > -
- - {org.name} - {org.description && ( - - - {org.description} - - )} -
- {selectedOrgIds.includes(org.id) && ( - - )} -
- )) - )} -
-
-
-
- )} -
-
-
+
+ +
-
+
Basic Information @@ -537,14 +956,15 @@ export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) { - - + +
- Questions -

- {questionnaire.questions?.length || 0} question - {questionnaire.questions?.length !== 1 ? "s" : ""} -

+ +

+ {questionnaire.questions?.length || 0} Question + {questionnaire.questions?.length !== 1 ? "s" : ""} +

+
{(questions || []).map((subQuestion, idx) => ( - { - const newQuestions = [...(questions || [])]; - newQuestions[idx] = updated; - updateField("questions", newQuestions); - }} - onDelete={() => { - const newQuestions = questions?.filter( - (_, i) => i !== idx, - ); - updateField("questions", newQuestions); - }} - isExpanded={expandedSubQuestions.has(subQuestion.id)} - onToggleExpand={() => - toggleSubQuestionExpanded(subQuestion.id) - } - depth={depth + 1} - parentId={getQuestionPath()} - onMoveUp={() => { - if (idx > 0) { + id={`question-${subQuestion.id}`} + className="relative bg-white rounded-lg shadow-md" + > + { const newQuestions = [...(questions || [])]; - [newQuestions[idx - 1], newQuestions[idx]] = [ - newQuestions[idx], - newQuestions[idx - 1], - ]; + newQuestions[idx] = updated; updateField("questions", newQuestions); - } - }} - onMoveDown={() => { - if (idx < (questions?.length || 0) - 1) { - const newQuestions = [...(questions || [])]; - [newQuestions[idx], newQuestions[idx + 1]] = [ - newQuestions[idx + 1], - newQuestions[idx], - ]; + }} + onDelete={() => { + const newQuestions = questions?.filter( + (_, i) => i !== idx, + ); updateField("questions", newQuestions); + }} + isExpanded={expandedSubQuestions.has(subQuestion.id)} + onToggleExpand={() => + toggleSubQuestionExpanded(subQuestion.id) } - }} - isFirst={idx === 0} - isLast={idx === (questions?.length || 0) - 1} - /> + depth={depth + 1} + parentId={getQuestionPath()} + onMoveUp={() => { + if (idx > 0) { + const newQuestions = [...(questions || [])]; + [newQuestions[idx - 1], newQuestions[idx]] = [ + newQuestions[idx], + newQuestions[idx - 1], + ]; + updateField("questions", newQuestions); + } + }} + onMoveDown={() => { + if (idx < (questions?.length || 0) - 1) { + const newQuestions = [...(questions || [])]; + [newQuestions[idx], newQuestions[idx + 1]] = [ + newQuestions[idx + 1], + newQuestions[idx], + ]; + updateField("questions", newQuestions); + } + }} + isFirst={idx === 0} + isLast={idx === (questions?.length || 0) - 1} + /> +
))}
diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index a02062c2325..c59c96d07f1 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -463,63 +463,67 @@ export function QuestionnaireForm({ {/* Search and Add Questionnaire */} -
- { - if ( - questionnaireForms.some( - (form) => form.questionnaire.id === selected.id, - ) - ) { - return; - } - - setQuestionnaireForms((prev) => [ - ...prev, - { - questionnaire: selected, - responses: initializeResponses(selected.questions), - errors: [], - }, - ]); - }} - disabled={isPending} - /> -
- - {/* Submit and Cancel Buttons */} - {questionnaireForms.length > 0 && ( -
- - -
+ { + if ( + questionnaireForms.some( + (form) => form.questionnaire.id === selected.id, + ) + ) { + return; + } + + setQuestionnaireForms((prev) => [ + ...prev, + { + questionnaire: selected, + responses: initializeResponses(selected.questions), + errors: [], + }, + ]); + }} + disabled={isPending} + /> +
+ + {/* Submit and Cancel Buttons */} + {questionnaireForms.length > 0 && ( +
+ + +
+ )} + )} ; - } - const questionnaireList = response?.results || []; return (
-
-
+
+

{t("questionnaires")}

{t("manage_and_view_questionnaires")}

- + +
+
+ updateQuery({ status: value })} + className="w-full" + > + + + + {t("active")} + + + + {t("draft")} + + + + {t("retired")} + + + +
+ + updateQuery({ title: e.target.value })} + /> +
+
+ +
+ +
+
-
- - +
+
+ - - - - @@ -73,29 +122,43 @@ export function QuestionnaireList() { } className="cursor-pointer hover:bg-gray-50" > - - - - ))} diff --git a/src/components/Questionnaire/show.tsx b/src/components/Questionnaire/show.tsx index f18ca66f1e0..6c3fda69b27 100644 --- a/src/components/Questionnaire/show.tsx +++ b/src/components/Questionnaire/show.tsx @@ -1,4 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query"; +import { Tags } from "lucide-react"; import { useNavigate } from "raviger"; import { useState } from "react"; @@ -94,7 +95,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { isLoading, error, } = useQuery({ - queryKey: ["questionnaire", id], + queryKey: ["questionnaireDetail", id], queryFn: query(questionnaireApi.detail, { pathParams: { id }, }), @@ -182,7 +183,7 @@ export function QuestionnaireShow({ id }: QuestionnaireShowProps) { questionnaire={questionnaire} trigger={ e.preventDefault()}> - + Manage Tags } diff --git a/src/components/Resource/ResourceForm.tsx b/src/components/Resource/ResourceForm.tsx index fb93e88e316..bc4981b2ab9 100644 --- a/src/components/Resource/ResourceForm.tsx +++ b/src/components/Resource/ResourceForm.tsx @@ -543,7 +543,7 @@ export default function ResourceForm({ facilityId, id }: ResourceProps) { -
+
+ {t("title")} + {t("description")} - {t("status")} - + {t("slug")}
-
+
+
{questionnaire.title}
-
- {questionnaire.description} +
+
+
+ {questionnaire.description} +
+ + + + + +
+

{questionnaire.title}

+

+ {questionnaire.description} +

+
+
+
- - {questionnaire.status} - - - {questionnaire.slug} + +