2025-08-12 13:33:25 +03:00

1337 lines
53 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% block title %}Broadcast Communications - {{ block.super }}{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1">Broadcast Communications</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'communications:dashboard' %}">Communications</a></li>
<li class="breadcrumb-item active">Broadcasts</li>
</ol>
</nav>
</div>
<div class="btn-group">
<a href="{% url 'communications:broadcast_create' %}" class="btn btn-primary">
<i class="fas fa-bullhorn me-2"></i>Create Broadcast
</a>
<button type="button" class="btn btn-outline-success" onclick="scheduleBroadcast()">
<i class="fas fa-calendar-plus me-2"></i>Schedule Broadcast
</button>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-cog me-2"></i>Options
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="broadcastAnalytics()">
<i class="fas fa-chart-bar me-2"></i>Analytics
</a></li>
<li><a class="dropdown-item" href="#" onclick="audienceManager()">
<i class="fas fa-users me-2"></i>Audience Manager
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="exportBroadcasts()">
<i class="fas fa-download me-2"></i>Export Data
</a></li>
</ul>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4" hx-get="{% url 'communications:broadcast_stats' %}" hx-trigger="load, every 300s">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-primary text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-white-75 small">Total Broadcasts</div>
<div class="h4 mb-0" id="total-broadcasts">{{ stats.total|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-bullhorn fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
<i class="fas fa-plus me-1"></i>
{{ stats.new_this_month|default:0 }} this month
</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-success text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-white-75 small">Active Campaigns</div>
<div class="h4 mb-0" id="active-campaigns">{{ stats.active|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-play-circle fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
Currently running
</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-info text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-white-75 small">Total Recipients</div>
<div class="h4 mb-0" id="total-recipients">{{ stats.total_recipients|default:0 }}</div>
</div>
<div class="text-white-50">
<i class="fas fa-users fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
This month
</small>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card bg-gradient-warning text-white h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-white-75 small">Success Rate</div>
<div class="h4 mb-0" id="success-rate">{{ stats.success_rate|default:0 }}%</div>
</div>
<div class="text-white-50">
<i class="fas fa-check-circle fa-2x"></i>
</div>
</div>
<div class="mt-2">
<small class="text-white-75">
Last 30 days
</small>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Filters Sidebar -->
<div class="col-lg-3 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-filter me-2"></i>Filters
</h5>
</div>
<div class="card-body">
<!-- Status Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Status</label>
<select class="form-select" id="statusFilter" onchange="applyFilters()">
<option value="">All Statuses</option>
<option value="DRAFT">Draft</option>
<option value="SCHEDULED">Scheduled</option>
<option value="SENDING">Sending</option>
<option value="SENT">Sent</option>
<option value="COMPLETED">Completed</option>
<option value="FAILED">Failed</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<!-- Type Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Type</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeEmail" value="EMAIL" checked onchange="applyFilters()">
<label class="form-check-label" for="typeEmail">
<i class="fas fa-envelope me-2"></i>Email
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeSMS" value="SMS" checked onchange="applyFilters()">
<label class="form-check-label" for="typeSMS">
<i class="fas fa-sms me-2"></i>SMS
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typePush" value="PUSH" checked onchange="applyFilters()">
<label class="form-check-label" for="typePush">
<i class="fas fa-mobile-alt me-2"></i>Push Notification
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeInApp" value="IN_APP" checked onchange="applyFilters()">
<label class="form-check-label" for="typeInApp">
<i class="fas fa-bell me-2"></i>In-App
</label>
</div>
</div>
<!-- Priority Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Priority</label>
<select class="form-select" id="priorityFilter" onchange="applyFilters()">
<option value="">All Priorities</option>
<option value="CRITICAL">Critical</option>
<option value="URGENT">Urgent</option>
<option value="HIGH">High</option>
<option value="NORMAL">Normal</option>
<option value="LOW">Low</option>
</select>
</div>
<!-- Date Range Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Date Range</label>
<select class="form-select" id="dateRangeFilter" onchange="applyFilters()">
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week" selected>This Week</option>
<option value="month">This Month</option>
<option value="quarter">This Quarter</option>
<option value="year">This Year</option>
<option value="custom">Custom Range</option>
</select>
</div>
<!-- Custom Date Range -->
<div id="customDateRange" style="display: none;">
<div class="mb-2">
<label class="form-label small">From</label>
<input type="date" class="form-control form-control-sm" id="dateFrom" onchange="applyFilters()">
</div>
<div class="mb-3">
<label class="form-label small">To</label>
<input type="date" class="form-control form-control-sm" id="dateTo" onchange="applyFilters()">
</div>
</div>
<!-- Department Filter -->
<div class="mb-3">
<label class="form-label fw-bold">Department</label>
<select class="form-select" id="departmentFilter" onchange="applyFilters()">
<option value="">All Departments</option>
<option value="EMERGENCY">Emergency</option>
<option value="CARDIOLOGY">Cardiology</option>
<option value="NEUROLOGY">Neurology</option>
<option value="PEDIATRICS">Pediatrics</option>
<option value="SURGERY">Surgery</option>
<option value="RADIOLOGY">Radiology</option>
<option value="LABORATORY">Laboratory</option>
<option value="PHARMACY">Pharmacy</option>
<option value="ADMINISTRATION">Administration</option>
</select>
</div>
<!-- Clear Filters -->
<button type="button" class="btn btn-outline-secondary btn-sm w-100" onclick="clearFilters()">
<i class="fas fa-times me-2"></i>Clear All Filters
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-bolt me-2"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'communications:broadcast_create' %}" class="btn btn-outline-primary">
<i class="fas fa-plus me-2"></i>New Broadcast
</a>
<button type="button" class="btn btn-outline-success" onclick="duplicateSelected()">
<i class="fas fa-copy me-2"></i>Duplicate Selected
</button>
<button type="button" class="btn btn-outline-warning" onclick="pauseSelected()">
<i class="fas fa-pause me-2"></i>Pause Selected
</button>
<button type="button" class="btn btn-outline-danger" onclick="cancelSelected()">
<i class="fas fa-stop me-2"></i>Cancel Selected
</button>
</div>
</div>
</div>
<!-- Scheduled Broadcasts -->
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-calendar me-2"></i>Upcoming
</h5>
</div>
<div class="card-body">
<div id="upcoming-broadcasts" hx-get="{% url 'communications:upcoming_broadcasts' %}" hx-trigger="load, every 60s">
<div class="text-center">
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="col-lg-9">
<!-- Search and View Options -->
<div class="card mb-3">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search broadcasts..." id="searchInput" onkeyup="searchBroadcasts()">
<button class="btn btn-outline-secondary" type="button" onclick="searchBroadcasts()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end align-items-center">
<div class="btn-group me-3" role="group">
<input type="radio" class="btn-check" name="viewType" id="cardView" value="card" checked onchange="changeView('card')">
<label class="btn btn-outline-primary" for="cardView">
<i class="fas fa-th-large me-1"></i>Cards
</label>
<input type="radio" class="btn-check" name="viewType" id="listView" value="list" onchange="changeView('list')">
<label class="btn btn-outline-primary" for="listView">
<i class="fas fa-list me-1"></i>List
</label>
</div>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-sort me-1"></i>Sort
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('created_desc')">Newest First</a></li>
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('created_asc')">Oldest First</a></li>
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('scheduled')">By Schedule</a></li>
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('status')">By Status</a></li>
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('recipients')">By Recipients</a></li>
<li><a class="dropdown-item" href="#" onclick="sortBroadcasts('priority')">By Priority</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Broadcasts Content -->
<div id="broadcastsContent">
<!-- Card View -->
<div id="cardViewContent" class="view-content">
<div class="row" id="broadcast-cards" hx-get="{% url 'communications:broadcast_cards' %}" hx-trigger="load">
<div class="col-12 text-center p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading broadcasts...</span>
</div>
</div>
</div>
</div>
<!-- List View -->
<div id="listViewContent" class="view-content" style="display: none;">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Broadcasts</h5>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll" onchange="toggleSelectAll()">
<label class="form-check-label" for="selectAll">
Select All
</label>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="40">
<input type="checkbox" class="form-check-input" id="selectAllTable" onchange="toggleSelectAll()">
</th>
<th>Title</th>
<th>Type</th>
<th>Status</th>
<th>Recipients</th>
<th>Scheduled</th>
<th>Progress</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="broadcast-list" hx-get="{% url 'communications:broadcast_list_data' %}" hx-trigger="load">
<tr>
<td colspan="8" class="text-center p-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading broadcasts...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
Showing <span id="showing-count">0</span> of <span id="total-count">0</span> broadcasts
</div>
<nav aria-label="Broadcast pagination">
<ul class="pagination pagination-sm mb-0" id="pagination">
<!-- Pagination will be loaded dynamically -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Broadcast Detail Modal -->
<div class="modal fade" id="broadcastDetailModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Broadcast Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="broadcastDetailContent">
<!-- Content will be loaded dynamically -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="editBroadcast()">
<i class="fas fa-edit me-2"></i>Edit
</button>
<button type="button" class="btn btn-outline-success" onclick="duplicateBroadcast()">
<i class="fas fa-copy me-2"></i>Duplicate
</button>
<button type="button" class="btn btn-outline-info" onclick="viewAnalytics()">
<i class="fas fa-chart-bar me-2"></i>Analytics
</button>
</div>
</div>
</div>
</div>
<!-- Schedule Broadcast Modal -->
<div class="modal fade" id="scheduleBroadcastModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Schedule Broadcast</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Broadcast Title</label>
<input type="text" class="form-control" id="scheduleTitle" placeholder="Enter broadcast title">
</div>
<div class="mb-3">
<label class="form-label">Template</label>
<select class="form-select" id="scheduleTemplate">
<option value="">Select a template...</option>
<!-- Options will be populated dynamically -->
</select>
</div>
<div class="mb-3">
<label class="form-label">Audience</label>
<select class="form-select" id="scheduleAudience">
<option value="">Select audience...</option>
<option value="all_patients">All Patients</option>
<option value="active_patients">Active Patients</option>
<option value="department_staff">Department Staff</option>
<option value="custom">Custom Audience</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Schedule Type</label>
<select class="form-select" id="scheduleType" onchange="toggleScheduleOptions()">
<option value="immediate">Send Immediately</option>
<option value="scheduled">Schedule for Later</option>
<option value="recurring">Recurring Schedule</option>
</select>
</div>
<div id="scheduledOptions" style="display: none;">
<div class="mb-3">
<label class="form-label">Send Date & Time</label>
<input type="datetime-local" class="form-control" id="scheduleDateTime">
</div>
</div>
<div id="recurringOptions" style="display: none;">
<div class="mb-3">
<label class="form-label">Recurrence Pattern</label>
<select class="form-select" id="recurrencePattern">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="custom">Custom</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Priority</label>
<select class="form-select" id="schedulePriority">
<option value="NORMAL">Normal</option>
<option value="HIGH">High</option>
<option value="URGENT">Urgent</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="executeSchedule()">
<i class="fas fa-calendar-plus me-2"></i>Schedule Broadcast
</button>
</div>
</div>
</div>
</div>
<!-- Audience Manager Modal -->
<div class="modal fade" id="audienceManagerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Audience Manager</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="audienceManagerContent">
<!-- Content will be loaded dynamically -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="createAudience()">
<i class="fas fa-plus me-2"></i>Create New Audience
</button>
<button type="button" class="btn btn-primary" onclick="saveAudienceChanges()">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
<!-- Broadcast Analytics Modal -->
<div class="modal fade" id="broadcastAnalyticsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Broadcast Analytics</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<canvas id="deliveryChart" width="400" height="200"></canvas>
</div>
<div class="col-md-6">
<canvas id="engagementChart" width="400" height="200"></canvas>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<canvas id="timelineChart" width="400" height="200"></canvas>
</div>
<div class="col-md-6">
<canvas id="channelChart" width="400" height="200"></canvas>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="exportAnalyticsReport()">
<i class="fas fa-download me-2"></i>Export Report
</button>
</div>
</div>
</div>
</div>
<script>
let currentView = 'card';
let selectedBroadcasts = new Set();
let currentBroadcastId = null;
// Initialize broadcast list page
document.addEventListener('DOMContentLoaded', function() {
loadBroadcasts();
setupEventListeners();
setupDateRangeHandler();
});
function setupEventListeners() {
// Search input with debounce
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(searchBroadcasts, 300);
});
}
function setupDateRangeHandler() {
document.getElementById('dateRangeFilter').addEventListener('change', function() {
const customRange = document.getElementById('customDateRange');
if (this.value === 'custom') {
customRange.style.display = 'block';
} else {
customRange.style.display = 'none';
}
});
}
function loadBroadcasts(page = 1, filters = {}) {
const params = new URLSearchParams({
page: page,
view: currentView,
...filters
});
const targetSelector = currentView === 'card' ? '#broadcast-cards' : '#broadcast-list';
const url = currentView === 'card' ?
'{% url "communications:broadcast_cards" %}' :
'{% url "communications:broadcast_list_data" %}';
htmx.ajax('GET', `${url}?${params}`, {
target: targetSelector,
swap: 'innerHTML'
});
}
function changeView(viewType) {
currentView = viewType;
// Hide all view contents
document.querySelectorAll('.view-content').forEach(content => {
content.style.display = 'none';
});
// Show selected view
document.getElementById(viewType + 'ViewContent').style.display = 'block';
// Load content for the selected view
applyFilters();
}
function applyFilters() {
const filters = {
status: document.getElementById('statusFilter').value,
priority: document.getElementById('priorityFilter').value,
date_range: document.getElementById('dateRangeFilter').value,
department: document.getElementById('departmentFilter').value,
search: document.getElementById('searchInput').value
};
// Add communication types
const types = [];
['typeEmail', 'typeSMS', 'typePush', 'typeInApp'].forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox.checked) {
types.push(checkbox.value);
}
});
filters.types = types.join(',');
// Add custom date range if selected
if (filters.date_range === 'custom') {
filters.date_from = document.getElementById('dateFrom').value;
filters.date_to = document.getElementById('dateTo').value;
}
// Remove empty filters
Object.keys(filters).forEach(key => {
if (!filters[key]) delete filters[key];
});
loadBroadcasts(1, filters);
}
function clearFilters() {
document.getElementById('statusFilter').value = '';
document.getElementById('priorityFilter').value = '';
document.getElementById('dateRangeFilter').value = 'week';
document.getElementById('departmentFilter').value = '';
document.getElementById('searchInput').value = '';
document.getElementById('customDateRange').style.display = 'none';
// Reset communication type checkboxes
['typeEmail', 'typeSMS', 'typePush', 'typeInApp'].forEach(id => {
document.getElementById(id).checked = true;
});
loadBroadcasts();
}
function searchBroadcasts() {
applyFilters();
}
function sortBroadcasts(sortBy) {
const currentFilters = getCurrentFilters();
currentFilters.sort = sortBy;
loadBroadcasts(1, currentFilters);
}
function getCurrentFilters() {
return {
status: document.getElementById('statusFilter').value,
priority: document.getElementById('priorityFilter').value,
date_range: document.getElementById('dateRangeFilter').value,
department: document.getElementById('departmentFilter').value,
search: document.getElementById('searchInput').value
};
}
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAll') || document.getElementById('selectAllTable');
const broadcastCheckboxes = document.querySelectorAll('.broadcast-checkbox');
broadcastCheckboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
if (selectAllCheckbox.checked) {
selectedBroadcasts.add(checkbox.value);
} else {
selectedBroadcasts.delete(checkbox.value);
}
});
updateSelectedCount();
}
function toggleBroadcastSelection(broadcastId, checkbox) {
if (checkbox.checked) {
selectedBroadcasts.add(broadcastId);
} else {
selectedBroadcasts.delete(broadcastId);
}
updateSelectedCount();
}
function updateSelectedCount() {
const count = selectedBroadcasts.size;
const countElements = document.querySelectorAll('#selectedCount');
countElements.forEach(element => {
element.textContent = count;
});
}
function viewBroadcastDetail(broadcastId) {
currentBroadcastId = broadcastId;
fetch(`{% url 'communications:broadcast_detail' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', broadcastId))
.then(response => response.text())
.then(html => {
document.getElementById('broadcastDetailContent').innerHTML = html;
const modal = new bootstrap.Modal(document.getElementById('broadcastDetailModal'));
modal.show();
})
.catch(error => {
console.error('Error loading broadcast detail:', error);
showToast('Error', 'Failed to load broadcast details', 'error');
});
}
function editBroadcast() {
if (!currentBroadcastId) return;
window.location.href = `{% url 'communications:broadcast_edit' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', currentBroadcastId);
}
function duplicateBroadcast() {
if (!currentBroadcastId) return;
if (confirm('Create a copy of this broadcast?')) {
fetch(`{% url 'communications:broadcast_duplicate' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', currentBroadcastId), {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Broadcast duplicated successfully', 'success');
loadBroadcasts();
bootstrap.Modal.getInstance(document.getElementById('broadcastDetailModal')).hide();
} else {
showToast('Error', data.error || 'Failed to duplicate broadcast', 'error');
}
})
.catch(error => {
console.error('Error duplicating broadcast:', error);
showToast('Error', 'Failed to duplicate broadcast', 'error');
});
}
}
function pauseBroadcast(broadcastId) {
if (confirm('Pause this broadcast?')) {
fetch(`{% url 'communications:broadcast_pause' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', broadcastId), {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Broadcast paused', 'success');
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to pause broadcast', 'error');
}
})
.catch(error => {
console.error('Error pausing broadcast:', error);
showToast('Error', 'Failed to pause broadcast', 'error');
});
}
}
function resumeBroadcast(broadcastId) {
if (confirm('Resume this broadcast?')) {
fetch(`{% url 'communications:broadcast_resume' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', broadcastId), {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Broadcast resumed', 'success');
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to resume broadcast', 'error');
}
})
.catch(error => {
console.error('Error resuming broadcast:', error);
showToast('Error', 'Failed to resume broadcast', 'error');
});
}
}
function cancelBroadcast(broadcastId) {
if (confirm('Cancel this broadcast? This action cannot be undone.')) {
fetch(`{% url 'communications:broadcast_cancel' 'BROADCAST_ID' %}`.replace('BROADCAST_ID', broadcastId), {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken()
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Broadcast cancelled', 'success');
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to cancel broadcast', 'error');
}
})
.catch(error => {
console.error('Error cancelling broadcast:', error);
showToast('Error', 'Failed to cancel broadcast', 'error');
});
}
}
function duplicateSelected() {
if (selectedBroadcasts.size === 0) {
showToast('Warning', 'Please select broadcasts to duplicate', 'warning');
return;
}
if (confirm(`Duplicate ${selectedBroadcasts.size} selected broadcasts?`)) {
fetch('{% url "communications:broadcast_bulk_duplicate" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
broadcast_ids: Array.from(selectedBroadcasts)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', `${data.duplicated_count} broadcasts duplicated`, 'success');
selectedBroadcasts.clear();
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to duplicate broadcasts', 'error');
}
})
.catch(error => {
console.error('Error duplicating broadcasts:', error);
showToast('Error', 'Failed to duplicate broadcasts', 'error');
});
}
}
function pauseSelected() {
if (selectedBroadcasts.size === 0) {
showToast('Warning', 'Please select broadcasts to pause', 'warning');
return;
}
if (confirm(`Pause ${selectedBroadcasts.size} selected broadcasts?`)) {
fetch('{% url "communications:broadcast_bulk_pause" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
broadcast_ids: Array.from(selectedBroadcasts)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', `${data.paused_count} broadcasts paused`, 'success');
selectedBroadcasts.clear();
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to pause broadcasts', 'error');
}
})
.catch(error => {
console.error('Error pausing broadcasts:', error);
showToast('Error', 'Failed to pause broadcasts', 'error');
});
}
}
function cancelSelected() {
if (selectedBroadcasts.size === 0) {
showToast('Warning', 'Please select broadcasts to cancel', 'warning');
return;
}
if (confirm(`Cancel ${selectedBroadcasts.size} selected broadcasts? This action cannot be undone.`)) {
fetch('{% url "communications:broadcast_bulk_cancel" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
broadcast_ids: Array.from(selectedBroadcasts)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', `${data.cancelled_count} broadcasts cancelled`, 'success');
selectedBroadcasts.clear();
loadBroadcasts();
} else {
showToast('Error', data.error || 'Failed to cancel broadcasts', 'error');
}
})
.catch(error => {
console.error('Error cancelling broadcasts:', error);
showToast('Error', 'Failed to cancel broadcasts', 'error');
});
}
}
function scheduleBroadcast() {
// Load templates for the schedule modal
fetch('{% url "communications:template_options" %}')
.then(response => response.json())
.then(data => {
const templateSelect = document.getElementById('scheduleTemplate');
templateSelect.innerHTML = '<option value="">Select a template...</option>';
data.templates.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = template.name;
templateSelect.appendChild(option);
});
})
.catch(error => console.error('Error loading templates:', error));
const modal = new bootstrap.Modal(document.getElementById('scheduleBroadcastModal'));
modal.show();
}
function toggleScheduleOptions() {
const scheduleType = document.getElementById('scheduleType').value;
document.getElementById('scheduledOptions').style.display = scheduleType === 'scheduled' ? 'block' : 'none';
document.getElementById('recurringOptions').style.display = scheduleType === 'recurring' ? 'block' : 'none';
}
function executeSchedule() {
const scheduleData = {
title: document.getElementById('scheduleTitle').value,
template: document.getElementById('scheduleTemplate').value,
audience: document.getElementById('scheduleAudience').value,
schedule_type: document.getElementById('scheduleType').value,
priority: document.getElementById('schedulePriority').value
};
if (scheduleData.schedule_type === 'scheduled') {
scheduleData.scheduled_time = document.getElementById('scheduleDateTime').value;
} else if (scheduleData.schedule_type === 'recurring') {
scheduleData.recurrence_pattern = document.getElementById('recurrencePattern').value;
}
if (!scheduleData.title || !scheduleData.template || !scheduleData.audience) {
showToast('Warning', 'Please fill in all required fields', 'warning');
return;
}
fetch('{% url "communications:broadcast_schedule" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify(scheduleData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Success', 'Broadcast scheduled successfully', 'success');
loadBroadcasts();
bootstrap.Modal.getInstance(document.getElementById('scheduleBroadcastModal')).hide();
} else {
showToast('Error', data.error || 'Failed to schedule broadcast', 'error');
}
})
.catch(error => {
console.error('Error scheduling broadcast:', error);
showToast('Error', 'Failed to schedule broadcast', 'error');
});
}
function audienceManager() {
fetch('{% url "communications:audience_manager" %}')
.then(response => response.text())
.then(html => {
document.getElementById('audienceManagerContent').innerHTML = html;
const modal = new bootstrap.Modal(document.getElementById('audienceManagerModal'));
modal.show();
})
.catch(error => {
console.error('Error loading audience manager:', error);
showToast('Error', 'Failed to load audience manager', 'error');
});
}
function createAudience() {
// Implementation would depend on your audience creation system
showToast('Info', 'Audience creation feature coming soon', 'info');
}
function saveAudienceChanges() {
// Implementation would depend on your audience management system
showToast('Success', 'Audience changes saved', 'success');
}
function broadcastAnalytics() {
const modal = new bootstrap.Modal(document.getElementById('broadcastAnalyticsModal'));
modal.show();
// Load analytics charts
setTimeout(loadBroadcastAnalyticsCharts, 500); // Wait for modal to be fully shown
}
function loadBroadcastAnalyticsCharts() {
fetch('{% url "communications:broadcast_analytics" %}')
.then(response => response.json())
.then(data => {
createDeliveryChart(data.delivery_data);
createEngagementChart(data.engagement_data);
createTimelineChart(data.timeline_data);
createChannelChart(data.channel_data);
})
.catch(error => console.error('Error loading broadcast analytics:', error));
}
function createDeliveryChart(data) {
const ctx = document.getElementById('deliveryChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.labels,
datasets: [{
data: data.values,
backgroundColor: [
'#28a745', // Delivered
'#ffc107', // Pending
'#dc3545', // Failed
'#6c757d' // Cancelled
]
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Delivery Status'
}
}
}
});
}
function createEngagementChart(data) {
const ctx = document.getElementById('engagementChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
label: 'Engagement Rate (%)',
data: data.values,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Engagement Rates'
}
},
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
function createTimelineChart(data) {
const ctx = document.getElementById('timelineChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'Broadcasts Sent',
data: data.values,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Broadcast Timeline'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
function createChannelChart(data) {
const ctx = document.getElementById('channelChart').getContext('2d');
new Chart(ctx, {
type: 'radar',
data: {
labels: data.labels,
datasets: [{
label: 'Channel Performance',
data: data.values,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)'
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Channel Performance'
}
},
scales: {
r: {
beginAtZero: true,
max: 100
}
}
}
});
}
function viewAnalytics() {
if (!currentBroadcastId) return;
broadcastAnalytics();
}
function exportBroadcasts() {
const filters = getCurrentFilters();
const params = new URLSearchParams(filters);
window.open(`{% url 'communications:export_broadcasts' %}?${params}`, '_blank');
}
function exportAnalyticsReport() {
window.open('{% url "communications:export_broadcast_analytics" %}', '_blank');
}
function getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]').value;
}
function showToast(title, message, type) {
// Implementation depends on your toast system
console.log(`${type.toUpperCase()}: ${title} - ${message}`);
}
// Auto-refresh for real-time updates
setInterval(function() {
// Refresh stats and upcoming broadcasts
htmx.trigger('[hx-get*="broadcast_stats"]', 'refresh');
htmx.trigger('[hx-get*="upcoming_broadcasts"]', 'refresh');
}, 60000); // Refresh every minute
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
switch (e.key) {
case 'n':
window.location.href = '{% url "communications:broadcast_create" %}';
break;
case 'f':
document.getElementById('searchInput').focus();
break;
case 'c':
clearFilters();
break;
case 's':
scheduleBroadcast();
break;
case 'a':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
document.getElementById('selectAll')?.click();
}
break;
}
});
</script>
<style>
.broadcast-card {
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.broadcast-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.broadcast-card.selected {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
.progress-ring {
width: 60px;
height: 60px;
}
.progress-ring circle {
fill: transparent;
stroke: #e9ecef;
stroke-width: 4;
}
.progress-ring .progress {
stroke: #007bff;
stroke-linecap: round;
transition: stroke-dasharray 0.3s;
}
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.upcoming-broadcast {
padding: 0.75rem;
border-bottom: 1px solid #dee2e6;
transition: background-color 0.2s;
}
.upcoming-broadcast:hover {
background-color: #f8f9fa;
}
.upcoming-broadcast:last-child {
border-bottom: none;
}
.broadcast-progress {
height: 6px;
border-radius: 3px;
overflow: hidden;
background-color: #e9ecef;
}
.broadcast-progress .progress-bar {
height: 100%;
transition: width 0.3s ease;
}
</style>
{% endblock %}