update survey-feedback
This commit is contained in:
parent
edfd1cfe2e
commit
ac58f5c82f
201
SURVEY_FEEDBACK_INTEGRATION.md
Normal file
201
SURVEY_FEEDBACK_INTEGRATION.md
Normal file
@ -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/<uuid>/log-contact/` - Log patient contact
|
||||||
|
- `POST /surveys/instances/<uuid>/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.
|
||||||
25
apps/feedback/migrations/0002_add_survey_linkage.py
Normal file
25
apps/feedback/migrations/0002_add_survey_linkage.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -20,6 +20,7 @@ class FeedbackType(models.TextChoices):
|
|||||||
SUGGESTION = 'suggestion', 'Suggestion'
|
SUGGESTION = 'suggestion', 'Suggestion'
|
||||||
GENERAL = 'general', 'General Feedback'
|
GENERAL = 'general', 'General Feedback'
|
||||||
INQUIRY = 'inquiry', 'Inquiry'
|
INQUIRY = 'inquiry', 'Inquiry'
|
||||||
|
SATISFACTION_CHECK = 'satisfaction_check', 'Satisfaction Check'
|
||||||
|
|
||||||
|
|
||||||
class FeedbackStatus(models.TextChoices):
|
class FeedbackStatus(models.TextChoices):
|
||||||
@ -84,6 +85,16 @@ class Feedback(UUIDModel, TimeStampedModel):
|
|||||||
help_text="Related encounter ID if applicable"
|
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
|
# Organization
|
||||||
hospital = models.ForeignKey(
|
hospital = models.ForeignKey(
|
||||||
'organizations.Hospital',
|
'organizations.Hospital',
|
||||||
|
|||||||
51
apps/surveys/migrations/0003_add_survey_linkage.py
Normal file
51
apps/surveys/migrations/0003_add_survey_linkage.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -262,6 +262,36 @@ class SurveyInstance(UUIDModel, TimeStampedModel):
|
|||||||
# Metadata
|
# Metadata
|
||||||
metadata = models.JSONField(default=dict, blank=True)
|
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:
|
class Meta:
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
indexes = [
|
indexes = [
|
||||||
|
|||||||
@ -248,3 +248,115 @@ def process_survey_completion(survey_instance_id):
|
|||||||
error_msg = f"Error processing survey completion: {str(e)}"
|
error_msg = f"Error processing survey completion: {str(e)}"
|
||||||
logger.error(error_msg, exc_info=True)
|
logger.error(error_msg, exc_info=True)
|
||||||
return {'status': 'error', 'reason': error_msg}
|
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))
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
Survey Console UI views - Server-rendered templates for survey management
|
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.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Q, Prefetch
|
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 apps.organizations.models import Department, Hospital
|
||||||
|
|
||||||
from .models import SurveyInstance, SurveyTemplate
|
from .models import SurveyInstance, SurveyTemplate
|
||||||
|
from .tasks import send_satisfaction_feedback
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -198,3 +203,117 @@ def survey_template_list(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'surveys/template_list.html', context)
|
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)
|
||||||
|
|||||||
@ -27,6 +27,8 @@ urlpatterns = [
|
|||||||
# UI Views (authenticated)
|
# UI Views (authenticated)
|
||||||
path('instances/', ui_views.survey_instance_list, name='instance_list'),
|
path('instances/', ui_views.survey_instance_list, name='instance_list'),
|
||||||
path('instances/<uuid:pk>/', ui_views.survey_instance_detail, name='instance_detail'),
|
path('instances/<uuid:pk>/', ui_views.survey_instance_detail, name='instance_detail'),
|
||||||
|
path('instances/<uuid:pk>/log-contact/', ui_views.survey_log_patient_contact, name='log_patient_contact'),
|
||||||
|
path('instances/<uuid:pk>/send-satisfaction/', ui_views.survey_send_satisfaction_feedback, name='send_satisfaction_feedback'),
|
||||||
path('templates/', ui_views.survey_template_list, name='template_list'),
|
path('templates/', ui_views.survey_template_list, name='template_list'),
|
||||||
|
|
||||||
# Public API endpoints (no auth required)
|
# Public API endpoints (no auth required)
|
||||||
|
|||||||
@ -352,6 +352,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Related Survey (if satisfaction check) -->
|
||||||
|
{% if feedback.related_survey %}
|
||||||
|
<div class="detail-card border-info">
|
||||||
|
<div class="detail-card-header bg-info text-white">
|
||||||
|
<i class="bi bi-link-45deg me-2"></i>Related Survey
|
||||||
|
</div>
|
||||||
|
<div class="detail-card-body">
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
This is a <strong>satisfaction check</strong> feedback following a negative survey response.
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">Survey:</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<strong>{{ feedback.related_survey.survey_template.name }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">Original Score:</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<span class="badge bg-danger">{{ feedback.related_survey.total_score|floatformat:1 }}/5.0</span>
|
||||||
|
<span class="text-muted ms-2">(Negative)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">Survey Date:</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ feedback.related_survey.completed_at|date:"M d, Y H:i" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if feedback.related_survey.patient_contacted %}
|
||||||
|
<div class="info-row">
|
||||||
|
<div class="info-label">Patient Contacted:</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
{{ feedback.related_survey.patient_contacted_at|date:"M d, Y H:i" }}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">By {{ feedback.related_survey.patient_contacted_by.get_full_name }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{% url 'surveys:instance_detail' feedback.related_survey.id %}"
|
||||||
|
class="btn btn-info btn-sm w-100">
|
||||||
|
<i class="bi bi-box-arrow-up-right me-1"></i>View Original Survey
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Timeline -->
|
<!-- Timeline -->
|
||||||
<div class="detail-card">
|
<div class="detail-card">
|
||||||
<div class="detail-card-header">Timeline & Responses</div>
|
<div class="detail-card-header">Timeline & Responses</div>
|
||||||
|
|||||||
@ -87,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="mb-0"><i class="bi bi-person me-2"></i>Patient Information</h6>
|
<h6 class="mb-0"><i class="bi bi-person me-2"></i>Patient Information</h6>
|
||||||
</div>
|
</div>
|
||||||
@ -102,6 +102,94 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if survey.is_negative %}
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-exclamation-triangle me-2"></i>Follow-up Actions</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if not survey.patient_contacted %}
|
||||||
|
<div class="alert alert-warning mb-3">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>Action Required:</strong> Contact patient to discuss negative feedback.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'surveys:log_patient_contact' survey.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="contact_notes" class="form-label">Contact Notes *</label>
|
||||||
|
<textarea class="form-control" id="contact_notes" name="contact_notes" rows="4" required
|
||||||
|
placeholder="Document your conversation with the patient..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="issue_resolved" name="issue_resolved">
|
||||||
|
<label class="form-check-label" for="issue_resolved">
|
||||||
|
Issue resolved or explained to patient
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-warning w-100">
|
||||||
|
<i class="bi bi-telephone me-2"></i>Log Patient Contact
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-success mb-3">
|
||||||
|
<i class="bi bi-check-circle me-2"></i>
|
||||||
|
<strong>Patient Contacted</strong><br>
|
||||||
|
<small>By {{ survey.patient_contacted_by.get_full_name }} on {{ survey.patient_contacted_at|date:"M d, Y H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Contact Notes:</strong>
|
||||||
|
<p class="mb-0 mt-2">{{ survey.contact_notes }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Status:</strong><br>
|
||||||
|
{% if survey.issue_resolved %}
|
||||||
|
<span class="badge bg-success">Issue Resolved</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">Issue Discussed</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not survey.satisfaction_feedback_sent %}
|
||||||
|
<hr>
|
||||||
|
<h6 class="mb-3">Send Satisfaction Feedback</h6>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
Send a feedback form to the patient to assess their satisfaction with how their concerns were addressed.
|
||||||
|
</p>
|
||||||
|
<form method="post" action="{% url 'surveys:send_satisfaction_feedback' survey.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-send me-2"></i>Send Satisfaction Feedback
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<hr>
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-check-circle me-2"></i>
|
||||||
|
<strong>Satisfaction Feedback Sent</strong><br>
|
||||||
|
<small>{{ survey.satisfaction_feedback_sent_at|date:"M d, Y H:i" }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if survey.follow_up_feedbacks.exists %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<strong>Related Feedback:</strong>
|
||||||
|
{% for feedback in survey.follow_up_feedbacks.all %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="{% url 'feedback:feedback_detail' feedback.id %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-chat-left-text me-1"></i>View Feedback #{{ feedback.id|slice:":8" }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user