805 lines
34 KiB
HTML
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 %}
|
|
|