516 lines
19 KiB
HTML
516 lines
19 KiB
HTML
{% extends 'base/base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Bulk Submit Bills - {{ block.super }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.bill-validation-error {
|
|
color: #dc3545;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.bill-validation-success {
|
|
color: #28a745;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.bill-row {
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.bill-row:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.bill-row.selected {
|
|
background-color: #e3f2fd;
|
|
}
|
|
|
|
.validation-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.validation-valid {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.validation-invalid {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.bulk-actions {
|
|
position: sticky;
|
|
top: 0;
|
|
background: white;
|
|
z-index: 100;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 0;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.progress-container {
|
|
display: none;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.results-container {
|
|
display: none;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.result-item {
|
|
padding: 0.5rem;
|
|
margin: 0.25rem 0;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.result-success {
|
|
background-color: #d4edda;
|
|
border: 1px solid #c3e6cb;
|
|
color: #155724;
|
|
}
|
|
|
|
.result-error {
|
|
background-color: #f8d7da;
|
|
border: 1px solid #f5c6cb;
|
|
color: #721c24;
|
|
}
|
|
|
|
.result-warning {
|
|
background-color: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
color: #856404;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<div class="page-title">
|
|
<h4>Bulk Submit Bills</h4>
|
|
<h6>Submit multiple draft bills for processing</h6>
|
|
</div>
|
|
<div class="page-btn">
|
|
<a href="{% url 'billing:bill_list' %}" class="btn btn-secondary">
|
|
<i class="fas fa-arrow-left"></i> Back to Bills
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ total_draft_bills }}</h4>
|
|
<p class="mb-0">Total Draft Bills</p>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-file-invoice fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ submittable_bills }}</h4>
|
|
<p class="mb-0">Ready to Submit</p>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-check-circle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0">{{ total_draft_bills|add:"-"|add:submittable_bills }}</h4>
|
|
<p class="mb-0">Need Attention</p>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-exclamation-triangle fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-info text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<h4 class="mb-0" id="selected-count">0</h4>
|
|
<p class="mb-0">Selected Bills</p>
|
|
</div>
|
|
<div class="align-self-center">
|
|
<i class="fas fa-tasks fa-2x"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Actions -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="bulk-actions">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input" id="select-all">
|
|
<label class="form-check-label" for="select-all">
|
|
Select All Valid Bills
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<button type="button" class="btn btn-primary" id="bulk-submit-btn" disabled>
|
|
<i class="fas fa-paper-plane"></i> Submit Selected Bills
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="select-valid-btn">
|
|
<i class="fas fa-check"></i> Select All Valid
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="clear-selection-btn">
|
|
<i class="fas fa-times"></i> Clear Selection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="progress-container">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-spinner fa-spin"></i> Submitting bills, please wait...
|
|
</div>
|
|
<div class="progress">
|
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Container -->
|
|
<div class="results-container">
|
|
<h5>Submission Results</h5>
|
|
<div id="results-content"></div>
|
|
</div>
|
|
|
|
<!-- Bills Table -->
|
|
{% if bills_with_status %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th width="50">
|
|
<input type="checkbox" id="header-checkbox" class="form-check-input">
|
|
</th>
|
|
<th>Bill Number</th>
|
|
<th>Patient</th>
|
|
<th>Bill Date</th>
|
|
<th>Total Amount</th>
|
|
<th>Status</th>
|
|
<th>Validation</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in bills_with_status %}
|
|
<tr class="bill-row" data-bill-id="{{ item.bill.bill_id }}"
|
|
data-can-submit="{{ item.can_submit|yesno:'true,false' }}">
|
|
<td>
|
|
{% if item.can_submit %}
|
|
<input type="checkbox" class="form-check-input bill-checkbox"
|
|
value="{{ item.bill.bill_id }}">
|
|
{% else %}
|
|
<input type="checkbox" class="form-check-input" disabled>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<strong>{{ item.bill.bill_number }}</strong>
|
|
</td>
|
|
<td>
|
|
{% if item.bill.patient %}
|
|
{{ item.bill.patient.get_full_name }}
|
|
{% else %}
|
|
<span class="text-muted">No patient assigned</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if item.bill.bill_date %}
|
|
{{ item.bill.bill_date|date:"M d, Y" }}
|
|
{% else %}
|
|
<span class="text-muted">No date</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if item.bill.total_amount %}
|
|
${{ item.bill.total_amount|floatformat:2 }}
|
|
{% else %}
|
|
<span class="text-muted">$0.00</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-warning">{{ item.bill.get_status_display }}</span>
|
|
</td>
|
|
<td>
|
|
{% if item.can_submit %}
|
|
<span class="validation-badge validation-valid">
|
|
<i class="fas fa-check"></i> Valid
|
|
</span>
|
|
{% else %}
|
|
<span class="validation-badge validation-invalid">
|
|
<i class="fas fa-exclamation-triangle"></i> Issues
|
|
</span>
|
|
<div class="bill-validation-error mt-1">
|
|
{% for error in item.validation_errors %}
|
|
<small>• {{ error }}</small><br>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'billing:bill_detail' item.bill.bill_id %}"
|
|
class="btn btn-outline-primary" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="{% url 'billing:bill_update' item.bill.bill_id %}"
|
|
class="btn btn-outline-secondary" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Draft Bills Found</h5>
|
|
<p class="text-muted">There are no draft bills available for submission.</p>
|
|
<a href="{% url 'billing:bill_create' %}" class="btn btn-primary">
|
|
<i class="fas fa-plus"></i> Create New Bill
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
let selectedBills = [];
|
|
|
|
// Update selected count
|
|
function updateSelectedCount() {
|
|
const count = selectedBills.length;
|
|
$('#selected-count').text(count);
|
|
$('#bulk-submit-btn').prop('disabled', count === 0);
|
|
}
|
|
|
|
// Handle individual checkbox changes
|
|
$('.bill-checkbox').change(function() {
|
|
const billId = $(this).val();
|
|
const isChecked = $(this).is(':checked');
|
|
|
|
if (isChecked) {
|
|
if (!selectedBills.includes(billId)) {
|
|
selectedBills.push(billId);
|
|
}
|
|
$(this).closest('tr').addClass('selected');
|
|
} else {
|
|
selectedBills = selectedBills.filter(id => id !== billId);
|
|
$(this).closest('tr').removeClass('selected');
|
|
}
|
|
|
|
updateSelectedCount();
|
|
|
|
// Update header checkbox
|
|
const totalCheckboxes = $('.bill-checkbox').length;
|
|
const checkedCheckboxes = $('.bill-checkbox:checked').length;
|
|
$('#header-checkbox').prop('indeterminate', checkedCheckboxes > 0 && checkedCheckboxes < totalCheckboxes);
|
|
$('#header-checkbox').prop('checked', checkedCheckboxes === totalCheckboxes);
|
|
});
|
|
|
|
// Handle header checkbox
|
|
$('#header-checkbox').change(function() {
|
|
const isChecked = $(this).is(':checked');
|
|
$('.bill-checkbox').prop('checked', isChecked).trigger('change');
|
|
});
|
|
|
|
// Select all valid bills
|
|
$('#select-valid-btn').click(function() {
|
|
$('.bill-checkbox').prop('checked', true).trigger('change');
|
|
});
|
|
|
|
// Clear selection
|
|
$('#clear-selection-btn').click(function() {
|
|
$('.bill-checkbox').prop('checked', false).trigger('change');
|
|
});
|
|
|
|
// Bulk submit
|
|
$('#bulk-submit-btn').click(function() {
|
|
if (selectedBills.length === 0) {
|
|
alert('Please select at least one bill to submit.');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to submit ${selectedBills.length} bill(s)?`)) {
|
|
return;
|
|
}
|
|
|
|
// Show progress
|
|
$('.progress-container').show();
|
|
$('.results-container').hide();
|
|
$(this).prop('disabled', true);
|
|
|
|
// Prepare form data
|
|
const formData = new FormData();
|
|
selectedBills.forEach(billId => {
|
|
formData.append('bill_ids', billId);
|
|
});
|
|
|
|
// Add CSRF token
|
|
formData.append('csrfmiddlewaretoken', $('[name=csrfmiddlewaretoken]').val());
|
|
|
|
// Submit request
|
|
$.ajax({
|
|
url: '{% url "billing:bulk_submit_bills" %}',
|
|
type: 'POST',
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
success: function(response) {
|
|
$('.progress-container').hide();
|
|
displayResults(response);
|
|
|
|
// Refresh page after successful submission
|
|
if (response.success && response.summary.total_submitted > 0) {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 3000);
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
$('.progress-container').hide();
|
|
alert('An error occurred while submitting bills: ' + error);
|
|
},
|
|
complete: function() {
|
|
$('#bulk-submit-btn').prop('disabled', false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Display results
|
|
function displayResults(response) {
|
|
const resultsContent = $('#results-content');
|
|
resultsContent.empty();
|
|
|
|
// Summary
|
|
const summary = response.summary;
|
|
resultsContent.append(`
|
|
<div class="alert alert-info">
|
|
<h6>Summary</h6>
|
|
<ul class="mb-0">
|
|
<li>Total Processed: ${summary.total_processed}</li>
|
|
<li>Successfully Submitted: ${summary.total_submitted}</li>
|
|
<li>Failed: ${summary.total_failed}</li>
|
|
<li>Already Submitted: ${summary.total_already_submitted}</li>
|
|
</ul>
|
|
</div>
|
|
`);
|
|
|
|
// Successful submissions
|
|
if (response.results.submitted.length > 0) {
|
|
resultsContent.append('<h6>Successfully Submitted:</h6>');
|
|
response.results.submitted.forEach(bill => {
|
|
resultsContent.append(`
|
|
<div class="result-item result-success">
|
|
<strong>${bill.bill_number}</strong> - ${bill.patient_name} ($${bill.total_amount})
|
|
</div>
|
|
`);
|
|
});
|
|
}
|
|
|
|
// Failed submissions
|
|
if (response.results.failed.length > 0) {
|
|
resultsContent.append('<h6>Failed Submissions:</h6>');
|
|
response.results.failed.forEach(bill => {
|
|
resultsContent.append(`
|
|
<div class="result-item result-error">
|
|
<strong>${bill.bill_number}</strong> - ${bill.errors.join(', ')}
|
|
</div>
|
|
`);
|
|
});
|
|
}
|
|
|
|
// Already submitted
|
|
if (response.results.already_submitted.length > 0) {
|
|
resultsContent.append('<h6>Already Submitted:</h6>');
|
|
response.results.already_submitted.forEach(bill => {
|
|
resultsContent.append(`
|
|
<div class="result-item result-warning">
|
|
<strong>${bill.bill_number}</strong> - Status: ${bill.status}
|
|
</div>
|
|
`);
|
|
});
|
|
}
|
|
|
|
$('.results-container').show();
|
|
}
|
|
|
|
// Get CSRF token
|
|
function getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
|
|
// Set up CSRF token for AJAX
|
|
$.ajaxSetup({
|
|
beforeSend: function(xhr, settings) {
|
|
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
|
|
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|