561 lines
24 KiB
HTML
561 lines
24 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Data Sources - Hospital Management{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="content">
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h4>Data Sources</h4>
|
|
<h6>Manage analytics data sources and connections</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% url 'analytics:data_source_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-1"></i>Add Data Source
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row">
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="dash-widget-header">
|
|
<span class="dash-widget-icon text-primary border-primary">
|
|
<i class="fas fa-database"></i>
|
|
</span>
|
|
<div class="dash-count">
|
|
<h3>{{ total_sources|default:"0" }}</h3>
|
|
</div>
|
|
</div>
|
|
<div class="dash-widget-info">
|
|
<h6 class="text-muted">Total Sources</h6>
|
|
<div class="progress progress-sm">
|
|
<div class="progress-bar bg-primary" style="width: 100%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="dash-widget-header">
|
|
<span class="dash-widget-icon text-success border-success">
|
|
<i class="fas fa-check-circle"></i>
|
|
</span>
|
|
<div class="dash-count">
|
|
<h3>{{ active_sources|default:"0" }}</h3>
|
|
</div>
|
|
</div>
|
|
<div class="dash-widget-info">
|
|
<h6 class="text-muted">Active Sources</h6>
|
|
<div class="progress progress-sm">
|
|
<div class="progress-bar bg-success" style="width: {{ active_percentage|default:0 }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="dash-widget-header">
|
|
<span class="dash-widget-icon text-warning border-warning">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</span>
|
|
<div class="dash-count">
|
|
<h3>{{ error_sources|default:"0" }}</h3>
|
|
</div>
|
|
</div>
|
|
<div class="dash-widget-info">
|
|
<h6 class="text-muted">With Errors</h6>
|
|
<div class="progress progress-sm">
|
|
<div class="progress-bar bg-warning" style="width: {{ error_percentage|default:0 }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="dash-widget-header">
|
|
<span class="dash-widget-icon text-info border-info">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</span>
|
|
<div class="dash-count">
|
|
<h3>{{ syncing_sources|default:"0" }}</h3>
|
|
</div>
|
|
</div>
|
|
<div class="dash-widget-info">
|
|
<h6 class="text-muted">Syncing</h6>
|
|
<div class="progress progress-sm">
|
|
<div class="progress-bar bg-info" style="width: {{ syncing_percentage|default:0 }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters and Search -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="form-group">
|
|
<label class="form-label">Source Type</label>
|
|
<select class="form-select" id="sourceTypeFilter">
|
|
<option value="">All Types</option>
|
|
<option value="database">Database</option>
|
|
<option value="api">API</option>
|
|
<option value="file">File</option>
|
|
<option value="stream">Stream</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-sm-6">
|
|
<div class="form-group">
|
|
<label class="form-label">Status</label>
|
|
<select class="form-select" id="statusFilter">
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
<option value="error">Error</option>
|
|
<option value="syncing">Syncing</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4 col-sm-6">
|
|
<div class="form-group">
|
|
<label class="form-label">Search</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="Search data sources...">
|
|
<button class="btn btn-outline-secondary" type="button" id="searchBtn">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-sm-6">
|
|
<div class="form-group">
|
|
<label class="form-label"> </label>
|
|
<div class="d-grid">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearFilters()">
|
|
<i class="fas fa-times me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Sources Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">
|
|
<i class="fas fa-database me-2"></i>Data Sources
|
|
</h5>
|
|
<div class="card-tools">
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshSources()">
|
|
<i class="fas fa-sync-alt me-1"></i>Refresh
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="exportSources()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" id="dataSourcesTable">
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="selectAll">
|
|
</div>
|
|
</th>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Status</th>
|
|
<th>Last Sync</th>
|
|
<th>Records</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for source in data_sources %}
|
|
<tr>
|
|
<td>
|
|
<div class="form-check">
|
|
<input class="form-check-input row-checkbox" type="checkbox" value="{{ source.id }}">
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="source-icon me-2">
|
|
{% if source.source_type == 'database' %}
|
|
<i class="fas fa-database text-primary"></i>
|
|
{% elif source.source_type == 'api' %}
|
|
<i class="fas fa-plug text-info"></i>
|
|
{% elif source.source_type == 'file' %}
|
|
<i class="fas fa-file text-warning"></i>
|
|
{% elif source.source_type == 'stream' %}
|
|
<i class="fas fa-stream text-success"></i>
|
|
{% else %}
|
|
<i class="fas fa-question-circle text-muted"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<h6 class="mb-0">{{ source.name }}</h6>
|
|
<small class="text-muted">{{ source.description|truncatechars:50 }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ source.get_source_type_display }}</span>
|
|
</td>
|
|
<td>
|
|
{% if source.status == 'active' %}
|
|
<span class="badge bg-success">
|
|
<i class="fas fa-circle me-1" style="font-size: 8px;"></i>Active
|
|
</span>
|
|
{% elif source.status == 'inactive' %}
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-circle me-1" style="font-size: 8px;"></i>Inactive
|
|
</span>
|
|
{% elif source.status == 'error' %}
|
|
<span class="badge bg-danger">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>Error
|
|
</span>
|
|
{% elif source.status == 'syncing' %}
|
|
<span class="badge bg-info">
|
|
<i class="fas fa-sync-alt fa-spin me-1"></i>Syncing
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if source.last_sync %}
|
|
<span class="text-muted">{{ source.last_sync|timesince }} ago</span>
|
|
{% else %}
|
|
<span class="text-muted">Never</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="text-muted">{{ source.record_count|default:"0"|floatformat:0 }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="text-muted">{{ source.created_at|date:"M d, Y" }}</span>
|
|
</td>
|
|
<td>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'analytics:data_source_detail' source.pk %}">
|
|
<i class="fas fa-eye me-2"></i>View Details
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'analytics:data_source_update' source.pk %}">
|
|
<i class="fas fa-edit me-2"></i>Edit
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" onclick="testConnection({{ source.id }})">
|
|
<i class="fas fa-plug me-2"></i>Test Connection
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" onclick="syncSource({{ source.id }})">
|
|
<i class="fas fa-sync-alt me-2"></i>Sync Now
|
|
</button>
|
|
</li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<a class="dropdown-item text-danger" href="{% url 'analytics:data_source_delete' source.pk %}">
|
|
<i class="fas fa-trash me-2"></i>Delete
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4">
|
|
<div class="empty-state">
|
|
<i class="fas fa-database fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Data Sources Found</h5>
|
|
<p class="text-muted">Create your first data source to start building analytics.</p>
|
|
<a href="{% url 'analytics:data_source_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-1"></i>Add Data Source
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if is_paginated %}
|
|
<nav aria-label="Data sources pagination">
|
|
<ul class="pagination justify-content-center">
|
|
{% if page_obj.has_previous %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page=1">« First</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if page_obj.number == num %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ num }}</span>
|
|
</li>
|
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page_obj.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last »</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize filters
|
|
initializeFilters();
|
|
|
|
// Initialize select all checkbox
|
|
initializeSelectAll();
|
|
});
|
|
|
|
function initializeFilters() {
|
|
const sourceTypeFilter = document.getElementById('sourceTypeFilter');
|
|
const statusFilter = document.getElementById('statusFilter');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
|
|
// Add event listeners
|
|
sourceTypeFilter.addEventListener('change', applyFilters);
|
|
statusFilter.addEventListener('change', applyFilters);
|
|
searchInput.addEventListener('keyup', function(e) {
|
|
if (e.key === 'Enter') {
|
|
applyFilters();
|
|
}
|
|
});
|
|
searchBtn.addEventListener('click', applyFilters);
|
|
}
|
|
|
|
function initializeSelectAll() {
|
|
const selectAllCheckbox = document.getElementById('selectAll');
|
|
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
|
|
|
|
selectAllCheckbox.addEventListener('change', function() {
|
|
rowCheckboxes.forEach(checkbox => {
|
|
checkbox.checked = this.checked;
|
|
});
|
|
});
|
|
|
|
rowCheckboxes.forEach(checkbox => {
|
|
checkbox.addEventListener('change', function() {
|
|
const allChecked = Array.from(rowCheckboxes).every(cb => cb.checked);
|
|
const someChecked = Array.from(rowCheckboxes).some(cb => cb.checked);
|
|
|
|
selectAllCheckbox.checked = allChecked;
|
|
selectAllCheckbox.indeterminate = someChecked && !allChecked;
|
|
});
|
|
});
|
|
}
|
|
|
|
function applyFilters() {
|
|
const sourceType = document.getElementById('sourceTypeFilter').value;
|
|
const status = document.getElementById('statusFilter').value;
|
|
const search = document.getElementById('searchInput').value;
|
|
|
|
// Build query parameters
|
|
const params = new URLSearchParams();
|
|
if (sourceType) params.append('source_type', sourceType);
|
|
if (status) params.append('status', status);
|
|
if (search) params.append('search', search);
|
|
|
|
// Redirect with filters
|
|
window.location.href = '?' + params.toString();
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById('sourceTypeFilter').value = '';
|
|
document.getElementById('statusFilter').value = '';
|
|
document.getElementById('searchInput').value = '';
|
|
window.location.href = window.location.pathname;
|
|
}
|
|
|
|
function refreshSources() {
|
|
window.location.reload();
|
|
}
|
|
|
|
function exportSources() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.row-checkbox:checked'))
|
|
.map(cb => cb.value);
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('Please select data sources to export');
|
|
return;
|
|
}
|
|
|
|
// Create export URL with selected IDs
|
|
const params = new URLSearchParams();
|
|
selectedIds.forEach(id => params.append('ids', id));
|
|
|
|
window.open(`{% url 'analytics:data_source_export' %}?${params.toString()}`);
|
|
}
|
|
|
|
function testConnection(sourceId) {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Testing...';
|
|
btn.disabled = true;
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
alert('Connection test completed successfully!');
|
|
}, 2000);
|
|
}
|
|
|
|
function syncSource(sourceId) {
|
|
if (confirm('Start synchronization for this data source?')) {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Syncing...';
|
|
btn.disabled = true;
|
|
|
|
// Simulate sync process
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
alert('Synchronization started successfully!');
|
|
window.location.reload();
|
|
}, 2000);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dash-widget-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.dash-widget-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
margin-right: 15px;
|
|
border: 2px solid;
|
|
}
|
|
|
|
.dash-count h3 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.dash-widget-info h6 {
|
|
font-size: 0.875rem;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.source-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 8px;
|
|
background: #f8f9fa;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.progress-sm {
|
|
height: 4px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-btn {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.card-tools {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.dash-widget-header {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
}
|
|
|
|
.dash-widget-icon {
|
|
margin-right: 0;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|