diff --git a/SURVEY_FEEDBACK_INTEGRATION.md b/SURVEY_FEEDBACK_INTEGRATION.md new file mode 100644 index 0000000..0ab22e9 --- /dev/null +++ b/SURVEY_FEEDBACK_INTEGRATION.md @@ -0,0 +1,201 @@ +# Survey-Feedback Integration + +## Overview + +This document describes the integration between the Survey and Feedback systems, enabling a closed-loop workflow for handling negative survey responses. + +## Workflow + +### 1. Negative Survey Detection +When a patient completes a survey with a score below the threshold (default: 3.0/5.0): +- Survey is automatically marked as `is_negative=True` +- Staff receives notification/alert about the negative feedback +- Survey detail page displays a warning banner with follow-up actions + +### 2. Patient Contact +Staff member contacts the patient to discuss the negative feedback: +- **Action**: Click "Log Patient Contact" button on survey detail page +- **Required**: Contact notes documenting the conversation +- **Optional**: Mark issue as "Resolved" or "Explained" +- **Tracked**: Who contacted, when, and what was discussed + +### 3. Send Satisfaction Feedback +After contacting the patient, staff can send a satisfaction check: +- **Action**: Click "Send Satisfaction Feedback" button +- **Creates**: New Feedback record of type `SATISFACTION_CHECK` +- **Links**: Feedback is linked to the original survey via `related_survey` field +- **Sends**: Notification to patient with feedback form link (TODO: implement notification) + +### 4. Patient Completes Feedback +Patient receives and completes the satisfaction feedback form: +- Rates their satisfaction with how concerns were addressed +- Provides additional comments if needed +- Feedback is tracked in the system + +### 5. Close Loop +Staff reviews the satisfaction feedback: +- If satisfied → Close both survey and feedback +- If still unsatisfied → Escalate or repeat process + +## Database Schema + +### Feedback Model Changes +```python +# New field in Feedback model +related_survey = ForeignKey( + 'surveys.SurveyInstance', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='follow_up_feedbacks' +) + +# New feedback type +FeedbackType.SATISFACTION_CHECK = 'satisfaction_check', 'Satisfaction Check' +``` + +### SurveyInstance Model Changes +```python +# Patient contact tracking +patient_contacted = BooleanField(default=False) +patient_contacted_at = DateTimeField(null=True, blank=True) +patient_contacted_by = ForeignKey('accounts.User', ...) +contact_notes = TextField(blank=True) +issue_resolved = BooleanField(default=False) + +# Satisfaction feedback tracking +satisfaction_feedback_sent = BooleanField(default=False) +satisfaction_feedback_sent_at = DateTimeField(null=True, blank=True) +``` + +## API Endpoints + +### Survey Actions +- `POST /surveys/instances//log-contact/` - Log patient contact +- `POST /surveys/instances//send-satisfaction/` - Send satisfaction feedback + +### Views +- `survey_log_patient_contact(request, pk)` - Handle patient contact logging +- `survey_send_satisfaction_feedback(request, pk)` - Trigger satisfaction feedback + +## Background Tasks + +### `send_satisfaction_feedback` +```python +@shared_task(bind=True, max_retries=3) +def send_satisfaction_feedback(self, survey_instance_id, user_id=None): + """ + Creates and sends satisfaction feedback form to patient. + + - Validates patient was contacted + - Creates Feedback record linked to survey + - Sends notification to patient + - Updates survey tracking fields + """ +``` + +## UI Components + +### Survey Detail Page +For negative surveys, displays: +1. **Warning Alert**: "Action Required: Contact patient to discuss negative feedback" +2. **Contact Form**: + - Contact notes textarea + - Issue resolved checkbox + - Submit button +3. **Contact Summary** (after logging): + - Who contacted and when + - Contact notes + - Resolution status +4. **Send Satisfaction Button** (after contact): + - Disabled until patient contacted + - Triggers satisfaction feedback creation + +### Feedback Detail Page +For satisfaction check feedbacks, displays: +1. **Related Survey Card**: + - Original survey name and score + - Survey completion date + - Patient contact information + - Link to view original survey + +## Permissions + +- **Log Patient Contact**: Hospital Admin or PX Admin +- **Send Satisfaction Feedback**: Hospital Admin or PX Admin +- **View Related Survey**: Same as survey permissions + +## Audit Trail + +All actions are logged: +- `survey_patient_contacted` - When patient is contacted +- `satisfaction_feedback_sent` - When satisfaction feedback is sent +- Standard feedback audit events for the satisfaction check + +## Migration + +Run migrations to apply database changes: +```bash +python3 manage.py migrate feedback +python3 manage.py migrate surveys +``` + +## Future Enhancements + +1. **Notifications**: Implement SMS/WhatsApp/Email notifications for satisfaction feedback +2. **Automated Reminders**: Send reminders if satisfaction feedback not completed +3. **Analytics**: Track satisfaction improvement rates +4. **Templates**: Customizable satisfaction feedback templates +5. **Multi-language**: Support for Arabic satisfaction feedback forms + +## Testing Checklist + +- [ ] Create survey with negative score +- [ ] Verify warning appears on survey detail +- [ ] Log patient contact with notes +- [ ] Verify contact information is saved +- [ ] Send satisfaction feedback +- [ ] Verify feedback record is created and linked +- [ ] View feedback detail and verify survey link +- [ ] View survey detail and verify feedback link +- [ ] Test permissions for different user roles +- [ ] Verify audit logs are created + +## Related Files + +### Models +- `apps/feedback/models.py` - Feedback model with related_survey field +- `apps/surveys/models.py` - SurveyInstance model with contact tracking + +### Views +- `apps/surveys/ui_views.py` - Survey contact and satisfaction views +- `apps/feedback/views.py` - Feedback views (no changes needed) + +### Tasks +- `apps/surveys/tasks.py` - send_satisfaction_feedback task + +### Templates +- `templates/surveys/instance_detail.html` - Survey detail with follow-up actions +- `templates/feedback/feedback_detail.html` - Feedback detail with survey link + +### URLs +- `apps/surveys/urls.py` - New URL patterns for contact and satisfaction + +### Migrations +- `apps/feedback/migrations/0002_add_survey_linkage.py` +- `apps/surveys/migrations/0003_add_survey_linkage.py` + +## Configuration + +### Settings +```python +# Survey token expiry (default: 30 days) +SURVEY_TOKEN_EXPIRY_DAYS = 30 + +# Negative survey threshold (default: 3.0 out of 5.0) +# Configured per survey template in database +``` + +## Support + +For questions or issues, contact the development team or refer to the main project documentation. diff --git a/apps/feedback/migrations/0002_add_survey_linkage.py b/apps/feedback/migrations/0002_add_survey_linkage.py new file mode 100644 index 0000000..7c41696 --- /dev/null +++ b/apps/feedback/migrations/0002_add_survey_linkage.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.14 on 2025-12-28 16:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0001_initial'), + ('surveys', '0003_add_survey_linkage'), + ] + + operations = [ + migrations.AddField( + model_name='feedback', + name='related_survey', + field=models.ForeignKey(blank=True, help_text='Survey that triggered this satisfaction check feedback', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='follow_up_feedbacks', to='surveys.surveyinstance'), + ), + migrations.AlterField( + model_name='feedback', + name='feedback_type', + field=models.CharField(choices=[('compliment', 'Compliment'), ('suggestion', 'Suggestion'), ('general', 'General Feedback'), ('inquiry', 'Inquiry'), ('satisfaction_check', 'Satisfaction Check')], db_index=True, default='general', max_length=20), + ), + ] diff --git a/apps/feedback/models.py b/apps/feedback/models.py index 33d034f..edc5ad7 100644 --- a/apps/feedback/models.py +++ b/apps/feedback/models.py @@ -20,6 +20,7 @@ class FeedbackType(models.TextChoices): SUGGESTION = 'suggestion', 'Suggestion' GENERAL = 'general', 'General Feedback' INQUIRY = 'inquiry', 'Inquiry' + SATISFACTION_CHECK = 'satisfaction_check', 'Satisfaction Check' class FeedbackStatus(models.TextChoices): @@ -84,6 +85,16 @@ class Feedback(UUIDModel, TimeStampedModel): help_text="Related encounter ID if applicable" ) + # Survey linkage (for satisfaction checks after negative surveys) + related_survey = models.ForeignKey( + 'surveys.SurveyInstance', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='follow_up_feedbacks', + help_text="Survey that triggered this satisfaction check feedback" + ) + # Organization hospital = models.ForeignKey( 'organizations.Hospital', diff --git a/apps/surveys/migrations/0003_add_survey_linkage.py b/apps/surveys/migrations/0003_add_survey_linkage.py new file mode 100644 index 0000000..f229052 --- /dev/null +++ b/apps/surveys/migrations/0003_add_survey_linkage.py @@ -0,0 +1,51 @@ +# Generated by Django 5.0.14 on 2025-12-28 16:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('surveys', '0002_surveyquestion_surveyresponse_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='surveyinstance', + name='contact_notes', + field=models.TextField(blank=True, help_text='Notes from patient contact'), + ), + migrations.AddField( + model_name='surveyinstance', + name='issue_resolved', + field=models.BooleanField(default=False, help_text='Whether the issue was resolved/explained'), + ), + migrations.AddField( + model_name='surveyinstance', + name='patient_contacted', + field=models.BooleanField(default=False, help_text='Whether patient was contacted about negative survey'), + ), + migrations.AddField( + model_name='surveyinstance', + name='patient_contacted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='surveyinstance', + name='patient_contacted_by', + field=models.ForeignKey(blank=True, help_text='User who contacted the patient', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacted_surveys', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='surveyinstance', + name='satisfaction_feedback_sent', + field=models.BooleanField(default=False, help_text='Whether satisfaction feedback form was sent'), + ), + migrations.AddField( + model_name='surveyinstance', + name='satisfaction_feedback_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/apps/surveys/models.py b/apps/surveys/models.py index 4f47117..0bc4f0a 100644 --- a/apps/surveys/models.py +++ b/apps/surveys/models.py @@ -262,6 +262,36 @@ class SurveyInstance(UUIDModel, TimeStampedModel): # Metadata metadata = models.JSONField(default=dict, blank=True) + # Patient contact tracking (for negative surveys) + patient_contacted = models.BooleanField( + default=False, + help_text="Whether patient was contacted about negative survey" + ) + patient_contacted_at = models.DateTimeField(null=True, blank=True) + patient_contacted_by = models.ForeignKey( + 'accounts.User', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='contacted_surveys', + help_text="User who contacted the patient" + ) + contact_notes = models.TextField( + blank=True, + help_text="Notes from patient contact" + ) + issue_resolved = models.BooleanField( + default=False, + help_text="Whether the issue was resolved/explained" + ) + + # Satisfaction feedback tracking + satisfaction_feedback_sent = models.BooleanField( + default=False, + help_text="Whether satisfaction feedback form was sent" + ) + satisfaction_feedback_sent_at = models.DateTimeField(null=True, blank=True) + class Meta: ordering = ['-created_at'] indexes = [ diff --git a/apps/surveys/tasks.py b/apps/surveys/tasks.py index d8a8c09..1ae1e42 100644 --- a/apps/surveys/tasks.py +++ b/apps/surveys/tasks.py @@ -248,3 +248,115 @@ def process_survey_completion(survey_instance_id): error_msg = f"Error processing survey completion: {str(e)}" logger.error(error_msg, exc_info=True) return {'status': 'error', 'reason': error_msg} + + +@shared_task(bind=True, max_retries=3) +def send_satisfaction_feedback(self, survey_instance_id, user_id=None): + """ + Send satisfaction feedback form to patient after negative survey contact. + + This creates a feedback form linked to the survey and sends it to the patient + to assess their satisfaction with the resolution. + + Args: + survey_instance_id: UUID of SurveyInstance + user_id: UUID of User who initiated the feedback (optional) + + Returns: + dict: Result with feedback_id and delivery status + """ + from apps.core.services import create_audit_log + from apps.feedback.models import Feedback, FeedbackType, FeedbackResponse + from apps.notifications.services import NotificationService + from apps.surveys.models import SurveyInstance + + try: + # Get survey instance + survey_instance = SurveyInstance.objects.select_related( + 'patient', 'survey_template', 'journey_instance__hospital' + ).get(id=survey_instance_id) + + # Check if feedback already sent + if survey_instance.satisfaction_feedback_sent: + logger.warning(f"Satisfaction feedback already sent for survey {survey_instance_id}") + return {'status': 'skipped', 'reason': 'already_sent'} + + # Check if patient was contacted + if not survey_instance.patient_contacted: + logger.warning(f"Patient not contacted yet for survey {survey_instance_id}") + return {'status': 'skipped', 'reason': 'patient_not_contacted'} + + patient = survey_instance.patient + hospital = survey_instance.journey_instance.hospital if survey_instance.journey_instance else patient.primary_hospital + + # Create satisfaction feedback + with transaction.atomic(): + feedback = Feedback.objects.create( + patient=patient, + hospital=hospital, + department=survey_instance.journey_stage_instance.stage_template.department if survey_instance.journey_stage_instance else None, + feedback_type=FeedbackType.SATISFACTION_CHECK, + title=f"Satisfaction Check - {survey_instance.survey_template.name}", + message=f"Please rate your satisfaction with how we addressed your concerns regarding the survey.", + category='communication', + priority='medium', + sentiment='neutral', + status='submitted', + related_survey=survey_instance, + encounter_id=survey_instance.encounter_id, + source='system', + metadata={ + 'survey_id': str(survey_instance.id), + 'survey_score': float(survey_instance.total_score) if survey_instance.total_score else None, + 'auto_generated': True + } + ) + + # Create initial response + FeedbackResponse.objects.create( + feedback=feedback, + response_type='note', + message=f"Satisfaction feedback automatically created following negative survey (Score: {survey_instance.total_score})", + created_by_id=user_id, + is_internal=True + ) + + # Update survey instance + survey_instance.satisfaction_feedback_sent = True + survey_instance.satisfaction_feedback_sent_at = timezone.now() + survey_instance.save(update_fields=['satisfaction_feedback_sent', 'satisfaction_feedback_sent_at']) + + # Send notification to patient + # TODO: Implement feedback form link notification + # For now, we'll log it + logger.info(f"Satisfaction feedback {feedback.id} created for survey {survey_instance.id}") + + # Log audit event + create_audit_log( + event_type='satisfaction_feedback_sent', + description=f"Satisfaction feedback sent to {patient.get_full_name()} for survey {survey_instance.survey_template.name}", + content_object=feedback, + metadata={ + 'survey_id': str(survey_instance.id), + 'survey_score': float(survey_instance.total_score) if survey_instance.total_score else None, + 'feedback_id': str(feedback.id) + } + ) + + return { + 'status': 'sent', + 'feedback_id': str(feedback.id), + 'survey_id': str(survey_instance.id) + } + + except SurveyInstance.DoesNotExist: + error_msg = f"Survey instance {survey_instance_id} not found" + logger.error(error_msg) + return {'status': 'error', 'reason': error_msg} + + except Exception as e: + error_msg = f"Error sending satisfaction feedback: {str(e)}" + logger.error(error_msg, exc_info=True) + + # Retry the task + raise self.retry(exc=e, countdown=60 * (self.request.retries + 1)) diff --git a/apps/surveys/ui_views.py b/apps/surveys/ui_views.py index e7fbd79..5b8ea8a 100644 --- a/apps/surveys/ui_views.py +++ b/apps/surveys/ui_views.py @@ -1,14 +1,19 @@ """ Survey Console UI views - Server-rendered templates for survey management """ +from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q, Prefetch -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.views.decorators.http import require_http_methods +from apps.core.services import AuditService from apps.organizations.models import Department, Hospital from .models import SurveyInstance, SurveyTemplate +from .tasks import send_satisfaction_feedback @login_required @@ -198,3 +203,117 @@ def survey_template_list(request): } return render(request, 'surveys/template_list.html', context) + + +@login_required +@require_http_methods(["POST"]) +def survey_log_patient_contact(request, pk): + """ + Log patient contact for negative survey. + + This records that the user contacted the patient to discuss + the negative survey feedback. + """ + survey = get_object_or_404(SurveyInstance, pk=pk) + + # Check permission + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + if user.hospital and survey.survey_template.hospital != user.hospital: + messages.error(request, "You don't have permission to modify this survey.") + return redirect('surveys:instance_detail', pk=pk) + + # Check if survey is negative + if not survey.is_negative: + messages.warning(request, "This survey is not marked as negative.") + return redirect('surveys:instance_detail', pk=pk) + + # Get form data + contact_notes = request.POST.get('contact_notes', '') + issue_resolved = request.POST.get('issue_resolved') == 'on' + + if not contact_notes: + messages.error(request, "Please provide contact notes.") + return redirect('surveys:instance_detail', pk=pk) + + try: + # Update survey + survey.patient_contacted = True + survey.patient_contacted_at = timezone.now() + survey.patient_contacted_by = user + survey.contact_notes = contact_notes + survey.issue_resolved = issue_resolved + survey.save(update_fields=[ + 'patient_contacted', 'patient_contacted_at', + 'patient_contacted_by', 'contact_notes', 'issue_resolved' + ]) + + # Log audit + AuditService.log_event( + event_type='survey_patient_contacted', + description=f"Patient contacted for negative survey by {user.get_full_name()}", + user=user, + content_object=survey, + metadata={ + 'contact_notes': contact_notes, + 'issue_resolved': issue_resolved, + 'survey_score': float(survey.total_score) if survey.total_score else None + } + ) + + status = "resolved" if issue_resolved else "discussed" + messages.success(request, f"Patient contact logged successfully. Issue marked as {status}.") + + except Exception as e: + messages.error(request, f"Error logging patient contact: {str(e)}") + + return redirect('surveys:instance_detail', pk=pk) + + +@login_required +@require_http_methods(["POST"]) +def survey_send_satisfaction_feedback(request, pk): + """ + Send satisfaction feedback form to patient. + + This creates and sends a feedback form to assess patient satisfaction + with how their negative survey concerns were addressed. + """ + survey = get_object_or_404(SurveyInstance, pk=pk) + + # Check permission + user = request.user + if not user.is_px_admin() and not user.is_hospital_admin(): + if user.hospital and survey.survey_template.hospital != user.hospital: + messages.error(request, "You don't have permission to modify this survey.") + return redirect('surveys:instance_detail', pk=pk) + + # Check if survey is negative + if not survey.is_negative: + messages.warning(request, "This survey is not marked as negative.") + return redirect('surveys:instance_detail', pk=pk) + + # Check if patient was contacted + if not survey.patient_contacted: + messages.error(request, "Please log patient contact before sending satisfaction feedback.") + return redirect('surveys:instance_detail', pk=pk) + + # Check if already sent + if survey.satisfaction_feedback_sent: + messages.warning(request, "Satisfaction feedback has already been sent for this survey.") + return redirect('surveys:instance_detail', pk=pk) + + try: + # Trigger async task to send satisfaction feedback + send_satisfaction_feedback.delay(str(survey.id), str(user.id)) + + messages.success( + request, + "Satisfaction feedback form is being sent to the patient. " + "They will receive a link to provide their feedback." + ) + + except Exception as e: + messages.error(request, f"Error sending satisfaction feedback: {str(e)}") + + return redirect('surveys:instance_detail', pk=pk) diff --git a/apps/surveys/urls.py b/apps/surveys/urls.py index 6d8a97c..b494a7f 100644 --- a/apps/surveys/urls.py +++ b/apps/surveys/urls.py @@ -27,6 +27,8 @@ urlpatterns = [ # UI Views (authenticated) path('instances/', ui_views.survey_instance_list, name='instance_list'), path('instances//', ui_views.survey_instance_detail, name='instance_detail'), + path('instances//log-contact/', ui_views.survey_log_patient_contact, name='log_patient_contact'), + path('instances//send-satisfaction/', ui_views.survey_send_satisfaction_feedback, name='send_satisfaction_feedback'), path('templates/', ui_views.survey_template_list, name='template_list'), # Public API endpoints (no auth required) diff --git a/templates/feedback/feedback_detail.html b/templates/feedback/feedback_detail.html index 7e69e69..97c08b6 100644 --- a/templates/feedback/feedback_detail.html +++ b/templates/feedback/feedback_detail.html @@ -352,6 +352,57 @@ + + {% if feedback.related_survey %} +
+
+ Related Survey +
+
+
+ + This is a satisfaction check feedback following a negative survey response. +
+
+
Survey:
+
+ {{ feedback.related_survey.survey_template.name }} +
+
+
+
Original Score:
+
+ {{ feedback.related_survey.total_score|floatformat:1 }}/5.0 + (Negative) +
+
+
+
Survey Date:
+
+ {{ feedback.related_survey.completed_at|date:"M d, Y H:i" }} +
+
+ {% if feedback.related_survey.patient_contacted %} +
+
Patient Contacted:
+
+ + {{ feedback.related_survey.patient_contacted_at|date:"M d, Y H:i" }} +
+ By {{ feedback.related_survey.patient_contacted_by.get_full_name }} +
+
+ {% endif %} + +
+
+ {% endif %} +
Timeline & Responses
diff --git a/templates/surveys/instance_detail.html b/templates/surveys/instance_detail.html index 038fe9b..9e8d702 100644 --- a/templates/surveys/instance_detail.html +++ b/templates/surveys/instance_detail.html @@ -87,7 +87,7 @@
-
+
Patient Information
@@ -102,6 +102,94 @@
+ + {% if survey.is_negative %} +
+
+
Follow-up Actions
+
+
+ {% if not survey.patient_contacted %} +
+ + Action Required: Contact patient to discuss negative feedback. +
+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
+ {% else %} +
+ + Patient Contacted
+ By {{ survey.patient_contacted_by.get_full_name }} on {{ survey.patient_contacted_at|date:"M d, Y H:i" }} +
+ +
+ Contact Notes: +

{{ survey.contact_notes }}

+
+ +
+ Status:
+ {% if survey.issue_resolved %} + Issue Resolved + {% else %} + Issue Discussed + {% endif %} +
+ + {% if not survey.satisfaction_feedback_sent %} +
+
Send Satisfaction Feedback
+

+ Send a feedback form to the patient to assess their satisfaction with how their concerns were addressed. +

+
+ {% csrf_token %} + +
+ {% else %} +
+
+ + Satisfaction Feedback Sent
+ {{ survey.satisfaction_feedback_sent_at|date:"M d, Y H:i" }} +
+ + {% if survey.follow_up_feedbacks.exists %} +
+ Related Feedback: + {% for feedback in survey.follow_up_feedbacks.all %} + + {% endfor %} +
+ {% endif %} + {% endif %} + {% endif %} +
+
+ {% endif %}