1337 lines
53 KiB
HTML
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 %}
|
|
|