hospital-management/templates/core/bulk_operations.html
2025-08-12 13:33:25 +03:00

805 lines
34 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Bulk Operations{% endblock %}
{% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item active">Bulk Operations</li>
</ul>
<div class="row align-items-center mb-3">
<div class="col">
<h1 class="page-header">Bulk Operations</h1>
<p class="text-muted">Perform batch operations across multiple records</p>
</div>
<div class="col-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-primary" onclick="showOperationHistory()">
<i class="fa fa-history me-2"></i>Operation History
</button>
<button type="button" class="btn btn-outline-info" onclick="downloadTemplate()">
<i class="fa fa-download me-2"></i>Download Template
</button>
</div>
</div>
</div>
<!-- Operation Selection -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Select Operation Type</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('patient_update')">
<div class="card h-100 text-center operation-option" data-operation="patient_update">
<div class="card-body">
<div class="w-60px h-60px bg-primary bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-users fa-2x text-primary"></i>
</div>
<h5>Patient Updates</h5>
<p class="text-muted">Update patient information in bulk</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('appointment_reschedule')">
<div class="card h-100 text-center operation-option" data-operation="appointment_reschedule">
<div class="card-body">
<div class="w-60px h-60px bg-success bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-calendar fa-2x text-success"></i>
</div>
<h5>Appointment Management</h5>
<p class="text-muted">Reschedule or cancel multiple appointments</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('billing_update')">
<div class="card h-100 text-center operation-option" data-operation="billing_update">
<div class="card-body">
<div class="w-60px h-60px bg-warning bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-dollar-sign fa-2x text-warning"></i>
</div>
<h5>Billing Operations</h5>
<p class="text-muted">Update billing information and status</p>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('inventory_update')">
<div class="card h-100 text-center operation-option" data-operation="inventory_update">
<div class="card-body">
<div class="w-60px h-60px bg-info bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-boxes fa-2x text-info"></i>
</div>
<h5>Inventory Management</h5>
<p class="text-muted">Update inventory levels and information</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('user_management')">
<div class="card h-100 text-center operation-option" data-operation="user_management">
<div class="card-body">
<div class="w-60px h-60px bg-danger bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-user-cog fa-2x text-danger"></i>
</div>
<h5>User Management</h5>
<p class="text-muted">Manage user accounts and permissions</p>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="operation-card" onclick="selectOperation('data_export')">
<div class="card h-100 text-center operation-option" data-operation="data_export">
<div class="card-body">
<div class="w-60px h-60px bg-secondary bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
<i class="fa fa-download fa-2x text-secondary"></i>
</div>
<h5>Data Export</h5>
<p class="text-muted">Export data in various formats</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Operation Configuration -->
<div id="operationConfig" class="card mb-4" style="display: none;">
<div class="card-header">
<h4 class="card-title">Configure Operation</h4>
<div class="card-toolbar">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetOperation()">
<i class="fa fa-times"></i> Reset
</button>
</div>
</div>
<div class="card-body">
<form id="bulkOperationForm">
{% csrf_token %}
<input type="hidden" name="operation_type" id="operationType">
<!-- Data Source Selection -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Data Source</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="data_source" value="selection" checked>
<label class="form-check-label">Select from existing records</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="data_source" value="upload">
<label class="form-check-label">Upload CSV file</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="data_source" value="filter">
<label class="form-check-label">Use filters to select records</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Operation Mode</label>
<select name="operation_mode" class="form-select">
<option value="update">Update existing records</option>
<option value="create">Create new records</option>
<option value="delete">Delete records</option>
<option value="export">Export data</option>
</select>
</div>
</div>
<!-- File Upload Section -->
<div id="uploadSection" class="mb-4" style="display: none;">
<div class="row">
<div class="col-md-8">
<label class="form-label">Upload CSV File</label>
<input type="file" name="csv_file" class="form-control" accept=".csv">
<div class="form-text">Upload a CSV file with the data to process. First row should contain column headers.</div>
</div>
<div class="col-md-4">
<label class="form-label">Options</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="skip_header" checked>
<label class="form-check-label">Skip header row</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="validate_only">
<label class="form-check-label">Validate only (don't save)</label>
</div>
</div>
</div>
</div>
<!-- Filter Section -->
<div id="filterSection" class="mb-4" style="display: none;">
<div class="row">
<div class="col-md-12">
<label class="form-label">Filter Criteria</label>
<div id="filterCriteria">
<!-- Dynamic filter fields will be added here -->
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addFilterCriteria()">
<i class="fa fa-plus me-2"></i>Add Filter
</button>
</div>
</div>
</div>
<!-- Record Selection Section -->
<div id="selectionSection" class="mb-4">
<div class="row">
<div class="col-md-12">
<label class="form-label">Select Records</label>
<div class="table-responsive">
<table id="recordsTable" class="table table-striped">
<thead>
<tr>
<th width="50">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll">
</div>
</th>
<th>ID</th>
<th>Name</th>
<th>Status</th>
<th>Last Modified</th>
</tr>
</thead>
<tbody>
<!-- Records will be loaded dynamically -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Operation Parameters -->
<div id="operationParameters" class="mb-4">
<!-- Dynamic operation-specific parameters will be added here -->
</div>
<!-- Execution Options -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Execution Options</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="dry_run">
<label class="form-check-label">Dry run (preview changes only)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="send_notification" checked>
<label class="form-check-label">Send completion notification</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="create_backup">
<label class="form-check-label">Create backup before operation</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Batch Size</label>
<select name="batch_size" class="form-select">
<option value="10">10 records per batch</option>
<option value="50" selected>50 records per batch</option>
<option value="100">100 records per batch</option>
<option value="500">500 records per batch</option>
</select>
<div class="form-text">Smaller batches are safer but slower</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between">
<div>
<button type="button" class="btn btn-outline-info" onclick="previewOperation()">
<i class="fa fa-eye me-2"></i>Preview Changes
</button>
<button type="button" class="btn btn-outline-warning" onclick="validateOperation()">
<i class="fa fa-check me-2"></i>Validate
</button>
</div>
<div>
<button type="button" class="btn btn-secondary" onclick="resetOperation()">
<i class="fa fa-times me-2"></i>Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="fa fa-play me-2"></i>Execute Operation
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Operation Progress -->
<div id="operationProgress" class="card mb-4" style="display: none;">
<div class="card-header">
<h4 class="card-title">Operation Progress</h4>
</div>
<div class="card-body">
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<span>Processing...</span>
<span id="progressText">0%</span>
</div>
<div class="progress">
<div id="progressBar" class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="text-center">
<div class="fs-24px fw-600 text-primary" id="processedCount">0</div>
<div class="text-muted small">Processed</div>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="fs-24px fw-600 text-success" id="successCount">0</div>
<div class="text-muted small">Successful</div>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="fs-24px fw-600 text-danger" id="errorCount">0</div>
<div class="text-muted small">Errors</div>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<div class="fs-24px fw-600 text-info" id="remainingCount">0</div>
<div class="text-muted small">Remaining</div>
</div>
</div>
</div>
<div class="mt-3">
<div class="d-flex justify-content-between">
<div>
<button type="button" class="btn btn-outline-warning" onclick="pauseOperation()">
<i class="fa fa-pause me-2"></i>Pause
</button>
<button type="button" class="btn btn-outline-danger" onclick="cancelOperation()">
<i class="fa fa-stop me-2"></i>Cancel
</button>
</div>
<div>
<button type="button" class="btn btn-outline-info" onclick="viewOperationLog()">
<i class="fa fa-list me-2"></i>View Log
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Operation Results -->
<div id="operationResults" class="card" style="display: none;">
<div class="card-header">
<h4 class="card-title">Operation Results</h4>
<div class="card-toolbar">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="downloadResults()">
<i class="fa fa-download me-2"></i>Download Report
</button>
</div>
</div>
<div class="card-body">
<div id="resultsContent">
<!-- Results will be loaded here -->
</div>
</div>
</div>
</div>
</div>
<!-- Operation History Modal -->
<div class="modal fade" id="operationHistoryModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Operation History</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Operation</th>
<th>Records</th>
<th>Status</th>
<th>User</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="historyTableBody">
<!-- History will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Preview Changes</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="previewContent">
<!-- Preview will be loaded here -->
</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-primary" onclick="confirmOperation()">Proceed with Operation</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
var currentOperation = null;
var operationId = null;
$(document).ready(function() {
// Initialize data source radio buttons
$('input[name="data_source"]').change(function() {
toggleDataSourceSections();
});
// Initialize select all checkbox
$('#selectAll').change(function() {
$('.record-checkbox').prop('checked', this.checked);
});
// Form submission
$('#bulkOperationForm').submit(function(e) {
e.preventDefault();
executeOperation();
});
});
function selectOperation(operation) {
currentOperation = operation;
$('#operationType').val(operation);
// Highlight selected operation
$('.operation-option').removeClass('border-primary');
$('[data-operation="' + operation + '"]').addClass('border-primary');
// Show configuration section
$('#operationConfig').show();
// Load operation-specific configuration
loadOperationConfig(operation);
// Load records for selection
loadRecords(operation);
}
function loadOperationConfig(operation) {
$.get('{% url "core:get_operation_config" %}', {operation: operation}, function(data) {
$('#operationParameters').html(data.parameters_html);
// Initialize any special form elements
$('.select2').select2();
});
}
function loadRecords(operation) {
$.get('{% url "core:get_operation_records" %}', {operation: operation}, function(data) {
var tbody = $('#recordsTable tbody');
tbody.empty();
data.records.forEach(function(record) {
var row = '<tr>' +
'<td><div class="form-check"><input class="form-check-input record-checkbox" type="checkbox" value="' + record.id + '"></div></td>' +
'<td>' + record.id + '</td>' +
'<td>' + record.name + '</td>' +
'<td><span class="badge bg-' + record.status_color + '">' + record.status + '</span></td>' +
'<td>' + record.last_modified + '</td>' +
'</tr>';
tbody.append(row);
});
// Initialize DataTable
if ($.fn.DataTable.isDataTable('#recordsTable')) {
$('#recordsTable').DataTable().destroy();
}
$('#recordsTable').DataTable({
pageLength: 25,
order: [[1, 'desc']],
columnDefs: [
{orderable: false, targets: 0}
]
});
});
}
function toggleDataSourceSections() {
var dataSource = $('input[name="data_source"]:checked').val();
$('#uploadSection, #filterSection, #selectionSection').hide();
switch(dataSource) {
case 'upload':
$('#uploadSection').show();
break;
case 'filter':
$('#filterSection').show();
break;
case 'selection':
$('#selectionSection').show();
break;
}
}
function addFilterCriteria() {
var filterHtml = '<div class="filter-row row mb-2">' +
'<div class="col-md-3">' +
'<select class="form-select filter-field">' +
'<option value="">Select field</option>' +
'<option value="name">Name</option>' +
'<option value="status">Status</option>' +
'<option value="date_created">Date Created</option>' +
'<option value="last_modified">Last Modified</option>' +
'</select>' +
'</div>' +
'<div class="col-md-2">' +
'<select class="form-select filter-operator">' +
'<option value="equals">Equals</option>' +
'<option value="contains">Contains</option>' +
'<option value="starts_with">Starts with</option>' +
'<option value="greater_than">Greater than</option>' +
'<option value="less_than">Less than</option>' +
'</select>' +
'</div>' +
'<div class="col-md-5">' +
'<input type="text" class="form-control filter-value" placeholder="Value">' +
'</div>' +
'<div class="col-md-2">' +
'<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeFilterCriteria(this)">' +
'<i class="fa fa-trash"></i>' +
'</button>' +
'</div>' +
'</div>';
$('#filterCriteria').append(filterHtml);
}
function removeFilterCriteria(button) {
$(button).closest('.filter-row').remove();
}
function previewOperation() {
var formData = new FormData($('#bulkOperationForm')[0]);
formData.append('preview', 'true');
$.post('{% url "core:preview_bulk_operation" %}', formData, function(data) {
$('#previewContent').html(data.preview_html);
$('#previewModal').modal('show');
}).fail(function() {
toastr.error('Failed to generate preview');
});
}
function validateOperation() {
var formData = new FormData($('#bulkOperationForm')[0]);
formData.append('validate', 'true');
$.post('{% url "core:validate_bulk_operation" %}', formData, function(data) {
if (data.valid) {
toastr.success('Operation validation passed');
} else {
toastr.error('Validation failed: ' + data.errors.join(', '));
}
}).fail(function() {
toastr.error('Failed to validate operation');
});
}
function executeOperation() {
var formData = new FormData($('#bulkOperationForm')[0]);
// Get selected records
var selectedRecords = [];
$('.record-checkbox:checked').each(function() {
selectedRecords.push($(this).val());
});
if (selectedRecords.length === 0 && $('input[name="data_source"]:checked').val() === 'selection') {
toastr.warning('Please select records to process');
return;
}
formData.append('selected_records', JSON.stringify(selectedRecords));
// Show progress section
$('#operationConfig').hide();
$('#operationProgress').show();
// Start operation
$.post('{% url "core:execute_bulk_operation" %}', formData, function(data) {
if (data.success) {
operationId = data.operation_id;
monitorOperation(operationId);
} else {
toastr.error('Failed to start operation: ' + data.error);
$('#operationProgress').hide();
$('#operationConfig').show();
}
}).fail(function() {
toastr.error('Failed to start operation');
$('#operationProgress').hide();
$('#operationConfig').show();
});
}
function monitorOperation(operationId) {
var interval = setInterval(function() {
$.get('{% url "core:get_operation_status" %}', {operation_id: operationId}, function(data) {
updateProgress(data);
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
clearInterval(interval);
showOperationResults(data);
}
});
}, 2000);
}
function updateProgress(data) {
var percentage = Math.round((data.processed / data.total) * 100);
$('#progressBar').css('width', percentage + '%');
$('#progressText').text(percentage + '%');
$('#processedCount').text(data.processed);
$('#successCount').text(data.successful);
$('#errorCount').text(data.errors);
$('#remainingCount').text(data.total - data.processed);
}
function showOperationResults(data) {
$('#operationProgress').hide();
$('#operationResults').show();
var resultsHtml = '<div class="alert alert-' + (data.status === 'completed' ? 'success' : 'danger') + '">' +
'<h5>Operation ' + data.status.charAt(0).toUpperCase() + data.status.slice(1) + '</h5>' +
'<p>Processed ' + data.processed + ' of ' + data.total + ' records</p>' +
'<p>Successful: ' + data.successful + ', Errors: ' + data.errors + '</p>' +
'</div>';
if (data.errors > 0) {
resultsHtml += '<div class="mt-3"><h6>Error Details:</h6><ul>';
data.error_details.forEach(function(error) {
resultsHtml += '<li>' + error + '</li>';
});
resultsHtml += '</ul></div>';
}
$('#resultsContent').html(resultsHtml);
}
function pauseOperation() {
$.post('{% url "core:pause_operation" %}', {operation_id: operationId}, function(data) {
if (data.success) {
toastr.success('Operation paused');
} else {
toastr.error('Failed to pause operation');
}
});
}
function cancelOperation() {
if (confirm('Are you sure you want to cancel this operation?')) {
$.post('{% url "core:cancel_operation" %}', {operation_id: operationId}, function(data) {
if (data.success) {
toastr.success('Operation cancelled');
} else {
toastr.error('Failed to cancel operation');
}
});
}
}
function resetOperation() {
currentOperation = null;
operationId = null;
$('#operationConfig, #operationProgress, #operationResults').hide();
$('.operation-option').removeClass('border-primary');
$('#bulkOperationForm')[0].reset();
}
function showOperationHistory() {
$.get('{% url "core:get_operation_history" %}', function(data) {
var tbody = $('#historyTableBody');
tbody.empty();
data.operations.forEach(function(op) {
var row = '<tr>' +
'<td>' + op.date + '</td>' +
'<td>' + op.operation_type + '</td>' +
'<td>' + op.record_count + '</td>' +
'<td><span class="badge bg-' + op.status_color + '">' + op.status + '</span></td>' +
'<td>' + op.user + '</td>' +
'<td>' +
'<button class="btn btn-outline-info btn-sm" onclick="viewOperationDetails(\'' + op.id + '\')">' +
'<i class="fa fa-eye"></i>' +
'</button>' +
'</td>' +
'</tr>';
tbody.append(row);
});
$('#operationHistoryModal').modal('show');
});
}
function downloadTemplate() {
if (!currentOperation) {
toastr.warning('Please select an operation type first');
return;
}
var url = '{% url "core:download_operation_template" %}?operation=' + currentOperation;
window.location.href = url;
}
function downloadResults() {
if (!operationId) {
toastr.warning('No operation results to download');
return;
}
var url = '{% url "core:download_operation_results" %}?operation_id=' + operationId;
window.location.href = url;
}
function confirmOperation() {
$('#previewModal').modal('hide');
executeOperation();
}
</script>
<style>
.operation-card {
cursor: pointer;
transition: all 0.3s ease;
}
.operation-card:hover .card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.operation-option.border-primary {
border: 2px solid #007bff !important;
}
.filter-row {
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
background-color: #f8f9fa;
}
.progress {
height: 20px;
}
#operationProgress .card-body {
min-height: 200px;
}
.record-checkbox {
cursor: pointer;
}
#recordsTable tbody tr:hover {
background-color: #f8f9fa;
}
</style>
{% endblock %}