508 lines
24 KiB
HTML
508 lines
24 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Survey Comments" %} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="mb-4">
|
|
<h2 class="mb-1">
|
|
<i class="bi bi-chat-quote text-info me-2"></i>
|
|
{% trans "Survey Comments" %}
|
|
</h2>
|
|
<p class="text-muted mb-0">
|
|
{% trans "View all patient comments with AI-powered sentiment analysis" %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h6 class="text-muted mb-1">{% trans "Total Comments" %}</h6>
|
|
<h3 class="mb-0">{{ stats.total }}</h3>
|
|
</div>
|
|
<div class="text-primary">
|
|
<i class="bi bi-chat-quote" style="font-size: 2rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h6 class="text-muted mb-1">{% trans "Positive" %}</h6>
|
|
<h3 class="mb-0 text-success">{{ stats.positive }}</h3>
|
|
</div>
|
|
<div class="text-success">
|
|
<i class="bi bi-emoji-smile" style="font-size: 2rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h6 class="text-muted mb-1">{% trans "Negative" %}</h6>
|
|
<h3 class="mb-0 text-danger">{{ stats.negative }}</h3>
|
|
</div>
|
|
<div class="text-danger">
|
|
<i class="bi bi-emoji-frown" style="font-size: 2rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<h6 class="text-muted mb-1">{% trans "AI Analyzed" %}</h6>
|
|
<h3 class="mb-0 text-info">{{ stats.analyzed }}</h3>
|
|
</div>
|
|
<div class="text-info">
|
|
<i class="bi bi-robot" style="font-size: 2rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="bi bi-pie-chart me-2"></i>
|
|
{% trans "Patient Type Distribution" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="patientTypeDistributionChart" style="min-height: 300px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="bi bi-bar-chart me-2"></i>
|
|
{% trans "Sentiment by Patient Type" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="sentimentByPatientTypeChart" style="min-height: 300px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="search" class="form-label">{% trans "Search" %}</label>
|
|
<input type="text" class="form-control" id="search" name="search"
|
|
placeholder="{% trans 'MRN, name, or comment...' %}"
|
|
value="{{ filters.search }}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="sentiment" class="form-label">{% trans "Sentiment" %}</label>
|
|
<select class="form-select" id="sentiment" name="sentiment">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="positive" {% if filters.sentiment == 'positive' %}selected{% endif %}>
|
|
{% trans "Positive" %}
|
|
</option>
|
|
<option value="negative" {% if filters.sentiment == 'negative' %}selected{% endif %}>
|
|
{% trans "Negative" %}
|
|
</option>
|
|
<option value="neutral" {% if filters.sentiment == 'neutral' %}selected{% endif %}>
|
|
{% trans "Neutral" %}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="survey_type" class="form-label">{% trans "Survey Type" %}</label>
|
|
<select class="form-select" id="survey_type" name="survey_type">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="stage" {% if filters.survey_type == 'stage' %}selected{% endif %}>
|
|
{% trans "Journey Stage" %}
|
|
</option>
|
|
<option value="complaint_resolution" {% if filters.survey_type == 'complaint_resolution' %}selected{% endif %}>
|
|
{% trans "Complaint Resolution" %}
|
|
</option>
|
|
<option value="general" {% if filters.survey_type == 'general' %}selected{% endif %}>
|
|
{% trans "General" %}
|
|
</option>
|
|
<option value="nps" {% if filters.survey_type == 'nps' %}selected{% endif %}>
|
|
{% trans "NPS" %}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="hospital" class="form-label">{% trans "Hospital" %}</label>
|
|
<select class="form-select" id="hospital" name="hospital">
|
|
<option value="">{% trans "All" %}</option>
|
|
{% for hospital in hospitals %}
|
|
<option value="{{ hospital.id }}"
|
|
{% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ hospital.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label for="patient_type" class="form-label">{% trans "Patient Type" %}</label>
|
|
<select class="form-select" id="patient_type" name="patient_type">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="outpatient" {% if filters.patient_type == 'outpatient' %}selected{% endif %}>
|
|
{% trans "Outpatient" %}
|
|
</option>
|
|
<option value="inpatient" {% if filters.patient_type == 'inpatient' %}selected{% endif %}>
|
|
{% trans "Inpatient" %}
|
|
</option>
|
|
<option value="emergency" {% if filters.patient_type == 'emergency' %}selected{% endif %}>
|
|
{% trans "Emergency" %}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label for="date_from" class="form-label">{% trans "From" %}</label>
|
|
<input type="date" class="form-control" id="date_from" name="date_from"
|
|
value="{{ filters.date_from }}">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<label for="date_to" class="form-label">{% trans "To" %}</label>
|
|
<input type="date" class="form-control" id="date_to" name="date_to"
|
|
value="{{ filters.date_to }}">
|
|
</div>
|
|
<div class="col-md-1 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
</div>
|
|
<div class="col-12 mt-2">
|
|
<a href="{% url 'surveys:survey_comments_list' %}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-x-circle me-1"></i>{% trans "Clear Filters" %}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments List -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
{% if page_obj.object_list %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>{% trans "Patient" %}</th>
|
|
<th>{% trans "Type" %}</th>
|
|
<th>{% trans "Comment" %}</th>
|
|
<th>{% trans "Sentiment" %}</th>
|
|
<th>{% trans "Survey" %}</th>
|
|
<th>{% trans "Date" %}</th>
|
|
<th>{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for survey in page_obj.object_list %}
|
|
<tr>
|
|
<td>
|
|
<strong>{{ survey.patient.get_full_name }}</strong><br>
|
|
<small class="text-muted">{{ survey.patient.mrn }}</small>
|
|
{% if survey.survey_template.hospital %}
|
|
<br><small class="text-muted">{{ survey.survey_template.hospital.name }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if survey.patient_type and survey.patient_type.code %}
|
|
<span class="badge {{ survey.patient_type.color }} text-white" title="Patient Type: {{ survey.patient_type.label }}">
|
|
<i class="bi {{ survey.patient_type.icon }} me-1"></i>
|
|
{{ survey.patient_type.label }}
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">N/A</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
|
title="{{ survey.comment }}">
|
|
{{ survey.comment }}
|
|
</div>
|
|
{% if survey.comment_analyzed %}
|
|
{% if survey.comment_analysis.emotion %}
|
|
<small class="text-muted">
|
|
<i class="bi bi-heart-pulse me-1"></i>{{ survey.comment_analysis.emotion|title }}
|
|
</small>
|
|
{% endif %}
|
|
{% else %}
|
|
<small class="text-warning">
|
|
<i class="bi bi-hourglass-split me-1"></i>{% trans "Analyzing..." %}
|
|
</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if survey.comment_analyzed and survey.comment_analysis.sentiment %}
|
|
<span class="badge {% if survey.comment_analysis.sentiment == 'positive' %}bg-success{% elif survey.comment_analysis.sentiment == 'negative' %}bg-danger{% else %}bg-secondary{% endif %}">
|
|
{{ survey.comment_analysis.sentiment|title }}
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{% trans "Pending" %}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<strong>{{ survey.survey_template.name }}</strong><br>
|
|
<small class="text-muted">{{ survey.survey_template.get_survey_type_display }}</small><br>
|
|
{% if survey.total_score %}
|
|
<small class="{% if survey.is_negative %}text-danger{% else %}text-success{% endif %}">
|
|
<strong>Score: {{ survey.total_score|floatformat:1 }}/5.0</strong>
|
|
</small>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{{ survey.completed_at|date:"M d, Y" }}<br>
|
|
<small class="text-muted">{{ survey.completed_at|time:"H:i" }}</small>
|
|
</td>
|
|
<td>
|
|
<a href="{% url 'surveys:instance_detail' survey.id %}"
|
|
class="btn btn-sm btn-outline-primary"
|
|
title="{% trans 'View Full Survey' %}">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if page_obj.has_other_pages %}
|
|
<nav class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.sentiment %}&sentiment={{ filters.sentiment }}{% endif %}{% if filters.survey_type %}&survey_type={{ filters.survey_type }}{% endif %}{% if filters.hospital %}&hospital={{ filters.hospital }}{% endif %}{% if filters.date_from %}&date_from={{ filters.date_from }}{% endif %}{% if filters.date_to %}&date_to={{ filters.date_to }}{% endif %}">
|
|
<i class="bi bi-chevron-double-left"></i>
|
|
</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.sentiment %}&sentiment={{ filters.sentiment }}{% endif %}{% if filters.survey_type %}&survey_type={{ filters.survey_type }}{% endif %}{% if filters.hospital %}&hospital={{ filters.hospital }}{% endif %}{% if filters.date_from %}&date_from={{ filters.date_from }}{% endif %}{% if filters.date_to %}&date_to={{ filters.date_to }}{% endif %}">
|
|
<i class="bi bi-chevron-left"></i>
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link"><i class="bi bi-chevron-double-left"></i></span>
|
|
</li>
|
|
<li class="page-item disabled">
|
|
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
|
</li>
|
|
{% endif %}
|
|
|
|
<li class="page-item active">
|
|
<span class="page-link">
|
|
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
|
</span>
|
|
</li>
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.sentiment %}&sentiment={{ filters.sentiment }}{% endif %}{% if filters.survey_type %}&survey_type={{ filters.survey_type }}{% endif %}{% if filters.hospital %}&hospital={{ filters.hospital }}{% endif %}{% if filters.date_from %}&date_from={{ filters.date_from }}{% endif %}{% if filters.date_to %}&date_to={{ filters.date_to }}{% endif %}">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if filters.search %}&search={{ filters.search }}{% endif %}{% if filters.sentiment %}&sentiment={{ filters.sentiment }}{% endif %}{% if filters.survey_type %}&survey_type={{ filters.survey_type }}{% endif %}{% if filters.hospital %}&hospital={{ filters.hospital }}{% endif %}{% if filters.date_from %}&date_from={{ filters.date_from }}{% endif %}{% if filters.date_to %}&date_to={{ filters.date_to }}{% endif %}">
|
|
<i class="bi bi-chevron-double-right"></i>
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
|
</li>
|
|
<li class="page-item disabled">
|
|
<span class="page-link"><i class="bi bi-chevron-double-right"></i></span>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-chat-quote" style="font-size: 3rem; color: #ccc;"></i>
|
|
<p class="text-muted mt-3 mb-0">
|
|
{% trans "No survey comments found" %}
|
|
</p>
|
|
{% if filters.search or filters.sentiment or filters.survey_type or filters.hospital or filters.date_from or filters.date_to %}
|
|
<a href="{% url 'surveys:survey_comments_list' %}" class="btn btn-outline-secondary mt-3">
|
|
<i class="bi bi-x-circle me-1"></i>{% trans "Clear Filters" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart Scripts -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Patient Type Distribution Chart (Donut Chart)
|
|
const patientTypeDistributionData = {{ patient_type_distribution_json|safe }};
|
|
|
|
if (patientTypeDistributionData && patientTypeDistributionData.length > 0) {
|
|
const patientTypeLabels = patientTypeDistributionData.map(item => item.label);
|
|
const patientTypeCounts = patientTypeDistributionData.map(item => item.count);
|
|
const patientTypeColors = ['#008FFB', '#FEB019', '#F55252', '#82868a']; // Blue, Yellow, Red, Gray
|
|
|
|
const patientTypeChartOptions = {
|
|
series: patientTypeCounts,
|
|
labels: patientTypeLabels,
|
|
colors: patientTypeColors,
|
|
chart: {
|
|
type: 'donut',
|
|
height: 300,
|
|
fontFamily: 'inherit'
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%',
|
|
labels: {
|
|
show: true,
|
|
total: {
|
|
show: true,
|
|
showAlways: true,
|
|
label: 'Total',
|
|
formatter: function (w) {
|
|
return w.globals.seriesTotals.reduce((a, b) => a + b, 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: true,
|
|
formatter: function(val, opts) {
|
|
return patientTypeDistributionData[opts.seriesIndex].percentage + '%'
|
|
}
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function(value, { series, seriesIndex, dataPointIndex, w }) {
|
|
return value + ' comments (' + patientTypeDistributionData[seriesIndex].percentage + '%)'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
};
|
|
|
|
const patientTypeChart = new ApexCharts(document.querySelector("#patientTypeDistributionChart"), patientTypeChartOptions);
|
|
patientTypeChart.render();
|
|
}
|
|
|
|
// Sentiment by Patient Type Chart (Stacked Bar Chart)
|
|
const sentimentByPatientTypeData = {{ sentiment_by_patient_type_json|safe }};
|
|
|
|
if (sentimentByPatientTypeData && sentimentByPatientTypeData.types && sentimentByPatientTypeData.types.length > 0) {
|
|
const sentimentChartOptions = {
|
|
series: [
|
|
{
|
|
name: 'Positive',
|
|
data: sentimentByPatientTypeData.positive
|
|
},
|
|
{
|
|
name: 'Negative',
|
|
data: sentimentByPatientTypeData.negative
|
|
},
|
|
{
|
|
name: 'Neutral',
|
|
data: sentimentByPatientTypeData.neutral
|
|
}
|
|
],
|
|
chart: {
|
|
type: 'bar',
|
|
height: 300,
|
|
stacked: true,
|
|
fontFamily: 'inherit',
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
plotOptions: {
|
|
bar: {
|
|
horizontal: false,
|
|
borderRadius: 4,
|
|
dataLabels: {
|
|
total: {
|
|
enabled: true,
|
|
style: {
|
|
fontSize: '13px',
|
|
fontWeight: 900
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
xaxis: {
|
|
categories: sentimentByPatientTypeData.types,
|
|
labels: {
|
|
style: {
|
|
fontSize: '12px'
|
|
}
|
|
}
|
|
},
|
|
yaxis: {
|
|
title: {
|
|
text: 'Number of Comments'
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
},
|
|
fill: {
|
|
opacity: 1
|
|
},
|
|
colors: ['#00e396', '#F55252', '#82868a'], // Green, Red, Gray
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
tooltip: {
|
|
y: {
|
|
formatter: function (value, { series, seriesIndex, dataPointIndex, w }) {
|
|
return value + ' comments'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const sentimentChart = new ApexCharts(document.querySelector("#sentimentByPatientTypeChart"), sentimentChartOptions);
|
|
sentimentChart.render();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|