diff --git a/public/locale/en.json b/public/locale/en.json index cf54a0e11f7..16743c0a7b1 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -385,6 +385,7 @@ "activity_definitions_to_include": "Activity definitions to include", "activity_log": "Activity Log", "add": "Add", + "add_a_condition": "Add a condition", "add_access": "Add Assignment", "add_activity_definition": "Add Activity Definition", "add_allergy_one": "Add Allergy", @@ -409,7 +410,6 @@ "add_charge_items_invoice": "Add Charge Items to Invoice", "add_child_tag": "Add child tag", "add_child_tags_to_organize_better": "Add child tags to organize better", - "add_coding": "Add Coding", "add_column": "Add Column", "add_completion_note": "Add Completion Note", "add_component": "Add component", @@ -441,6 +441,7 @@ "add_file": "Add File", "add_files": "Add Files", "add_filter": "Add Filter", + "add_first_question": "Add First Question", "add_first_service_point": "Add First Service Point", "add_form": "Add Form", "add_forms": "Add Forms", @@ -482,8 +483,10 @@ "add_new_medications": "Add New Medications", "add_new_patient": "Add New Patient", "add_new_product": "Add New Product", + "add_new_question": "Add new question", "add_new_service_account": "Add New Service Account", "add_new_user": "Add New User", + "add_note": "Add note", "add_notes": "Add notes", "add_notes_about_symptom": "Add notes about the symptom...", "add_notes_about_the_allergy": "Add notes about the allergy", @@ -493,7 +496,6 @@ "add_organization": "Add Organization", "add_organization_one": "Add Organization", "add_organization_other": "Add Organizations", - "add_organizations": "Add organizations", "add_patient_identifier_config": "Add patient identifier config", "add_patient_updates": "Add Patient Updates", "add_point": "Add Point", @@ -510,8 +512,6 @@ "add_product_knowledge": "Add Product Knowledge", "add_products_to_receive": "Add products to receive", "add_question": "Add Question", - "add_question_above": "Add Question Above", - "add_question_below": "Add Question Below", "add_questionnaire": "Add Questionnaire", "add_range": "Add Range", "add_remarks": "Add remarks", @@ -543,7 +543,6 @@ "add_tag_config": "Add tag config", "add_tags": "Add Tags", "add_tax": "Add tax", - "add_template": "Add Template", "add_text": "Add Text", "add_this_activity_definition": "Add this activity definition", "add_this_medication": "Add this medication", @@ -553,9 +552,10 @@ "add_to_organization": "Add to Organization", "add_to_template": "Add to template", "add_token_category": "Add Token Category", - "add_unit": "Add a Unit", + "add_unit": "Add unit", "add_user": "Add User", "add_your_first_component": "Add your first component", + "add_your_first_question_hint": "Save the questionnaire, then use Edit Questions to add questions.", "added_on": "Added on", "adding": "Adding...", "additional_details": "Additional Details", @@ -646,6 +646,7 @@ "all_billing_statuses": "All Billing Statuses", "all_categories": "All Categories", "all_changes_have_been_saved": "All changes have been saved", + "all_conditions_and": "All conditions are true (AND)", "all_deliveries": "All Deliveries", "all_dept": "All Dept.", "all_details": "All Details", @@ -707,10 +708,10 @@ "and_the_status_of_request_is": "and the status of request is", "answer": "Answer", "answer_options": "Answer options", - "answer_options_description": "Define possible answers for this question", - "answer_value": "Answer value", + "answer_options_hint": "Define possible answers for this question", "antenatal": "Antenatal", "any_additional_information": "Any Additional Information", + "any_condition_or": "Any condition is true (OR)", "any_other_comments": "Any other comments", "app": "App", "app_base_url": "App base URL", @@ -1005,6 +1006,7 @@ "before": "before", "begin_clinical_encounter": "Begin a new clinical encounter for {{patientName}}. Select the appropriate encounter type, status, and priority to ensure proper documentation and care delivery.", "behavioral": "Behavioral", + "behaviour": "Behaviour", "beta": "beta", "bill": "Bill", "bill_all_pending_prescriptions": "Bill All Pending Prescriptions", @@ -1185,6 +1187,7 @@ "check_eligibility": "Check Eligibility", "check_for_available_update": "Check for available update and clear cache", "check_for_update": "Check for Update", + "check_how_form_looks": "Check how the questionnaire looks", "check_in": "Check-In", "check_item_condition_and_verify_receipt": "Check item condition and verify receipt", "check_policy_eligibility": "Check Policy Eligibility", @@ -1203,7 +1206,6 @@ "choose_file": "Upload From Device", "choose_file_or_drag": "Choose file or drag & drop it here", "choose_form": "Choose Form", - "choose_layout_style": "Choose the layout style that best fits your sub-questions from the available options.", "choose_localbody": "Choose Local Body", "choose_location": "Choose Location", "choose_other_search_type": "Choose other search types", @@ -1252,7 +1254,6 @@ "classification": "Classification", "clear": "Clear", "clear_all": "Clear all", - "clear_all_defaults": "Clear all defaults", "clear_all_filters": "Clear all filters", "clear_blocked_camera_permissions": "Clear blocked camera permissions", "clear_cache": "Clear Cache", @@ -1282,15 +1283,14 @@ "click_the_settings_icon_in_browser_address_bar": "Click the settings icon in browser address bar", "click_to_add_all_items": "Click to add all items", "click_to_add_dosage_instructions": "Click to add dosage instructions", - "click_to_add_first_question": "Click to add the first question", "click_to_edit": "Click to edit", "clinical": "Clinical", "clinical_history": "Clinical History", "clone": "Clone", + "clone_form": "Clone Questionnaire", "clone_questionnaire": "Clone Questionnaire", - "clone_questionnaire_description": "Create a copy of this questionnaire with a new slug and select organizations.", + "clone_questionnaire_draft_description": "A draft copy of this questionnaire will be created with the title and slug below.", "clone_role": "Clone Role", - "cloning": "Cloning...", "close": "Close", "close_account": "Close Account", "close_account_description": "Closing the account will also set the account to inactive.", @@ -1310,7 +1310,7 @@ "closed_completed": "Closed Completed", "closed_voided": "Closed Voided", "code": "Code", - "code_verification_required": "Code Verification is required", + "code_verified": "Code Verified", "code_verified_successfully": "Code verified successfully", "coding_details": "Coding Details", "collapse": "Collapse", @@ -1390,12 +1390,17 @@ "condition_metric__patient_age": "Patient Age", "condition_metric__patient_gender": "Patient Gender", "condition_metric__patient_tag": "Patient Tags", + "condition_n": "Condition {{n}}", "condition_operation__equality": "Equals to", "condition_operation__has_tag": "Has tag", "condition_operation__in_range": "In range", + "condition_target_not_answerable": "Visibility conditions can't target group or display questions — pick a question that records an answer", + "condition_target_required": "Every visibility condition needs a target question", "conditional": "Conditional", "conditional_discounts_not_applied": "Conditional discounts not applied to total", "conditions": "Conditions", + "conditions_count_one": "{{count}} Condition", + "conditions_count_other": "{{count}} Conditions", "config_create_failed": "Failed to create configuration", "config_created_successfully": "Configuration created successfully", "config_delete_failed": "Failed to delete configuration", @@ -1408,6 +1413,7 @@ "configure_facility": "Configure Facility", "configure_interpretation": "Configure interpretation", "configured": "Configured", + "configured_count": "Configured • {{count}}", "confirm": "Confirm", "confirm_action_description": "Are you sure you want to <1>{{action}}?", "confirm_add_lot": "Confirm & Add Lot", @@ -1425,7 +1431,6 @@ "confirm_dispense": "Confirm Dispense", "confirm_mark_as_error": "Confirm Mark as Entered in Error", "confirm_password": "Confirm Password", - "confirm_remove_questions": "Confirm Remove Questions", "confirm_removing_member": "Confirm Removing Member", "confirm_removing_member_description": "Are you sure you want to remove {{member}} from the care team?", "confirm_selection": "Confirm Selection", @@ -1530,10 +1535,13 @@ "container_information": "Container Information", "container_min_volume": "Minimum Volume", "container_requirements": "Container Requirements", - "content": "Content", + "context__encounterId": "an encounter", + "context__facilityId": "a facility", + "context__patientId": "a patient", "contexts": "Contexts", "continue": "Continue", "continue_and_clear": "Continue and Clear", + "continue_editing": "Continue Editing", "continue_watching": "Continue watching", "contribute_github": "Contribute on Github", "copied_to_clipboard": "Copied to clipboard!", @@ -1569,6 +1577,7 @@ "create_charge_item_definition_description": "Define pricing components for services or items", "create_consent": "Create Consent", "create_consultation": "Create Consultation", + "create_copy_of_form": "Create a copy of this questionnaire", "create_delivery": "Create Delivery", "create_delivery_order": "Create Delivery Order", "create_department_team": "Create Department/Team", @@ -1684,6 +1693,7 @@ "current_role": "Current Role", "current_service": "Current Service", "current_status": "Current Status", + "current_version_is": "Current version: {{version}}", "current_year_yy_help": "Current year (2 digits)", "current_year_yyyy_help": "Current year (4 digits)", "custom": "Custom", @@ -1707,8 +1717,7 @@ "dashboard_tab_associations": "These are the responsibilities - such as doctors, nurses, technicians, volunteers, or staff - that you are affiliated with.", "dashboard_tab_facilities": "These are the clinics, hospitals, or health centers you manage or belong to.", "dashboard_tab_governance": "These are the Local Self-Government (LSG) networks or groups you're affiliated with, including State, District, Block, Municipality, Corporation, Panchayat, and Ward-level governance bodies.", - "data_collection_details": "Data collection details", - "data_collection_details_description": "Specify key collection info: time, performer, body site, and method.", + "data_capture": "Data Capture", "data_source": "Data Source", "data_type": "Data Type", "date": "Date", @@ -1751,11 +1760,11 @@ "default_format": "Default Format", "default_interpretation": "Fallback", "default_interpretation_description": "Catch-all rule applied when a value does not match any configured range. Useful for highlighting out-of-range values.", - "default_selected": "Default Selected", + "default_unit": "Default Unit", + "default_unit_hint": "Pre-selected unit for this question. People filling the form can pick a different unit for their answer.", "default_value": "Serial Number", "default_value_help": "You can use a template string to auto-generate serial numbers.", "default_value_title": "Default pattern for this identifier type", - "defaults_selected": "Defaults Selected", "definition": "Definition", "definition_deleted_successfully": "Definition deleted successfully", "definition_not_found": "Definition not found", @@ -1782,6 +1791,7 @@ "delete_observation": "Delete Observation", "delete_observation_definition": "Delete Obsertvation Definition", "delete_organization": "Delete Organization", + "delete_question": "Delete question", "delete_record": "Delete Record", "delete_template": "Delete Template", "delete_template_confirmation": "Are you sure you want to delete \"{{name}}\"? This action cannot be undone.", @@ -1821,7 +1831,6 @@ "description_help": "A short description of this identifier type (e.g., National ID, Hospital MRN).", "designation": "Designation", "destination": "Destination", - "destination_question": "Destination Question", "destination_required": "Destination is required", "details": "Details", "details_about_the_equipment": "Details about the equipment", @@ -2003,9 +2012,7 @@ "display_help_short": "A user-friendly label for this identifier.", "display_name": "Display name", "display_style": "Display Style", - "display_text": "Display Text", "display_text_is_required_for_custom_codes": "Display text is required for custom codes", - "display_text_placeholder": "Display text (optional)", "disposition": "Disposition", "distributor": "Distributor", "district": "District", @@ -2041,8 +2048,10 @@ "download": "Download", "download_discharge_summary": "Download Discharge Summary", "download_failed": "Failed to download", + "download_json": "Download JSON", "download_latest_report": "Download Latest Report", "download_started": "Download started", + "download_the_form": "Download the questionnaire", "download_to_play": "Download to play locally", "download_type": "Download Type", "downloading": "Downloading", @@ -2111,7 +2120,6 @@ "edit_dosage_instructions": "Edit Dosage Instructions", "edit_facility": "Edit Facility", "edit_facility_details": "Edit Facility Details", - "edit_form": "Edit Form", "edit_healthcare_service": "Edit Healthcare Service", "edit_history": "Edit History", "edit_interpretation": "Edit interpretation", @@ -2134,6 +2142,7 @@ "edit_product_knowledge": "Edit Product Knowledge", "edit_profile": "Edit Profile", "edit_purchase_order": "Edit Purchase Order", + "edit_questions": "Edit Questions", "edit_queue": "Edit Queue", "edit_queue_description": "Update the queue name and settings", "edit_queue_name": "Edit queue name", @@ -2187,12 +2196,8 @@ "emergency_phone_number": "Emergency Phone Number", "empty_date_time": "--:-- --; --/--/----", "empty_template": "Empty template", - "enable_behavior": "Enable behaviour", "enable_watermark": "Enable Watermark", "enable_watermark_description": "Display a text watermark across printed pages", - "enable_when__all": "All the conditions must be met", - "enable_when__any": "Any one or more of the conditions must be met", - "enable_when_conditions": "Enable when conditions", "enabled": "Enabled", "enabled_only_for_partially_dispensed": "Enabled only for partially dispensed medications", "encounter": "Encounter", @@ -2315,7 +2320,6 @@ "end_time_required": "End time is required", "ended": "Ended", "ends": "Ends", - "ensure_conditions_are_valid": "Please ensure that conditions are still valid (or delete the condition) when modifying this question.", "enter": "Enter", "enter_2fa_code": "Enter the code generated by your 2-factor auth app", "enter_address": "Enter complete address", @@ -2325,7 +2329,6 @@ "enter_category_name": "Enter category name", "enter_category_slug": "Enter category slug", "enter_category_title": "Enter category title", - "enter_code": "Enter code", "enter_comma_separated_values": "Enter comma-separated values", "enter_conclusion": "Enter conclusion", "enter_contact_value": "Enter contact value", @@ -2333,6 +2336,7 @@ "enter_department_team_description": "Enter department/team description (optional)", "enter_department_team_name": "Enter department/team name", "enter_description": "Enter Description", + "enter_details": "Enter details", "enter_discharge_summary_advice": "Enter the discharge summary advice", "enter_display_name": "Enter display name", "enter_dosage_instructions": "Enter Dosage Instructions", @@ -2362,10 +2366,10 @@ "enter_phone_number": "Enter phone number", "enter_phone_number_to_login_register": "Enter phone number to login/register", "enter_pincode": "Enter PIN code", - "enter_position": "Enter position", "enter_priority": "Enter the priority", "enter_purchase_price": "Enter purchase price", "enter_quantity": "Enter quantity", + "enter_question_title": "Enter question title", "enter_queue_name": "Enter queue name", "enter_re_visit_allowed_days": "Enter re-visit allowed days", "enter_reason_for_archiving": "Enter reason for archiving", @@ -2392,7 +2396,6 @@ "enter_template_name_placeholder": "e.g., Common Cold, Hypertension Course", "enter_the_file_name": "Enter the file name", "enter_the_verification_code": "Enter the verification code sent to your phone", - "enter_title": "Enter Title", "enter_token_category_name": "Enter token category name", "enter_token_note": "Enter token note", "enter_user_friendly_name": "Enter a user friendly name for the device", @@ -2540,6 +2543,7 @@ "facility_linked_success": "Facility linked successfully", "facility_name": "Facility Name", "facility_not_found": "Facility Not Found", + "facility_organization": "Facility Organization", "facility_organization_type__dept": "Department", "facility_organization_type__other": "Other", "facility_organization_type__root": "Root", @@ -2566,7 +2570,6 @@ "failed_to_create_appointment": "Failed to create an appointment", "failed_to_create_category": "Failed to create category", "failed_to_create_invoice": "Failed to create invoice", - "failed_to_create_questionnaire": "Failed to create Questionnaire", "failed_to_create_queue": "Failed to create queue", "failed_to_create_template": "Failed to create template", "failed_to_crop_image_using_original_image": "Failed to crop image. Using original image", @@ -2594,7 +2597,6 @@ "failed_to_update_conclusion": "Failed to update conclusion", "failed_to_update_invoice": "Failed to update invoice", "failed_to_update_order": "Failed to update location order", - "failed_to_update_questionnaire": "Failed to update Questionnaire", "failed_to_update_queue": "Failed to update queue", "failed_to_update_service_point": "Failed to update service point", "failed_to_update_tags": "Failed to update tags", @@ -2698,7 +2700,7 @@ "footer_image": "Footer Image", "forget_password": "Forgot password?", "forget_password_instruction": "Enter your username, and if it exists, we will send you a link to reset your password.", - "form_preview": "Form Preview", + "form_properties": "Questionnaire Properties", "form_submission_discard_failed": "Failed to discard form submission", "form_submission_discarded": "Form submission discarded", "form_submissions": "Form Submissions", @@ -2709,6 +2711,7 @@ "frequency": "Frequency", "frequency_input_placeholder": "Type eg. 1-0-1, SOS, Q6H", "frequency_placeholder": "eg. 1-0-1", + "frequently_used": "Frequently used", "from": "from", "from_package": "From Package", "from_user": "from User", @@ -2764,8 +2767,8 @@ "group": "Group", "group_by": "Group by", "group_by_parent_category": "Group by Parent Category", - "group_layout_options": "Group layout options", - "group_must_have_sub_questions": "Group question must have one or more sub-questions", + "group_needs_subquestion": "Group questions must contain at least one sub-question", + "group_type_change_blocked": "Remove sub-questions before changing this question's type", "gst": "GST", "handed_over": "Handed Over", "handed_over_on": "Handed over on", @@ -2859,10 +2862,11 @@ "imaging": "Imaging", "immunisation-records": "Immunisation", "import": "Import", - "import_form": "Import Form", - "import_from_file": "Import from File", + "import_from_json_file": "Import from a JSON file", "import_from_url": "Import from URL", "import_questionnaire": "Import Questionnaire", + "import_questions": "Import Questions", + "import_questions_hint": "Add your first question or use Import Questions to bring them in from a URL or JSON.", "important_note": "Important Note", "importing": "Importing", "in": "in", @@ -3108,6 +3112,7 @@ "last_count_weeks": "Last {{count}} weeks", "last_discharge_reason": "Last Discharge Reason", "last_edited": "Last Edited", + "last_edited_by": "Last edited by {{name}}", "last_login": "Last Login", "last_modified": "Last Modified", "last_modified_by": "Last Modified By", @@ -3128,6 +3133,10 @@ "layout": "Layout", "layout_description": "Configure page size, margins, numbering and text settings", "layout_error": "There are errors in the Layout tab. Please fix the errors before saving.", + "layout_single_column": "Single column", + "layout_two_columns": "Two columns", + "layout_wide_end": "Wide end", + "layout_wide_start": "Wide start", "leaf_category": "Leaf Category", "left": "Left", "length": "Length ({{unit}})", @@ -3277,7 +3286,7 @@ "manage": "Manage", "manage_access": "Manage Assignments", "manage_activity_definitions": "Manage Activity Definitions", - "manage_and_view_questionnaires": "Manage and View Questionnaires", + "manage_and_view_questionnaires": "Manage and view questionnaires", "manage_and_view_roles": "Manage and view roles", "manage_and_view_valuesets": "Manage and View ValueSets", "manage_bed_presets": "Manage Presets of Bed", @@ -3296,7 +3305,6 @@ "manage_organization_description_other": "Add or remove organizations from this {{entityType}}", "manage_organization_one": "Manage Organization", "manage_organization_other": "Manage Organizations", - "manage_organizations_description": "Add or remove organizations from this questionnaire", "manage_patient_identifier_config": "Manage Patient Identifier Config", "manage_patient_identifiers": "Manage patient identifiers", "manage_patient_location_and_transfers": "Manage patient location and transfers", @@ -3543,9 +3551,8 @@ "move_back_to_waiting": "Move back to waiting", "move_back_to_waiting_confirmation": "Are you sure you want to move the token {{tokenNumber}} for {{patientName}} back to waiting?", "move_down": "Move Down", - "move_item": "Move Item", "move_left": "Move Left", - "move_questions": "Move Questions", + "move_n_questions": "Move {{count}} questions", "move_questions_count": "Move {{count}} questions", "move_right": "Move Right", "move_this_to_draft": "move this to draft", @@ -3556,7 +3563,6 @@ "move_to_next_page": "Move to next page", "move_to_onvif_preset": "Move to an ONVIF Preset", "move_to_previous_page": "Move to previous page", - "move_to_specific_position": "Move to specific position", "move_to_waiting": "Move to Waiting", "move_up": "Move Up", "moved_to_account_balance": "has been moved to the account balance.", @@ -3856,6 +3862,7 @@ "no_prescriptions_found_description": "No prescription in queue for this location", "no_presets": "No Presets", "no_preview_available": "No preview available", + "no_previous_versions": "No previous versions yet", "no_price_components": "No Price Components", "no_print_templates": "No print templates configured", "no_print_templates_hint": "Add a template to customize print layouts for this facility", @@ -3872,8 +3879,7 @@ "no_purchase_orders_found_description": "No purchase order found for selected status or priority.", "no_questionnaire_responses": "No Questionnaire Responses", "no_questionnaires_found": "No questionnaires found", - "no_questions_selected": "No questions selected", - "no_questions_yet": "No questions yet", + "no_questions_added_yet": "No questions added yet!", "no_queues_found": "No queues found", "no_queues_found_description": "No token queues have been created yet. Create your first queue to get started.", "no_reason_provided": "No reason provided", @@ -3882,7 +3888,6 @@ "no_remarks": "No remarks", "no_reports_found": "No reports found", "no_reports_found_description": "Try adjusting your filters (if applicable) or generate a new report.", - "no_requested_questionnaires_found": "The requested questionnaire could not be found.", "no_requests_found": "No requests found", "no_requests_found_description": "There are no supply requests matching your filters", "no_resource_requests_found": "No requests found", @@ -3968,6 +3973,7 @@ "no_valid_requests_to_dispense": "No valid requests to dispense", "no_valuesets_found": "No value sets found", "no_vendor_found": "No vendor found", + "no_visible_questions": "No questions to show — every question in this form is hidden by its visibility conditions.", "no_vitals_present": "No Vitals Monitor present in this location or facility", "no_wards_found": "No wards found", "no_wards_match_filters": "No wards match the current filters", @@ -4131,9 +4137,10 @@ "organization_permission_info": "The selected department will be granted access to view and manage this encounter", "organization_removed_successfully": "Organization removed successfully", "organization_required": "Organization is required", - "organization_selection_required": "At least one organization must be selected", "organizations": "Organizations", "organizations_fetch_error": "Error while fetching organizations", + "organizations_unavailable": "Organization settings are not available for this questionnaire in this view.", + "organizations_updated": "Organizations updated", "organizations_updated_successfully": "Organizations updated successfully", "origin": "Origin", "origin_facility": "Current facility", @@ -4148,6 +4155,7 @@ "other_details": "Other details", "other_encounters": "Other Encounters", "other_medications": "Other Medications", + "other_question_types": "Other question types", "other_tags": "Other Tags", "otp_verification_error": "Failed to verify OTP. Please try again later.", "otp_verification_success": "OTP has been verified successfully.", @@ -4223,7 +4231,9 @@ "past_encounters": "Past Encounters", "past_medications": "Past Medications", "past_prescriptions": "Past Prescriptions", + "past_revision": "Past revision", "past_symptoms": "Past Symptoms", + "paste_questionnaire_json_url": "Paste a questionnaire JSON URL", "patient": "Patient", "patient-notes": "Notes", "patient__additional-details": "Additional Details", @@ -4453,7 +4463,6 @@ "please_select_service_account_and_role": "Please select both service account and role", "please_select_state": "Please select the state", "please_select_status": "Please select Status", - "please_select_system_and_code": "Please select system and code", "please_select_user_and_role": "Please select both user and role", "please_select_user_type": "Please select the User Type", "please_upload_a_csv_file": "Please Upload A CSV file", @@ -4541,7 +4550,7 @@ "preview": "Preview", "preview_bed_names": "Preview Bed Names", "preview_cropped_image_hint": "Preview of cropped image. Click upload to save.", - "preview_form": "Preview form", + "preview_form": "Preview questionnaire", "preview_of_proportions": "Preview of proportions", "preview_proportions_error": "The sum of the ratios does not equal the total ratio. Please adjust either the total ratio or the ratios of the elements to enable preview.", "preview_proportions_zero_error": "One or more of the elements have a size ratio of 0. Please adjust the ratios of the elements to enable preview.", @@ -4644,7 +4653,6 @@ "professional_info_note_view": "View user's professional information", "profile": "Profile", "profile_picture_deleted": "Profile picture deleted", - "properties": "Properties", "property": "Property", "proposal": "Proposal", "proposed": "Proposed", @@ -4677,17 +4685,31 @@ "quantity_exceeds_available_stock": "Quantity for {{item}} (Lot: {{lot}}) selected exceeds available stock. Requested: {{requested}}, Available: {{available}}", "quantity_for_medication_selected_exceeds_available_stock": "For {{medication}} (Lot #{{lot}}), requested quantity ({{requested}}) exceeds available stock ({{available}}).", "quantity_must_be_positive": "Quantity must be positive", - "quantity_question_description": "Select the valueset of options for this quantity question", + "quantity_needs_valueset": "Quantity questions need a unit value set — pick one under Unit Options", "quantity_received": "Quantity Received", "quantity_requested": "Quantity Requested", "quantity_required": "Quantity Required", "question": "Question", - "question_actions": "Question Actions", - "question_description_placeholder": "Additional context or instructions for this question", - "question_settings": "Question Settings", - "question_settings_description": "Configure the basic behavior: mark as required, allow multiple entries, or set as read only.", - "question_structured_type_placeholder": "Select structured type", - "question_text": "Question Text", + "question_behaviour_subtitle": "Set how this question works and what data it captures.", + "question_behaviour_title": "Question Behaviour & Data Capture Settings", + "question_coding_explainer": "Bind an observation code to link this question's answers to clinical observation records. Codes are picked from the system observation valueset.", + "question_title": "Question Title", + "question_titles_required": "Every question needs a title", + "question_type": "Question Type", + "question_type__boolean": "Boolean", + "question_type__choice": "Choice", + "question_type__date": "Date", + "question_type__dateTime": "Date Time", + "question_type__decimal": "Decimal", + "question_type__display": "Display", + "question_type__group": "Group", + "question_type__integer": "Integer", + "question_type__quantity": "Quantity", + "question_type__string": "String", + "question_type__structured": "Structured", + "question_type__text": "Text", + "question_type__time": "Time", + "question_type__url": "URL", "question_type_boolean_description": "A simple yes/no question that users can answer with a single click.", "question_type_choice_description": "Presents a list of predefined options for users to choose from.", "question_type_date_description": "Lets users select a specific date from a calendar.", @@ -4696,32 +4718,31 @@ "question_type_display_description": "Shows text or instructions to users without collecting any answers.", "question_type_group_description": "A container for organizing related questions together. Must contain at least one question.", "question_type_integer_description": "Collects whole numbers (like 1, 2, 3).", - "question_type_placeholder": "Select question type", "question_type_quantity_description": "Collects a number with its unit of measurement (like 5 kg or 2 hours).", "question_type_string_description": "A short text field for brief answers like names or single-line responses.", "question_type_structured_description": "A specialized question type for collecting specific types of medical data.", "question_type_text_description": "A larger text area for longer responses like descriptions or detailed explanations.", "question_type_time_description": "Lets users select a specific time of day.", "question_type_url_description": "Collects website addresses or links.", + "question_visibility_subtitle": "This question will appear only when the selected conditions are true.", + "question_visibility_title": "Question Visibility Conditions", "questionnaire": "Questionnaire", "questionnaire_allergy_intolerance_no_encounter": "Allergy Intolerances cannot be recorded without an active encounter", "questionnaire_appointment_no_encounter": "Appointment cannot be recorded without an active encounter", "questionnaire_charge_item_no_encounter": "Charge Items cannot be recorded without an active encounter", + "questionnaire_cloned_successfully": "Questionnaire cloned successfully", "questionnaire_created_successfully": "Questionnaire created successfully!", "questionnaire_diagnosis_no_encounter": "Diagnosis cannot be recorded without an active encounter", "questionnaire_error_loading": "Failed to load questionnaire. Please try again later.", "questionnaire_files_no_encounter": "Create an encounter first to upload files", "questionnaire_form": "Questionnaire Form", "questionnaire_imported_successfully": "Questionnaire Imported Successfully", - "questionnaire_json_url": "Questionnaire JSON URL", - "questionnaire_json_url_placeholder": "https://example.com/questionnaire.json", "questionnaire_medication_request_no_encounter": "Medication requests cannot be recorded without an active encounter", "questionnaire_medication_statement_no_encounter": "Medication statements cannot be recorded without an active encounter", "questionnaire_no_encounter": "Create an encounter first in order to update it", "questionnaire_not_exist": "The questionnaire you tried to access does not exist.", "questionnaire_one": "Questionnaire", "questionnaire_other": "Questionnaires", - "questionnaire_question_dependent": "This question is used in following question(s) as an enable when condition: ", "questionnaire_response_entered_in_error_warning": "This action cannot be undone. The questionnaire response will be marked as entered in error and will appear as invalid in the system.", "questionnaire_response_logs": "Questionnaire Response Logs", "questionnaire_response_marked_as_entered_in_error": "Questionnaire response marked as entered in error", @@ -4731,12 +4752,8 @@ "questionnaire_submitted_successfully": "Questionnaire submitted successfully", "questionnaire_symptom_no_encounter": "Symptoms cannot be recorded without an active encounter", "questionnaire_updated_successfully": "Questionnaire updated successfully", - "questionniare_load_error": "Failed to load questionnaire. Please try again later.", "questions": "Questions", - "questions_copied": "Questions successfully copied", "questions_count": "Question count", - "questions_moved": "Questions successfully moved", - "questions_removed": "Questions successfully removed", "queue": "Queue", "queue_board": "Queue board", "queue_created_successfully": "Queue created successfully", @@ -4761,7 +4778,6 @@ "raise_dispatch_request": "All requests below are to be dispatched from {{location}} to other units.", "raise_stock_request": "Raise Stock Request", "raised": "Raised", - "range": "Range", "ranges": "Ranges", "ranges_valueset_conflict_error": "Ranges and value sets cannot be used together", "rate": "Rate", @@ -4884,7 +4900,6 @@ "remove_allergy": "Remove Allergy", "remove_charge_item": "Remove Charge Item", "remove_charge_item_confirmation": "Are you sure you want to remove this charge item from the invoice?", - "remove_coding": "Remove Coding", "remove_component_observation_interpretation": "Remove Component Observation Interpretation", "remove_component_observation_interpretation_description": "This will remove all component-level observation interpretations. You can then add root-level observation interpretations.", "remove_datetime": "Remove Date/Time", @@ -4894,14 +4909,12 @@ "remove_filter": "Remove filter", "remove_from_favorites": "Remove from favorites", "remove_from_preferred": "Remove from preferred", - "remove_from_selection": "Remove from selection", "remove_image": "Remove Image", "remove_item": "Remove item {{name}}", "remove_medication": "Remove Medication", "remove_medication_confirmation": "Are you sure you want to remove {{medication}}?", "remove_name": "Remove {{name}}", "remove_organization": "Remove organization", - "remove_questions": "Remove Questions", "remove_questions_confirmation": "Are you sure do you want to remove the selected questions ?", "remove_questions_count": "Remove {{count}} questions", "remove_root_observation_interpretation": "Remove Root Observation Interpretation", @@ -5174,6 +5187,7 @@ "save_current_medications_as_template": "Create a reusable template from your current medications", "save_current_medications_count": "{{count}} medication(s) will be saved", "save_current_service_requests_count": "{{count}} service request(s) will be saved", + "save_form": "Save Questionnaire", "save_investigation": "Save Investigation", "save_list": "Save List", "save_results": "Save Results", @@ -5327,7 +5341,7 @@ "search_products": "Search products", "search_products_to_add": "Search products to add", "search_purchase_orders": "Search purchase orders", - "search_questionnaires": "Search Questionnaires", + "search_questionnaires": "Search questionnaires", "search_reports": "Search reports", "search_resource": "Search Resource", "search_results": "Search results", @@ -5391,9 +5405,7 @@ "select_a_colour": "Select a color", "select_a_metric": "Select a metric", "select_a_number": "Select a number", - "select_a_question": "Select a question", "select_a_status": "Select a status", - "select_a_sub_question": "Select a sub-question", "select_a_value": "Select a value", "select_a_value_set": "Select a value set", "select_activity_definition": "Select Activity Definition", @@ -5524,7 +5536,6 @@ "select_organization_prompt_description": "Choose an organization from the sidebar to view its details", "select_organization_that_manages_this_service": "Select the organization that manages this service", "select_organization_type": "Select organization type", - "select_organizations": "Select Organizations", "select_organizations_who_can_use_template": "Select organizations whose users can use this template", "select_orientation": "Select orientation", "select_origin": "Select Origin", @@ -5556,6 +5567,7 @@ "select_product_knowledge": "Select product knowledge", "select_product_type": "Select the product type", "select_purchase_order": "Select Purchase Order", + "select_question": "Select question", "select_questionnaire": "Select Questionnaire", "select_reconciliation_type": "Select reconciliation type", "select_register_patient": "Select/Register Patient", @@ -5631,7 +5643,6 @@ "selected_items_not_in_stock": "Selected items: {{items}} are not in stock", "selected_location": "Selected Location", "selected_locations": "Selected Locations", - "selected_organizations": "Selected Organizations", "selected_payments": "Selected payments", "selected_practitioner": "Selected practitioner", "selected_slot_not_found": "Selected Slot Not Found", @@ -5725,6 +5736,7 @@ "set_as_primary_description": "Mark this queue as the primary queue for the resource", "set_average_weekly_working_hours_for": "Set Average weekly working hours for", "set_category": "Set Category", + "set_custom_options": "Set Custom Options", "set_home_facility": "Set as home facility", "set_monetary_components": "Set Monetary Components", "set_monetary_components_description": "Configure monetary components for the {{category}} category", @@ -5770,6 +5782,7 @@ "show_on_map": "Show on Map", "show_patient_details_and_specimen_id": "Show patient details and specimen ID?", "show_patient_presets": "Show Patient Presets", + "show_question_when": "Show this question when:", "show_status": "Show Status", "show_unread_notifications": "Show Unread", "showing": "Showing", @@ -5777,6 +5790,7 @@ "showing_all_medications": "Showing all medications", "showing_all_nutritional_products": "Showing all nutritional products", "showing_count_templates": "Showing {{count}} template(s)", + "showing_latest_of_versions": "Showing latest {{shown}} of {{total}} versions", "showing_x_of_y": "Showing {{x}} of {{y}}", "sidebar": "sidebar", "sidebar_description": "sidebar provides navigation to different sections", @@ -5810,7 +5824,6 @@ "something_went_wrong": "Something went wrong..!", "something_wrong": "Something went wrong! Try again later!", "sorry_there_was_an_error": "Sorry.. there was an error", - "sort_alphabetically": "Sort Alphabetically", "sort_by": "Sort By", "sort_by_latest_created": "Newest Entry First", "sort_by_latest_payment": "Newest Payment First", @@ -5916,14 +5929,27 @@ "stream_uuid": "Stream UUID", "string": "String", "stroke": "Stroke", - "structured_type": "Structured Type", + "structured_question_requires_context": "This section is available when filling the form with {{contexts}}.", + "structured_type__allergy_intolerance": "Allergies", + "structured_type__appointment": "Appointment", + "structured_type__charge_item": "Charge Items", + "structured_type__diagnosis": "Diagnosis", + "structured_type__encounter": "Encounter", + "structured_type__files": "Files", + "structured_type__medication_request": "Medication Request", + "structured_type__medication_statement": "Medication Statement", + "structured_type__service_request": "Service Request", + "structured_type__symptom": "Symptoms", + "structured_type__time_of_death": "Time of Death", "style_list": "List", "style_text": "Text", "styling": "Styling", "sub_category": "Sub Category", - "sub_questions": "sub-questions", + "sub_questions": "Sub-Questions", "sub_questions_count": "{{ count }} Sub-questions", - "sub_questions_for_group": "Sub-questions (for the \"{{ group }}\" group)", + "sub_questions_count_one": "{{count}} Sub-question", + "sub_questions_count_other": "{{count}} Sub-questions", + "sub_questions_selected": "{{count}} sub-questions selected - what would you like to do?", "sub_queue": "Sub Queue", "subcategories": "subcategories", "subject": "Subject", @@ -6061,7 +6087,6 @@ "taxes": "Taxes", "team": "Team", "technical_details": "Technical Details", - "template": "Template", "template_applied_medications": "Applied {{count}} medication(s) from template", "template_applied_service_requests": "Applied {{count}} service request(s) from template \"{{name}}\"", "template_builder": "Template Builder", @@ -6138,7 +6163,6 @@ "titrate_dosage": "Titrate Dosage", "to": "to", "to_be_conducted": "To be conducted", - "to_bottom": "To Bottom", "to_date_equal_or_after_from_date": "To date must be equal to or after from date", "to_dispatch": "To Dispatch", "to_dispatch_description": "All requests below are to be dispatched from {{location}} to other locations.", @@ -6147,12 +6171,12 @@ "to_receive": "To Receive", "to_receive_description": "These items have been requested from other stores and are pending dispatch.", "to_select": "To Select", - "to_top": "To Top", "to_view_available_slots_select_resource_and_date": "To view available slots, select a preferred resource and date.", "today": "Today", "todays_queues": "Today's Queues", "toggle_filters": "Toggle Filters", "toggle_sidebar": "Toggle Sidebar", + "toggle_sub_questions": "Toggle sub-questions", "token": "Token", "token_assigned_to_service_point": "Token has been assigned to service point", "token_booking": "Token Booking", @@ -6193,6 +6217,7 @@ "tokens_left": "{{ count }} left", "tomorrow": "Tomorrow", "top": "Top", + "top_level": "Top Level", "total": "Total", "total_amount": "Total Amount", "total_appointments": "Total Appointments", @@ -6272,6 +6297,7 @@ "type_changed_values_need_to_be_updated": "Type changed values need to be updated", "type_collected": "Type Collected", "type_d_cylinders": "D Type Cylinders", + "type_description": "Type description", "type_name": "Type Name", "type_of_encounter": "Type of Encounter", "type_patient_age": "Type Patient Age", @@ -6299,7 +6325,6 @@ "unique_help": "If enabled, this identifier must be unique across all patients.", "unique_id": "Unique Id", "unique_identifier": "Unique Identifier", - "unique_url_for_questionnaire": "A unique URL-friendly identifier for this questionnaire", "unissued_charge_items_summary_warning": "There are unbilled items in this account, turn off summary mode to review them.", "unit": "Unit", "unit_a": "Years", @@ -6308,12 +6333,16 @@ "unit_drop(s)": "Drop(s)", "unit_g": "g", "unit_h": "Hours", + "unit_hint": "Shown next to the question label in the form (e.g. mg)", "unit_mcg": "mcg", "unit_mg": "mg", "unit_min": "Minutes", "unit_ml": "ml", "unit_mo": "Months", "unit_ms": "Milliseconds", + "unit_options": "Unit Options", + "unit_options_hint": "Pick the value set of units people can answer with", + "unit_options_preview": "People filling the form will see", "unit_placeholder": "Select Unit", "unit_price": "Unit Price", "unit_price_components": "Unit Price Components", @@ -6353,11 +6382,13 @@ "unpaid_moved_to_account": "was unpaid and has been added to the account due.", "unpin_page": "Unpin/Remove from Overview", "unsaved_changes": "You have unsaved changes. Are you sure you want to leave?", + "unsaved_changes_warning": "You have unsaved changes. Leave without saving?", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", "unsubscribed_successfully": "Unsubscribed Successfully.", "unsupported_browser": "Unsupported Browser", "unsupported_browser_description": "Your browser ({{name}} version {{version}}) is not supported. Please update your browser to the latest version or switch to a supported browser for the best experience.", + "unsupported_question_type": "This question type is not supported yet", "until": "Until", "untitled_question": "Untitled Question", "unverified": "Unverified", @@ -6436,7 +6467,6 @@ "urgent": "Urgent", "uri": "URI", "url": "URL", - "url_required": "URL required", "usage": "Usage", "use": "Use", "use_auth_app": "Use authenticator app", @@ -6540,6 +6570,8 @@ "verify_using": "Verify Using", "verifying": "Verifying", "version": "Version", + "version_history": "Version history", + "versions": "Versions", "video_call": "Video Call", "video_conference_link": "Video Conference Link", "video_not_supported": "Your browser does not support video playback.", diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index adac948905e..49ae2b8f51a 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -103,12 +103,12 @@ const consultationRoutes: AppRoutes = { /> ), - "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire/:slug": - ({ facilityId, encounterId, slug, patientId }) => ( + "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire/:questionnaireId": + ({ facilityId, encounterId, questionnaireId, patientId }) => ( @@ -138,7 +138,7 @@ const consultationRoutes: AppRoutes = { ), diff --git a/src/Routers/routes/adminRoutes.tsx b/src/Routers/routes/adminRoutes.tsx index b482f32cc8c..34258f8f132 100644 --- a/src/Routers/routes/adminRoutes.tsx +++ b/src/Routers/routes/adminRoutes.tsx @@ -1,7 +1,10 @@ import { navigate } from "raviger"; -import QuestionnaireEditor from "@/components/Questionnaire/QuestionnaireEditor"; -import { QuestionnaireList } from "@/components/Questionnaire/QuestionnaireList"; +import { QuestionnaireBuilderPage } from "@/components/QuestionnaireV2/builder/QuestionnaireBuilderPage"; +import { QuestionnaireCreatePage } from "@/components/QuestionnaireV2/manage/QuestionnaireCreatePage"; +import { QuestionnaireDetailPage } from "@/components/QuestionnaireV2/manage/QuestionnaireDetailPage"; +import { QuestionnaireListPage } from "@/components/QuestionnaireV2/manage/QuestionnaireListPage"; +import { QuestionnaireRevisionPage } from "@/components/QuestionnaireV2/manage/QuestionnaireRevisionPage"; import { ValueSetEditor } from "@/components/ValueSet/ValueSetEditor"; import { ValueSetList } from "@/components/ValueSet/ValueSetList"; @@ -16,17 +19,41 @@ import { PlugConfigList } from "@/pages/Apps/PlugConfigList"; import PatientIdentifierConfigForm from "@/pages/settings/patientIdentifierConfig/PatientIdentifierConfigForm"; import PatientIdentifierConfigList from "@/pages/settings/patientIdentifierConfig/PatientIdentifierConfigList"; +const INSTANCE_SCOPE = { + authContext: "instance", + basePath: "/admin/questionnaires", +} as const; + const AdminRoutes: AppRoutes = { - "/admin/questionnaire": () => , - "/admin/questionnaire/create": () => , - "/admin/questionnaire/:slug/edit": ({ slug }) => ( - + "/admin/questionnaires": () => ( + + ), + // Must be registered before "/admin/questionnaires/:id" — raviger matches + // routes in object order, and "new" would otherwise be captured as an :id. + "/admin/questionnaires/new": () => ( + + ), + // Must be registered before "/admin/questionnaires/:id" for the same + // reason — otherwise "edit" would be captured as an :id. + "/admin/questionnaires/:id/edit": ({ id }) => ( + + ), + // Registered before "/admin/questionnaires/:id" like the routes above. + "/admin/questionnaires/:id/versions/:revisionId": ({ id, revisionId }) => ( + + ), + "/admin/questionnaires/:id": ({ id }) => ( + ), "/admin/valuesets": () => , "/admin/valuesets/create": () => ( navigate(`/admin/valuesets`)} /> ), - "/admin/valuesets/:slug/edit": ({ slug }) => , + "/admin/valuesets/:id/edit": ({ id }) => , "/admin/patient_identifier_config": () => , "/admin/patient_identifier_config/new": () => , "/admin/patient_identifier_config/:id": ({ id }) => ( diff --git a/src/components/Encounter/EncounterCommandDialog.tsx b/src/components/Encounter/EncounterCommandDialog.tsx index 27e36de8657..78903e37f59 100644 --- a/src/components/Encounter/EncounterCommandDialog.tsx +++ b/src/components/Encounter/EncounterCommandDialog.tsx @@ -24,7 +24,7 @@ import { RotateCcw, Users, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { PLUGIN_Component } from "@/PluginEngine"; import query from "@/Utils/request/query"; @@ -85,11 +85,15 @@ export function EncounterCommandDialog({ }), }); - useEffect(() => { - if (!open) { - setSearch(""); - } - }, [open]); + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + if (!nextOpen) { + setSearch(""); + } + onOpenChange(nextOpen); + }, + [onOpenChange], + ); const getShortcutDisplay = useShortcutDisplay(); @@ -182,8 +186,8 @@ export function EncounterCommandDialog({ // Handle dynamic questionnaire actions if (actionId.startsWith("questionnaire-")) { - const slug = actionId.replace("questionnaire-", ""); - navigate(buildEncounterUrl(`/questionnaire/${slug}`)); + const questionnaireId = actionId.replace("questionnaire-", ""); + navigate(buildEncounterUrl(`/questionnaire/${questionnaireId}`)); } }, [navigate, buildEncounterUrl, actions, encounter], @@ -407,10 +411,10 @@ export function EncounterCommandDialog({ group: t("questionnaire"), items: [ ...(questionnaires?.results || []).map((option) => ({ - id: `questionnaire-${option.slug}`, + id: `questionnaire-${option.id}`, label: option.title, icon: , - shortcut: getShortcutDisplay(`questionnaire-${option.slug}`), + shortcut: getShortcutDisplay(`questionnaire-${option.id}`), })), ], }); @@ -420,12 +424,10 @@ export function EncounterCommandDialog({ }, [ t, questionnaires, - search, getShortcutDisplay, canWriteSelectedEncounter, canRestartSelectedEncounter, - encounter.encounter_class, - encounter.status, + encounter, ]); const findRecentActions = useCallback( @@ -459,9 +461,9 @@ export function EncounterCommandDialog({ (actionId: string) => { addRecentAction(actionId); handleAction(actionId); - onOpenChange(false); + handleOpenChange(false); }, - [handleAction, onOpenChange, addRecentAction], + [handleAction, handleOpenChange, addRecentAction], ); const careApps = useCareApps(); @@ -471,7 +473,7 @@ export function EncounterCommandDialog({ {trigger}
diff --git a/src/components/Patient/EncounterQuestionnaire.tsx b/src/components/Patient/EncounterQuestionnaire.tsx index 31ffbe14fca..ac31aae190a 100644 --- a/src/components/Patient/EncounterQuestionnaire.tsx +++ b/src/components/Patient/EncounterQuestionnaire.tsx @@ -18,7 +18,7 @@ interface Props { facilityId?: string; patientId: string; encounterId?: string; - questionnaireSlug?: string; + questionnaireId?: string; subjectType?: string; } @@ -26,7 +26,7 @@ export default function EncounterQuestionnaire({ facilityId, patientId, encounterId, - questionnaireSlug, + questionnaireId, subjectType, }: Props) { const { t } = useTranslation(); @@ -64,7 +64,7 @@ export default function EncounterQuestionnaire({ patientId={patientId} subjectType={subjectType} encounterId={encounterId} - questionnaireSlug={questionnaireSlug} + questionnaireId={questionnaireId} onSubmit={() => { if (encounterId && facilityId) { navigate( diff --git a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx deleted file mode 100644 index fbcb7f22bac..00000000000 --- a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { Building, Check, ChevronsUpDown, Loader2, X } from "lucide-react"; -import { useNavigate } from "raviger"; -import { useState } from "react"; -import { UseFormReturn, useWatch } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; - -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import organizationApi from "@/types/organization/organizationApi"; -import type { QuestionnaireRead } from "@/types/questionnaire/questionnaire"; -import questionnaireApi from "@/types/questionnaire/questionnaireApi"; - -interface Props { - form: UseFormReturn; - trigger?: React.ReactNode; -} - -export default function CloneQuestionnaireSheet({ form, trigger }: Props) { - const navigate = useNavigate(); - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const slug = useWatch({ control: form.control, name: "slug" }); - const [newSlug, setNewSlug] = useState(slug + "-copy"); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedIds, setSelectedIds] = useState([]); - - const { data: availableOrganizations, isLoading: isLoadingOrganizations } = - useQuery({ - queryKey: ["organizations", searchQuery], - queryFn: query(organizationApi.list, { - queryParams: { - org_type: "role", - name: searchQuery || undefined, - }, - }), - enabled: open, - }); - - const { mutate: cloneQuestionnaire, isPending: isCloning } = useMutation({ - mutationFn: mutate(questionnaireApi.create, { - silent: true, - }), - onSuccess: async (data: QuestionnaireRead) => { - navigate(`/admin/questionnaire/${data.slug}/edit`); - setOpen(false); - }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, - }); - - const handleClone = () => { - if (!newSlug.trim()) { - setError("Slug is required"); - return; - } - - const clonedQuestionnaire = { - ...form.getValues(), - slug: newSlug.trim(), - id: undefined, - status: "draft" as const, - title: `${form.getValues("title")} (Clone)`, - organizations: selectedIds, - version: "1.0", // TODO: remove once backend handles versioning - }; - - cloneQuestionnaire(clonedQuestionnaire); - }; - - const handleToggleOrganization = (orgId: string) => { - setSelectedIds((current) => - current.includes(orgId) - ? current.filter((id) => id !== orgId) - : [...current, orgId], - ); - }; - - return ( - - {trigger} - - - {t("clone_questionnaire")} - - {t("clone_questionnaire_description")} - - - -
- {/* Slug Input */} -
- - { - setNewSlug(e.target.value); - setError(null); - }} - placeholder={t("slug_input_placeholder")} - /> - {error &&

{error}

} -
- - {/* Selected Organizations */} -
-

- {t("selected_organizations")} -

-
- {selectedIds.length > 0 ? ( - availableOrganizations?.results - .filter((org) => selectedIds.includes(org.id)) - .map((org) => ( - - {org.name} - - - )) - ) : ( -

- {t("no_organizations_selected")} -

- )} -
-
- - {/* Organization Selector */} -
-

{t("add_organizations")}

- - - - - - - - - {t("no_organizations_found")} - - {isLoadingOrganizations ? ( -
- -
- ) : ( - availableOrganizations?.results.map((org) => ( - handleToggleOrganization(org.id)} - className="flex items-center justify-between pr-2" - > -
- - {org.name} - {org.description && ( - - - {org.description} - - )} -
- {selectedIds.includes(org.id) && ( - - )} -
- )) - )} -
-
-
-
-
-
-
- - -
- - -
-
-
-
- ); -} diff --git a/src/components/Questionnaire/CodingEditor.tsx b/src/components/Questionnaire/CodingEditor.tsx deleted file mode 100644 index ea87bf8d685..00000000000 --- a/src/components/Questionnaire/CodingEditor.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { UpdateIcon } from "@radix-ui/react-icons"; -import { useMutation } from "@tanstack/react-query"; -import { t } from "i18next"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -import { Code } from "@/types/base/code/code"; -import { - TERMINOLOGY_SYSTEMS, - ValueSetLookupResponse, -} from "@/types/valueSet/valueSet"; -import valueSetApi from "@/types/valueSet/valueSetApi"; -import mutate from "@/Utils/request/mutate"; - -interface CodingEditorProps { - code?: Code; - name: string; - form: ReturnType>; - onChange: (code: Code | undefined) => void; -} - -export function CodingEditor({ - code, - onChange, - form, - name, -}: CodingEditorProps) { - const { mutate: verifyCode, isPending } = useMutation({ - mutationFn: mutate(valueSetApi.lookup), - onSuccess: (response: ValueSetLookupResponse) => { - if (response.metadata && code) { - onChange({ - ...code, - display: response.metadata.display, - }); - toast.success(t("code_verified_successfully")); - } - }, - onError: (error) => { - console.error(error); - form.setError(`${name}.code.display`, { - type: "manual", - message: t("code_verification_required"), - }); - toast.error(t("failed_to_verify_code")); - }, - }); - - if (!code) { - return ( -
- -
- ); - } - - return ( - - -
- - -
-
- - -
- ( - - {t("system")} - - - - - - )} - /> -
- -
-
- ( - - {t("code")} - - { - onChange({ - ...code, - code: e.target.value, - display: "", - }); - form.clearErrors([`${name}.code.display`]); - }} - placeholder={t("enter_code")} - /> - - - - )} - /> -
-
- ( - - {t("display")} - - - - - - )} - /> -
-
- -
-
-
-
- ); -} diff --git a/src/components/Questionnaire/ManageQuestionnaireOrganizationsSheet.tsx b/src/components/Questionnaire/ManageQuestionnaireOrganizationsSheet.tsx deleted file mode 100644 index 8ad2e1dcc37..00000000000 --- a/src/components/Questionnaire/ManageQuestionnaireOrganizationsSheet.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Building, Check, Loader2, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -import { cn } from "@/lib/utils"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import useBreakpoints from "@/hooks/useBreakpoints"; - -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import { Organization } from "@/types/organization/organization"; -import organizationApi from "@/types/organization/organizationApi"; -import questionnaireApi from "@/types/questionnaire/questionnaireApi"; - -interface Props { - questionnaireSlug: string; - trigger?: React.ReactNode; -} - -interface OrgSelectorProps { - title?: string; - selected: string[]; - onToggle: (orgId: string) => void; - searchQuery: string; - onSearchChange: (query: string) => void; - isLoading?: boolean; - organizations?: { - results: Array<{ - id: string; - name: string; - description?: string; - }>; - }; - className?: string; - triggerClassName?: string; -} - -export function OrgSelector({ - title, - selected, - onToggle, - searchQuery, - onSearchChange, - isLoading, - organizations, - className, - triggerClassName, -}: OrgSelectorProps) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const isMobile = useBreakpoints({ default: true, sm: false }); - - const triggerButton = ( - - ); - - const content = ( - 1}> - - - {t("no_organizations_found")} - - {isLoading ? ( -
- -
- ) : ( - organizations?.results.map((org) => ( - onToggle(org.id)} - > -
- - {org.name} - {org.description && ( - - - {org.description} - - )} -
- {selected.includes(org.id) && } -
- )) - )} -
-
-
- ); - - if (isMobile) { - return ( - { - if (!isOpen) { - onSearchChange(""); - } - setOpen(isOpen); - }} - > - {triggerButton} - -
- {content} -
-
-
- ); - } - - return ( - { - if (!isOpen) { - onSearchChange(""); - } - setOpen(isOpen); - }} - > - {triggerButton} - - {content} - - - ); -} - -export default function ManageQuestionnaireOrganizationsSheet({ - questionnaireSlug, - trigger, -}: Props) { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedOrganizations, setSelectedOrganizations] = useState< - Organization[] - >([]); - const isMobile = useBreakpoints({ default: true, sm: false }); - - const { data: organizations, isLoading } = useQuery({ - queryKey: ["questionnaire", questionnaireSlug, "organizations"], - queryFn: query(questionnaireApi.getOrganizations, { - pathParams: { slug: questionnaireSlug }, - }), - enabled: open, - }); - - const { data: availableOrganizations, isLoading: isLoadingOrganizations } = - useQuery({ - queryKey: ["organizations", searchQuery], - queryFn: query.debounced(organizationApi.list, { - queryParams: { - org_type: "role", - name: searchQuery || undefined, - }, - }), - enabled: open, - }); - - const { mutate: setOrganizations, isPending: isUpdating } = useMutation({ - mutationFn: mutate(questionnaireApi.setOrganizations, { - pathParams: { slug: questionnaireSlug }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["questionnaire", questionnaireSlug, "organizations"], - }); - toast.success(t("organizations_updated_successfully")); - setOpen(false); - }, - }); - useEffect(() => { - if (organizations?.results) { - setSelectedOrganizations(organizations.results); - } - }, [organizations?.results]); - - const handleToggleOrganization = (orgId: string) => { - setSelectedOrganizations((current) => { - const isSelected = current.some((org) => org.id === orgId); - - if (isSelected) { - return current.filter((org) => org.id !== orgId); - } else { - const allOrgs = [ - ...(organizations?.results ?? []), - ...(availableOrganizations?.results ?? []), - ]; - const orgToAdd = allOrgs.find((org) => org.id === orgId); - if (orgToAdd) { - return [...current, orgToAdd]; - } - return current; - } - }); - }; - - const handleSave = () => { - const selectedIds = selectedOrganizations.map((org) => org.id); - setOrganizations({ organizations: selectedIds }); - }; - const hasChanges = !organizations?.results - ? false - : new Set(organizations.results.map((org) => org.id)).size !== - new Set(selectedOrganizations.map((org) => org.id)).size || - !organizations.results.every((org) => - selectedOrganizations.some((selected) => selected.id === org.id), - ); - - const bodyContent = ( -
- {/* Selected Organizations */} -
-

{t("selected_organizations")}

-
- {selectedOrganizations.map((org) => ( - - {org.name} - - - ))} - {!isLoading && selectedOrganizations.length === 0 && ( -

- {t("no_organizations_selected")} -

- )} -
-
- - {/* Organization Selector */} -
-

- {t("add_organization", { count: 0 })} -

- org.id)} - onToggle={handleToggleOrganization} - searchQuery={searchQuery} - onSearchChange={setSearchQuery} - isLoading={isLoadingOrganizations} - organizations={availableOrganizations} - /> -
-
- ); - - const footerActions = ( -
- - -
- ); - - if (isMobile) { - return ( - - - {trigger || ( - - )} - - -
-
-

- {t("manage_organization", { count: 0 })} -

-

- {t("manage_organizations_description")} -

-
-
{bodyContent}
-
- {footerActions} -
-
-
-
- ); - } - - return ( - - - {trigger || ( - - )} - - - - {t("manage_organization", { count: 0 })} - - {t("manage_organizations_description")} - - - {bodyContent} - - {footerActions} - - - - ); -} diff --git a/src/components/Questionnaire/MoveQuestionDialog.tsx b/src/components/Questionnaire/MoveQuestionDialog.tsx deleted file mode 100644 index 3f9a6a55e89..00000000000 --- a/src/components/Questionnaire/MoveQuestionDialog.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import { t } from "i18next"; -import { - ArrowRight, - ChevronDown, - ChevronRight, - Copy, - Move, - X, -} from "lucide-react"; -import { Dispatch, SetStateAction, useState } from "react"; -import { toast } from "sonner"; - -import { cn } from "@/lib/utils"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; - -import { Question } from "@/types/questionnaire/question"; - -import { - addQuestionsToDestination, - copyQuestionWithNewIds, - extractGroupQuestions, - extractQuestionsByIds, - removeQuestionsFromSource, - scrollToQuestion, -} from "./utils"; - -interface QuestionTreeNodeProps { - question: Question; - selectedId: string; - onSelect: (id: string) => void; - expandedQuestions: Set; - onToggleExpand: (id: string) => void; - level?: number; -} - -function QuestionTreeNode({ - question, - selectedId, - onSelect, - expandedQuestions, - onToggleExpand, - level = 0, -}: QuestionTreeNodeProps) { - const isExpanded = expandedQuestions.has(question.id); - const isSelected = question.id === selectedId; - const hasChildren = question.questions && question.questions.length > 0; - - return ( -
-
- {hasChildren ? ( - - ) : ( - - )} -
{ - e.stopPropagation(); - onSelect(question.id); - }} - > - {question.text} -
-
- {isExpanded && hasChildren && question.questions && ( -
- {question.questions.map((child) => ( - - ))} -
- )} -
- ); -} - -interface MoveQuestionDialogProps { - questions: Question[]; - selectedQuestionIds: Set; - onSuccess: (questions: Question[]) => void; - updateSelectedQuestionIds: (ids: Set) => void; - setParentExpandedQuestions: Dispatch>>; -} - -export default function MoveQuestionDialog({ - questions, - selectedQuestionIds, - onSuccess, - updateSelectedQuestionIds, - setParentExpandedQuestions, - ...props -}: React.ComponentProps & MoveQuestionDialogProps) { - const [destinationId, setDestinationId] = useState(""); - const [expandedQuestions, setExpandedQuestions] = useState>( - new Set(), - ); - - const groupQuestions = extractGroupQuestions(questions); - - const selectedQuestions = extractQuestionsByIds( - selectedQuestionIds, - questions, - ); - - const handleToggleExpand = (id: string) => { - const newExpanded = new Set(expandedQuestions); - if (newExpanded.has(id)) { - newExpanded.delete(id); - } else { - newExpanded.add(id); - } - setExpandedQuestions(newExpanded); - }; - - const handleMove = () => { - if (!destinationId || selectedQuestions.length === 0) return; - onSuccess( - addQuestionsToDestination( - removeQuestionsFromSource([...questions], selectedQuestionIds), - destinationId, - selectedQuestions, - ), - ); - setParentExpandedQuestions((prev) => { - const newExpanded = new Set(prev); - newExpanded.add(destinationId); - return newExpanded; - }); - props.onOpenChange?.(false); - setDestinationId(""); - toast.success(t("questions_moved")); - scrollToQuestion(destinationQuestion.link_id); - }; - - const destinationQuestion = extractQuestionsByIds( - new Set([destinationId]), - questions, - )[0]; - - const handleCopy = () => { - if (!destinationId || selectedQuestions.length === 0) return; - const questionsCopy = selectedQuestions.map(copyQuestionWithNewIds); - - onSuccess( - addQuestionsToDestination([...questions], destinationId, questionsCopy), - ); - props.onOpenChange?.(false); - setDestinationId(""); - toast.success(t("questions_copied")); - scrollToQuestion(destinationQuestion.link_id); - setParentExpandedQuestions((prev) => { - const newExpanded = new Set(prev); - newExpanded.add(destinationId); - return newExpanded; - }); - }; - - const removeQuestionFromSelection = (questionId: string) => { - const newSelectedIds = new Set(selectedQuestionIds); - newSelectedIds.delete(questionId); - updateSelectedQuestionIds(newSelectedIds); - }; - - const handleCancel = () => { - props.onOpenChange?.(false); - setDestinationId(""); - }; - - return ( - - - - -
- - {t("move_questions")} -
-
-
-
-
- -
- {selectedQuestions.length !== 0 ? ( - selectedQuestions.map((question) => ( -
-
- - {question.text} -
- -
- )) - ) : ( -
- {t("no_questions_selected")} -
- )} -
-
- -
- -
- {groupQuestions.map((question) => ( - - ))} -
-
-
- - - - - - - -
-
- ); -} diff --git a/src/components/Questionnaire/OrgSelector.tsx b/src/components/Questionnaire/OrgSelector.tsx new file mode 100644 index 00000000000..21a61f57d74 --- /dev/null +++ b/src/components/Questionnaire/OrgSelector.tsx @@ -0,0 +1,154 @@ +import { Building, Check, Loader2 } from "lucide-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import useBreakpoints from "@/hooks/useBreakpoints"; + +interface OrgSelectorProps { + title?: string; + selected: string[]; + onToggle: (orgId: string) => void; + searchQuery: string; + onSearchChange: (query: string) => void; + isLoading?: boolean; + organizations?: { + results: Array<{ + id: string; + name: string; + description?: string; + }>; + }; + className?: string; + triggerClassName?: string; +} + +export function OrgSelector({ + title, + selected, + onToggle, + searchQuery, + onSearchChange, + isLoading, + organizations, + className, + triggerClassName, +}: OrgSelectorProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const isMobile = useBreakpoints({ default: true, sm: false }); + + const triggerButton = ( + + ); + + const content = ( + 1}> + + + {t("no_organizations_found")} + + {isLoading ? ( +
+ +
+ ) : ( + organizations?.results.map((org) => ( + onToggle(org.id)} + > +
+ + {org.name} + {org.description && ( + + - {org.description} + + )} +
+ {selected.includes(org.id) && } +
+ )) + )} +
+
+
+ ); + + if (isMobile) { + return ( + { + if (!isOpen) { + onSearchChange(""); + } + setOpen(isOpen); + }} + > + {triggerButton} + +
+ {content} +
+
+
+ ); + } + + return ( + { + if (!isOpen) { + onSearchChange(""); + } + setOpen(isOpen); + }} + > + {triggerButton} + + {content} + + + ); +} diff --git a/src/components/Questionnaire/QuestionActions.tsx b/src/components/Questionnaire/QuestionActions.tsx deleted file mode 100644 index 87ebb5804c9..00000000000 --- a/src/components/Questionnaire/QuestionActions.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { Move, Trash2 } from "lucide-react"; -import { Dispatch, SetStateAction, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; -import { toast } from "sonner"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; - -import { Question } from "@/types/questionnaire/question"; -import { QuestionnaireRead } from "@/types/questionnaire/questionnaire"; - -import MoveQuestionDialog from "./MoveQuestionDialog"; -import { removeQuestionsFromSource } from "./utils"; - -interface QuestionActionsProps { - selectedQuestions: Set; - questions: Question[]; - updateQuestionnaireField: ( - field: keyof QuestionnaireRead, - value: unknown, - ) => void; - onQuestionsChange: (questions: Question[]) => void; - setSelectedQuestions: (questions: Set) => void; - setExpandedQuestions: Dispatch>>; -} - -export function QuestionActions({ - selectedQuestions, - questions, - updateQuestionnaireField, - onQuestionsChange, - setSelectedQuestions, - setExpandedQuestions, -}: QuestionActionsProps) { - const { t } = useTranslation(); - const [showRemoveDialog, setShowRemoveDialog] = useState(false); - const [showMoveDialog, setShowMoveDialog] = useState(false); - - const handleRemoveQuestions = () => { - const updatedQuestions = removeQuestionsFromSource( - questions, - selectedQuestions, - ); - updateQuestionnaireField("questions", updatedQuestions); - setShowRemoveDialog(false); - setSelectedQuestions(new Set()); - toast.success(t("questions_removed")); - }; - - return ( - <> - - - - {t("question_actions")} - {selectedQuestions.size > 0 && ( - - {selectedQuestions.size} - - )} - - - - - - - - - - - - {t("confirm_remove_questions")} - - , - }} - /> - - - - - - - - - { - onQuestionsChange(questions); - setSelectedQuestions(new Set()); - }} - updateSelectedQuestionIds={setSelectedQuestions} - setParentExpandedQuestions={setExpandedQuestions} - /> - - ); -} diff --git a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx index 59c3ff5344f..4a3144485c4 100644 --- a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx @@ -115,7 +115,8 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({ if (!question.repeats) { return ( handleCodingChange(newValue, 0)} > @@ -128,7 +129,8 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({
handleCodingChange(newValue, idx)} /> @@ -156,7 +158,8 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({
diff --git a/src/components/Questionnaire/QuestionTypes/QuantityQuestion.tsx b/src/components/Questionnaire/QuestionTypes/QuantityQuestion.tsx index 4a6bbf56d15..a357c2ce3e5 100644 --- a/src/components/Questionnaire/QuestionTypes/QuantityQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuantityQuestion.tsx @@ -97,7 +97,8 @@ export const QuantityQuestion = memo(function QuantityQuestion({
diff --git a/src/components/Questionnaire/QuestionnaireEditor.tsx b/src/components/Questionnaire/QuestionnaireEditor.tsx deleted file mode 100644 index 72c29173dff..00000000000 --- a/src/components/Questionnaire/QuestionnaireEditor.tsx +++ /dev/null @@ -1,3258 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - AArrowDown, - AlertTriangle, - ChevronDown, - ChevronUp, - ChevronsDownUp, - ChevronsUpDown, - SquarePenIcon, - ViewIcon, -} from "lucide-react"; -import { useNavigate } from "raviger"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useForm, useWatch } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; -import * as z from "zod"; - -import { cn } from "@/lib/utils"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { EmptyState } from "@/components/ui/empty-state"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; - -import { AnimatedWrapper } from "@/components/Common/AnimatedWrapper"; -import { DebugPreview } from "@/components/Common/DebugPreview"; -import Loading from "@/components/Common/Loading"; -import { ScrollToTopButton } from "@/components/Common/ScrollToTop"; -import { - STRUCTURED_QUESTIONS, - StructuredQuestionType, -} from "@/components/Questionnaire/data/StructuredFormData"; - -import useBreakpoints from "@/hooks/useBreakpoints"; -import useDragAndDrop from "@/hooks/useDragAndDrop"; - -import mutate from "@/Utils/request/mutate"; -import query from "@/Utils/request/query"; -import { HTTPError } from "@/Utils/request/types"; -import { swapElements } from "@/Utils/request/utils"; -import organizationApi from "@/types/organization/organizationApi"; -import { - AnswerOption, - EnableWhen, - Question, - QuestionType, - SUPPORTED_QUESTION_TYPES, - TemplateConfig, -} from "@/types/questionnaire/question"; -import { QuestionnaireRead } from "@/types/questionnaire/questionnaire"; -import questionnaireApi from "@/types/questionnaire/questionnaireApi"; - -import { generateSlug } from "@/Utils/utils"; -import { CodingEditor } from "./CodingEditor"; -import { QuestionActions } from "./QuestionActions"; -import { QuestionnaireForm } from "./QuestionnaireForm"; -import { QuestionnaireProperties } from "./QuestionnaireProperties"; -import { SelectOrCreateValueset } from "./SelectOrCreateValueset"; -import ValueSetSelect from "./ValueSetSelect"; -import { scrollToQuestion } from "./utils"; - -interface QuestionnaireEditorProps { - slug?: string; -} -interface Organization { - id: string; - name: string; - description?: string; -} - -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 ( -
- - -
- ); -} - -const HIDE_REPEATABLE_QUESTION_TYPES = [ - "boolean", - "group", - "display", - "structured", -]; - -function findFirstErrorPath(errors: any, path: number[] = []): number[] | null { - for (let i = 0; i < errors.length; i++) { - const current = errors[i]; - const currentPath = [...path, i]; - - if (current && typeof current === "object") { - const hasOwnErrors = Object.entries(current).some(([key, value]) => { - // Ignore nested question arrays (they will be traversed separately) - if (key === "questions" && Array.isArray(value)) return false; - - // Any defined value (including objects holding a "message") indicates an error on the current node - return value !== undefined; - }); - - if (hasOwnErrors) { - return currentPath; - } - - if (Array.isArray(current.questions)) { - const subPath = findFirstErrorPath(current.questions, currentPath); - if (subPath) return subPath; - } - } - } - - return null; -} - -export default function QuestionnaireEditor({ - slug, -}: QuestionnaireEditorProps) { - const navigate = useNavigate(); - const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit"); - const [expandedQuestions, setExpandedQuestions] = useState>( - new Set(), - ); - const [selectedOrgs, setSelectedOrgs] = useState([]); - const [orgSearchQuery, setOrgSearchQuery] = useState(""); - const [orgError, setOrgError] = useState(); - const [importUrl, setImportUrl] = useState(""); - const [showImportDialog, setShowImportDialog] = useState(false); - const [showFileImportDialog, setShowFileImportDialog] = useState(false); - const [selectedQuestions, setSelectedQuestions] = useState>( - new Set(), - ); - const [selectedImportFile, setSelectedImportFile] = useState( - null, - ); - const [importedData, setImportedData] = useState( - null, - ); - const queryClient = useQueryClient(); - const [structuredTypeErrors, setStructuredTypeErrors] = useState< - Record - >({}); - const { dragOver, onDragOver, onDragLeave } = useDragAndDrop(); - const [enableWhenDependencies, setEnableWhenDependencies] = useState< - Map> - >(new Map()); - const [expandPath, setExpandPath] = useState([]); - const questionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - - const isMobile = useBreakpoints({ default: true, md: false }); - - const handleOnErrors = (error: HTTPError, fallbackMessage: string) => { - const errorData = ( - error as { - cause?: { errors: { msg: string; loc?: (string | number)[] }[] }; - } - )?.cause; - - if (!errorData?.errors) { - toast.error(fallbackMessage); - return; - } - - errorData.errors.forEach((er) => { - let fieldPath = er.loc?.join(" > "); - if (er.loc?.includes("questions")) { - const questionIndices: number[] = []; - - for (let i = 0; i < er.loc.length; i++) { - if (er.loc[i] === "questions" && typeof er.loc[i + 1] === "number") { - questionIndices.push(Number(er.loc[i + 1]) + 1); - } - } - - if (questionIndices.length > 0) { - fieldPath = `Question ${questionIndices.join(".")}`; - } - } - - const message = fieldPath ? `Error in ${fieldPath}: ${er.msg}` : er.msg; - toast.error(message); - }); - - toast.error(fallbackMessage); - }; - - const { - data: initialQuestionnaire, - isLoading, - error, - } = useQuery({ - queryKey: ["questionnaireDetail", slug], - queryFn: query(questionnaireApi.get, { - pathParams: { slug: slug! }, - }), - enabled: !!slug, - }); - - const { data: organizations } = useQuery({ - queryKey: ["questionnaire", slug, "organizations"], - queryFn: query(questionnaireApi.getOrganizations, { - pathParams: { slug: slug! }, - }), - enabled: !!slug, - }); - - const { - data: availableOrganizations, - isLoading: isLoadingAvailableOrganizations, - } = useQuery({ - queryKey: ["organizations", orgSearchQuery], - queryFn: query(organizationApi.list, { - queryParams: { - org_type: "role", - name: orgSearchQuery || undefined, - }, - }), - }); - - const { mutate: createQuestionnaire, isPending: isCreating } = useMutation({ - mutationFn: mutate(questionnaireApi.create, { - silent: true, - }), - onSuccess: (data: QuestionnaireRead) => { - toast.success(t("questionnaire_created_successfully")); - queryClient.invalidateQueries({ - queryKey: ["questionnaireDetail", data.slug], - }); - navigate(`/admin/questionnaire/${data.slug}/edit`); - }, - onError: (error) => - handleOnErrors(error, t("failed_to_create_questionnaire")), - }); - - const { mutate: updateQuestionnaire, isPending: isUpdating } = useMutation({ - mutationFn: mutate(questionnaireApi.update, { - pathParams: { slug: slug! }, - silent: true, - }), - onSuccess: (data: QuestionnaireRead) => { - toast.success(t("questionnaire_updated_successfully")); - navigate(`/admin/questionnaire/${data.slug}/edit`); - queryClient.invalidateQueries({ - queryKey: ["questionnaireDetail", data.slug], - }); - }, - onError: (error) => - handleOnErrors(error, t("failed_to_update_questionnaire")), - }); - - const { mutate: importQuestionnaire, isPending: isImporting } = useMutation({ - mutationFn: async (url: string) => { - const response = await fetch(url); - if (!response.ok) throw new Error("Failed to fetch questionnaire"); - return response.json(); - }, - onSuccess: (data) => { - setImportedData(data); - toast.success(t("questionnaire_imported_successfully")); - }, - onError: () => { - toast.error(t("failed_to_import_questionnaire")); - }, - }); - - const urlSchema = z.url(t("please enter a valid url")); - - const QuestionnaireFormPartialSchema = z.object({ - title: z.string().trim().min(1, t("field_required")), - slug: z - .string() - .trim() - .min(5, t("character_count_validation", { min: 5, max: 25 })) - .max(25, t("character_count_validation", { min: 5, max: 25 })) - .regex(/^[-\w]+$/, { - message: t("slug_format_message"), - }), - description: z.string().optional(), - questions: z.array( - z.object({ - text: z.string().trim().min(1, t("field_required")), - link_id: z.string().trim().min(1, t("field_required")), - description: z.string().optional(), - code: z - .object({ - system: z.string().optional(), - code: z.string().optional(), - display: z.string().optional(), - }) - .optional(), - - unit: z - .object({ - system: z.string().optional(), - code: z.string().optional(), - display: z.string().optional(), - }) - .optional(), - }), - ), - }); - - const [questionnaire, setQuestionnaire] = useState( - () => { - if (!slug) { - return { - id: "", - title: "", - description: "", - status: "draft", - version: "1.0", - subject_type: "encounter", - questions: [], - slug: "", - }; - } - return null; - }, - ); - - const form = useForm({ - resolver: zodResolver(QuestionnaireFormPartialSchema), - defaultValues: { - title: questionnaire?.title ?? "", - slug: questionnaire?.slug ?? "", - description: questionnaire?.description ?? "", - questions: questionnaire?.questions, - status: questionnaire?.status, - subject_type: questionnaire?.subject_type, - version: questionnaire?.version, - }, - mode: "onChange", - }); - - const { isDirty } = form.formState; - - useEffect(() => { - if (initialQuestionnaire) { - const formValues = { - title: initialQuestionnaire.title || "", - slug: initialQuestionnaire.slug || "", - description: initialQuestionnaire.description || "", - questions: initialQuestionnaire.questions, - status: initialQuestionnaire.status, - subject_type: initialQuestionnaire.subject_type, - version: initialQuestionnaire.version, - }; - - setQuestionnaire(initialQuestionnaire); - form.reset(formValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialQuestionnaire]); - - const handleToggleSelection = (questionId: string) => { - setSelectedQuestions((prev) => { - const next = new Set(prev); - if (next.has(questionId)) { - next.delete(questionId); - } else { - next.add(questionId); - } - return next; - }); - }; - const rootQuestions: Question[] = useWatch({ - control: form.control, - name: "questions", - }); - - useEffect(() => { - if (!rootQuestions) return; - const newEnableWhenDependencies = new Map< - string, - Set<{ question: Question; path: string[] }> - >(); - const processQuestions = ( - questions: Question[], - currentPath: string[] = [], - ) => { - questions.forEach((question) => { - question.enable_when?.forEach(({ question: dependentQuestionId }) => { - const deps = - newEnableWhenDependencies.get(dependentQuestionId) || new Set(); - deps.add({ - question: question, - path: [...currentPath, question.link_id], - }); - newEnableWhenDependencies.set(dependentQuestionId, deps); - }); - if (question.questions?.length) { - processQuestions(question.questions, [ - ...currentPath, - question.link_id, - ]); - } - }); - }; - - processQuestions(rootQuestions); - setEnableWhenDependencies(newEnableWhenDependencies); - }, [rootQuestions]); - - const handleEnableWhenDependentClick = (path: string[], targetId: string) => { - const rootQuestionId = path[0]; - toggleQuestionExpanded(rootQuestionId, false); - setExpandPath(path.slice(1)); - setTimeout(() => { - const element = document.getElementById(`question-${targetId}`); - if (element) element.scrollIntoView(); - setExpandPath([]); - }, 100); - }; - - if (slug && isLoading) return ; - - if (error) { - return ( - - - {t("error")} - {t("questionniare_load_error")} - - ); - } - if (!questionnaire) { - return ( - - - {t("not_found")} - - {t("no_requested_questionnaires_found")} - - - ); - } - - const updateQuestionnaireField = ( - field: keyof QuestionnaireRead, - value: unknown, - ) => { - form.setValue(field, value, { - shouldValidate: true, - shouldDirty: true, - shouldTouch: true, - }); - }; - const handleValidatedChange = ( - field: keyof QuestionnaireRead, - value: QuestionnaireRead[keyof QuestionnaireRead], - ) => { - let finalValue = value; - if (field === "slug" && typeof value === "string") { - finalValue = value.toLowerCase().replace(/[^a-z0-9_-]/g, ""); - } - - form.setValue(field as "title" | "description" | "slug", finalValue, { - shouldValidate: true, - shouldDirty: true, - }); - - if (field === "title") { - const next = generateSlug((value as string) || "", 25); - form.setValue("slug", next, { - shouldValidate: true, - shouldDirty: false, - }); - } - }; - - const updateQuestions = (newQuestions: Question[]) => { - form.setValue("questions", newQuestions, { - shouldValidate: true, - shouldDirty: true, - }); - }; - - const validateOrganizations = (): boolean => { - if (slug) { - if (!organizations?.results || organizations.results.length === 0) { - setOrgError(t("organization_selection_required")); - return false; - } - return true; - } - if (selectedOrgs.length === 0) { - setOrgError(t("organization_selection_required")); - return false; - } - setOrgError(undefined); - return true; - }; - - const validateStructuredType = (): boolean => { - let hasError = false; - const updatedErrors: Record = {}; - - rootQuestions.forEach((q) => { - if (q.type === "structured" && !q.structured_type) { - updatedErrors[q.id] = t("field_required"); - hasError = true; - } else { - updatedErrors[q.id] = undefined; - } - }); - - setStructuredTypeErrors(updatedErrors); - - return !hasError; - }; - - const handleSave = async () => { - let isValid = await form.trigger(); - const hasOrganizations = validateOrganizations(); - const hasValidStructuredType = validateStructuredType(); - - const validateQuestions = (questions: Question[], path = "questions") => { - questions.forEach((question, idx) => { - const currentPath = `${path}.${idx}`; - - if (question.code && !question.code?.display) { - form.setError(`${currentPath}.code.display`, { - type: "manual", - message: t("code_verification_required"), - }); - isValid = false; - } - - if (question.type === "group" && Array.isArray(question.questions)) { - validateQuestions(question.questions, `${currentPath}.questions`); - if (question.questions.length === 0) { - form.setError(`${currentPath}.questions`, { - type: "manual", - message: t("group_must_have_sub_questions"), - }); - isValid = false; - } - } - }); - }; - validateQuestions(rootQuestions); - - if (!isValid || !hasOrganizations || !hasValidStructuredType) { - setTimeout(() => { - const errorEntries = Object.entries(form.formState.errors); - - for (const [fieldName, error] of errorEntries) { - if (fieldName !== "questions") { - const el = document.querySelector(`[name="${fieldName}"]`); - if (el) { - el.scrollIntoView(); - break; - } - } else { - const errorPath = findFirstErrorPath(error); - if (errorPath) { - // Expand parent groups - for (let i = 0; i < errorPath.length; i++) { - const question = getQuestionByPath( - rootQuestions, - errorPath.slice(0, i + 1), - ); - if (question?.link_id) { - setExpandedQuestions((prev) => - new Set(prev).add(question.link_id), - ); - } - } - - // After expanding, scroll to the error question - setTimeout(() => { - const errorQuestion = getQuestionByPath( - rootQuestions, - errorPath, - ); - if ( - errorQuestion?.link_id && - questionRefs.current[errorQuestion.link_id] - ) { - questionRefs.current[errorQuestion.link_id]?.scrollIntoView(); - } - }, 200); - } - } - } - }, 0); // delay lets react-hook-form update `formState.errors` - return; - } - - if (slug) { - updateQuestionnaire({ - ...form.getValues(), - version: String(questionnaire.version), //TODO: remove when backend is fixed - questions: rootQuestions, - }); - } else { - createQuestionnaire({ - ...form.getValues(), - questions: rootQuestions, - organizations: selectedOrgs.map((o) => o.id), - }); - } - }; - - const handleCancel = () => { - navigate("/admin/questionnaire"); - }; - - const handleDownload = () => { - const dataStr = JSON.stringify(form.getValues(), null, 2); - const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`; - const exportFileDefaultName = `${form.getValues("slug") || "questionnaire"}.json`; - - const linkElement = document.createElement("a"); - linkElement.setAttribute("href", dataUri); - linkElement.setAttribute("download", exportFileDefaultName); - linkElement.click(); - }; - - const handleImport = async () => { - if (!importUrl) { - toast.error(t("url_required")); - return; - } - - try { - urlSchema.parse(importUrl); - importQuestionnaire(importUrl); - } catch (error) { - if (error instanceof z.ZodError) { - toast.error(error.issues[0].message); - } - } - }; - - const handleImportConfirm = () => { - if (!importedData) return; - - // Map only the necessary fields, ignoring id, created_by, tags etc. - const mappedData: Partial = { - title: importedData.title, - description: importedData.description, - status: importedData.status, - version: "1.0", - subject_type: importedData.subject_type || "encounter", - questions: - importedData.questions?.map((q: Question) => ({ - ...q, - id: crypto.randomUUID(), // Generate new IDs for questions - questions: q.questions?.map((sq: Question) => ({ - ...sq, - id: crypto.randomUUID(), // Generate new IDs for sub-questions - })), - })) || [], - slug: importedData.slug, - }; - - setQuestionnaire({ - ...form.getValues(), - ...mappedData, - }); - - form.reset({ - title: mappedData.title || "", - slug: mappedData.slug || "", - description: mappedData.description || "", - status: mappedData.status || "draft", - version: mappedData.version || "1.0", - subject_type: mappedData.subject_type || "encounter", - }); - updateQuestions(mappedData.questions || []); - - form.trigger(); - - setShowImportDialog(false); - setImportUrl(""); - setImportedData(null); - toast.success(t("questionnaire_imported_successfully")); - }; - - const toggleQuestionExpanded = ( - questionLinkId: string, - allowCollapse: boolean = true, - ) => { - setExpandedQuestions((prev) => { - const next = new Set(prev); - if (next.has(questionLinkId) && allowCollapse) { - next.delete(questionLinkId); - } else { - next.add(questionLinkId); - } - return next; - }); - }; - - const handleToggleOrganization = (orgId: string) => { - const newOrg = availableOrganizations?.results.find((o) => o.id === orgId); - setSelectedOrgs((current) => { - const newSelection = current.some((o) => o.id === orgId) - ? current.filter((o) => o.id !== orgId) - : newOrg - ? [...current, newOrg] - : current; - - // Clear error if at least one organization is selected - if (newSelection.length > 0) { - setOrgError(undefined); - } - - return newSelection; - }); - }; - - const handleAddQuestionAtIndex = (index: number) => { - const newQuestion: Question = { - id: crypto.randomUUID(), - link_id: `Q-${Date.now()}`, - text: "New Question", - type: "string", - questions: [], - }; - const newQuestions = [ - ...rootQuestions.slice(0, index), - newQuestion, - ...rootQuestions.slice(index), - ]; - updateQuestions(newQuestions); - setExpandedQuestions((prev) => new Set([...prev, newQuestion.link_id])); - setTimeout(() => { - scrollToQuestion(newQuestion.link_id); - }, 100); - }; - - const handleAddQuestion = (e: React.MouseEvent) => { - e.preventDefault(); - handleAddQuestionAtIndex(rootQuestions.length); - }; - - return ( -
- -
-
-

- {slug - ? t("edit") + " " + form.watch("title") - : t("create_questionnaire")} -

-

{form.watch("description")}

-
-
- - {slug && ( - - )} - {!slug && ( - - - - - - setShowImportDialog(true)}> - - {t("import_from_url")} - - setShowFileImportDialog(true)}> - - {t("import_from_file")} - - - - )} - -
-
- - setActiveTab(v as "edit" | "preview")} - > - - - - {t("edit_form")} - - - - {t("form_preview")} - - - -
- - - {t("navigation")} - - - - - - {isMobile && ( -
- - -
- )} -
-
- - - - {t("basic_info")} - - -
- ( - - {t("title")} - - - handleValidatedChange( - "title", - e.target.value, - ) - } - /> - - - - )} - /> - ( - - - {t("slug")}{" "} -

- {t("unique_url_for_questionnaire")} -

-
- - - handleValidatedChange( - "slug", - e.target.value, - ) - } - /> - - - - - {t("slug_format_message")} - -
- )} - /> - ( - - {t("description")} - -