update
This commit is contained in:
parent
0c980b4706
commit
fd2f7259c0
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -277,7 +277,7 @@ class ReportForm(forms.ModelForm):
|
||||
self.fields['data_source'].queryset = DataSource.objects.filter(
|
||||
tenant=user.tenant,
|
||||
is_active=True
|
||||
).order_by('source_name')
|
||||
).order_by('name')
|
||||
|
||||
def clean_report_name(self):
|
||||
name = self.cleaned_data['report_name']
|
||||
|
||||
@ -46,8 +46,8 @@ urlpatterns = [
|
||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path('reports/create/', views.ReportCreateView.as_view(), name='report_create'),
|
||||
path('reports/<uuid:pk>/', views.ReportDetailView.as_view(), name='report_detail'),
|
||||
path('reports/<int:pk>/update/', views.ReportUpdateView.as_view(), name='report_update'),
|
||||
path('reports/<int:pk>/delete/', views.ReportDeleteView.as_view(), name='report_delete'),
|
||||
path('reports/<uuid:pk>/update/', views.ReportUpdateView.as_view(), name='report_update'),
|
||||
path('reports/<uuid:pk>/delete/', views.ReportDeleteView.as_view(), name='report_delete'),
|
||||
path('ajax/report-list/', views.report_list, name='report_list_data'),
|
||||
|
||||
# ============================================================================
|
||||
@ -82,7 +82,7 @@ urlpatterns = [
|
||||
# ACTION URLS FOR WORKFLOW OPERATIONS
|
||||
# ============================================================================
|
||||
path('data-sources/<int:data_source_id>/test/', views.test_data_source, name='test_data_source'),
|
||||
path('reports/<int:report_id>/execute/', views.execute_report, name='execute_report'),
|
||||
path('reports/<uuid:report_id>/execute/', views.execute_report, name='execute_report'),
|
||||
path('metrics/<int:metric_id>/calculate/', views.calculate_metric, name='calculate_metric'),
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -384,7 +384,7 @@ class DataSourceListView(LoginRequiredMixin, ListView):
|
||||
if test_status:
|
||||
queryset = queryset.filter(last_test_status=test_status)
|
||||
|
||||
return queryset.order_by('source_name')
|
||||
return queryset.order_by('name')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -413,12 +413,12 @@ class DataSourceDetailView(LoginRequiredMixin, DetailView):
|
||||
# Related reports
|
||||
context['related_reports'] = Report.objects.filter(
|
||||
data_source=data_source
|
||||
).order_by('report_name')
|
||||
).order_by('name')
|
||||
|
||||
# Related metrics
|
||||
context['related_metrics'] = MetricDefinition.objects.filter(
|
||||
data_source=data_source
|
||||
).order_by('metric_name')
|
||||
).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
@ -557,7 +557,7 @@ class ReportDetailView(LoginRequiredMixin, DetailView):
|
||||
# Recent executions
|
||||
context['recent_executions'] = ReportExecution.objects.filter(
|
||||
report=report
|
||||
).order_by('-execution_time')[:10]
|
||||
).order_by('-started_at')[:10]
|
||||
|
||||
# Execution statistics
|
||||
context['total_executions'] = ReportExecution.objects.filter(
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
@ -11,7 +11,7 @@ urlpatterns = [
|
||||
# Main views
|
||||
path('', views.EMRDashboardView.as_view(), name='dashboard'),
|
||||
path('encounters/', views.EncounterListView.as_view(), name='encounter_list'),
|
||||
path('encounters/<uuid:pk>/', views.EncounterDetailView.as_view(), name='encounter_detail'),
|
||||
path('encounters/<int:pk>/', views.EncounterDetailView.as_view(), name='encounter_detail'),
|
||||
# path('encounters/<int:pk>/update/', views.EncounterUpdateView.as_view(), name='encounter_update'),
|
||||
# path('encounters/<int:pk>/delete/', views.EncounterDeleteView.as_view(), name='encounter_delete'),
|
||||
path('encounters/create/', views.EncounterCreateView.as_view(), name='encounter_create'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -10,7 +10,7 @@ app_name = 'patients'
|
||||
urlpatterns = [
|
||||
# Main views
|
||||
path('', views.PatientListView.as_view(), name='patient_list'),
|
||||
path('<int:pk>/details/', views.PatientDetailView.as_view(), name='patient_detail'),
|
||||
path('patientprofile/<int:pk>/details/', views.PatientDetailView.as_view(), name='patient_detail'),
|
||||
path('register/', views.PatientCreateView.as_view(), name='patient_registration'),
|
||||
path('update/<int:pk>/', views.PatientUpdateView.as_view(), name='patient_update'),
|
||||
path('delete/<int:pk>/', views.PatientDeleteView.as_view(), name='patient_delete'),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -38,6 +38,7 @@ urlpatterns = [
|
||||
path('prescription/<int:prescription_id>/verify/', views.verify_prescription, name='verify_prescription'),
|
||||
path('prescription/<int:prescription_id>/dispense/', views.dispense_medication, name='dispense_medication'),
|
||||
path('inventory/<int:item_id>/update/', views.update_inventory, name='update_inventory'),
|
||||
path('inventory-adjustment/<int:item_id>/', views.adjust_inventory, name='adjust_inventory'),
|
||||
|
||||
# API endpoints
|
||||
# path('api/', include('pharmacy.api.urls')),
|
||||
|
||||
@ -417,10 +417,10 @@ class InventoryItemListView(LoginRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
queryset = InventoryItem.objects.filter(
|
||||
medication__tenant=self.request.user.tenant
|
||||
).select_related('medication').order_by('medication__name', 'expiry_date')
|
||||
).select_related('medication').order_by('medication__generic_name', 'expiration_date')
|
||||
|
||||
# Apply search filters
|
||||
form = InventorySearchForm(data=self.request.GET, user=self.request.user)
|
||||
form = InventoryItemForm(data=self.request.GET)
|
||||
if form.is_valid():
|
||||
search = form.cleaned_data.get('search')
|
||||
if search:
|
||||
@ -458,9 +458,9 @@ class InventoryItemListView(LoginRequiredMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['search_form'] = InventorySearchForm(
|
||||
context['search_form'] = InventoryItemForm(
|
||||
data=self.request.GET,
|
||||
user=self.request.user
|
||||
# user=self.request.user
|
||||
)
|
||||
return context
|
||||
|
||||
@ -487,7 +487,7 @@ class InventoryItemDetailView(LoginRequiredMixin, DetailView):
|
||||
inventory_item=inventory_item
|
||||
).select_related(
|
||||
'prescription__patient', 'dispensed_by'
|
||||
).order_by('-dispensed_date')
|
||||
).order_by('-date_dispensed')
|
||||
|
||||
# Calculate total dispensed
|
||||
context['total_dispensed'] = DispenseRecord.objects.filter(
|
||||
@ -495,8 +495,8 @@ class InventoryItemDetailView(LoginRequiredMixin, DetailView):
|
||||
).aggregate(total=Sum('quantity_dispensed'))['total'] or 0
|
||||
|
||||
# Days until expiry
|
||||
if inventory_item.expiry_date:
|
||||
days_until_expiry = (inventory_item.expiry_date - timezone.now().date()).days
|
||||
if inventory_item.expiration_date:
|
||||
days_until_expiry = (inventory_item.expiration_date - timezone.now().date()).days
|
||||
context['days_until_expiry'] = days_until_expiry
|
||||
context['is_expiring_soon'] = days_until_expiry <= 30
|
||||
|
||||
@ -2054,55 +2054,55 @@ def update_inventory(request, item_id):
|
||||
# })
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def adjust_inventory(request, pk):
|
||||
# """
|
||||
# Action view to adjust inventory quantity.
|
||||
# """
|
||||
# inventory_item = get_object_or_404(
|
||||
# InventoryItem,
|
||||
# pk=pk,
|
||||
# medication__tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# if request.method == 'POST':
|
||||
# adjustment = int(request.POST.get('adjustment', 0))
|
||||
# reason = request.POST.get('reason', '')
|
||||
#
|
||||
# if adjustment == 0:
|
||||
# messages.error(request, 'Adjustment amount cannot be zero.')
|
||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
#
|
||||
# old_quantity = inventory_item.quantity_on_hand
|
||||
# inventory_item.quantity_on_hand += adjustment
|
||||
#
|
||||
# if inventory_item.quantity_on_hand < 0:
|
||||
# messages.error(request, 'Adjustment would result in negative inventory.')
|
||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
#
|
||||
# inventory_item.save()
|
||||
#
|
||||
# # Create audit log
|
||||
# AuditLogEntry.objects.create(
|
||||
# tenant=request.user.tenant,
|
||||
# user=request.user,
|
||||
# action='UPDATE',
|
||||
# model_name='InventoryItem',
|
||||
# object_id=inventory_item.id,
|
||||
# changes={
|
||||
# 'old_quantity': old_quantity,
|
||||
# 'new_quantity': inventory_item.quantity_on_hand,
|
||||
# 'adjustment': adjustment,
|
||||
# 'reason': reason
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f'Inventory adjusted by {adjustment} units.')
|
||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
#
|
||||
# return render(request, 'pharmacy/adjust_inventory.html', {
|
||||
# 'inventory_item': inventory_item
|
||||
# })
|
||||
@login_required
|
||||
def adjust_inventory(request, pk):
|
||||
"""
|
||||
Action view to adjust inventory quantity.
|
||||
"""
|
||||
inventory_item = get_object_or_404(
|
||||
InventoryItem,
|
||||
pk=pk,
|
||||
medication__tenant=request.user.tenant
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
adjustment = int(request.POST.get('adjustment', 0))
|
||||
reason = request.POST.get('reason', '')
|
||||
|
||||
if adjustment == 0:
|
||||
messages.error(request, 'Adjustment amount cannot be zero.')
|
||||
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
|
||||
old_quantity = inventory_item.quantity_on_hand
|
||||
inventory_item.quantity_on_hand += adjustment
|
||||
|
||||
if inventory_item.quantity_on_hand < 0:
|
||||
messages.error(request, 'Adjustment would result in negative inventory.')
|
||||
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
|
||||
inventory_item.save()
|
||||
|
||||
# Create audit log
|
||||
AuditLogEntry.objects.create(
|
||||
tenant=request.user.tenant,
|
||||
user=request.user,
|
||||
action='UPDATE',
|
||||
model_name='InventoryItem',
|
||||
object_id=inventory_item.id,
|
||||
changes={
|
||||
'old_quantity': old_quantity,
|
||||
'new_quantity': inventory_item.quantity_on_hand,
|
||||
'adjustment': adjustment,
|
||||
'reason': reason
|
||||
}
|
||||
)
|
||||
|
||||
messages.success(request, f'Inventory adjusted by {adjustment} units.')
|
||||
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||
|
||||
return render(request, 'pharmacy/adjust_inventory.html', {
|
||||
'inventory_item': inventory_item
|
||||
})
|
||||
#
|
||||
#
|
||||
# # Export Views
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -44,8 +44,8 @@ urlpatterns = [
|
||||
# IMAGING SERIES URLS (RESTRICTED CRUD - Clinical Data)
|
||||
# ============================================================================
|
||||
# urls.py
|
||||
path('studies/<uuid:study_pk>/series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
||||
path('series/<uuid:pk>/', views.ImagingSeriesDetailView.as_view(), name='imaging_series_detail'),
|
||||
path('studies/<int:pk>/series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
||||
path('series/<int:pk>/', views.ImagingSeriesDetailView.as_view(), name='imaging_series_detail'),
|
||||
path('series/create/', views.ImagingSeriesCreateView.as_view(), name='imaging_series_create'),
|
||||
# path('series/<uuid:pk>/edit/', views.ImagingSeriesUpdateView.as_view(), name='imaging_series_edit'),
|
||||
# path('series/<uuid:pk>/delete/', views.ImagingSeriesDeleteView.as_view(), name='imaging_series_delete'),
|
||||
|
||||
@ -413,7 +413,7 @@ class ImagingOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
"""
|
||||
model = ImagingOrder
|
||||
fields = ['status', 'notes'] # Restricted fields for clinical orders
|
||||
template_name = 'radiology/imaging_order_update_form.html'
|
||||
template_name = 'radiology/orders/imaging_order_form.html'
|
||||
permission_required = 'radiology.change_imagingorder'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -462,9 +462,9 @@ class ImagingStudyListView(LoginRequiredMixin, ListView):
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(study_id__icontains=search) |
|
||||
Q(order__patient__first_name__icontains=search) |
|
||||
Q(order__patient__last_name__icontains=search) |
|
||||
Q(order__patient__mrn__icontains=search)
|
||||
Q(imaging_order__patient__first_name__icontains=search) |
|
||||
Q(imaging_order__patient__last_name__icontains=search) |
|
||||
Q(imaging_order__patient__mrn__icontains=search)
|
||||
)
|
||||
|
||||
# Filter by status
|
||||
@ -475,9 +475,9 @@ class ImagingStudyListView(LoginRequiredMixin, ListView):
|
||||
# Filter by modality
|
||||
modality = self.request.GET.get('modality')
|
||||
if modality:
|
||||
queryset = queryset.filter(order__modality=modality)
|
||||
queryset = queryset.filter(imaging_order__modality=modality)
|
||||
|
||||
return queryset.select_related('order__patient').order_by('-study_datetime')
|
||||
return queryset.select_related('imaging_order__patient').order_by('-study_datetime')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -494,7 +494,7 @@ class ImagingStudyDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
model = ImagingStudy
|
||||
template_name = 'radiology/studies/imaging_study_detail.html'
|
||||
context_object_name = 'imaging_study'
|
||||
context_object_name = 'study'
|
||||
|
||||
def get_queryset(self):
|
||||
return ImagingStudy.objects.filter(tenant=self.request.user.tenant)
|
||||
@ -504,15 +504,15 @@ class ImagingStudyDetailView(LoginRequiredMixin, DetailView):
|
||||
imaging_study = self.object
|
||||
|
||||
# Get series for this study
|
||||
context['series'] = imaging_study.series.all().order_by('series_number')
|
||||
context['series'] = imaging_study.series
|
||||
|
||||
# Get reports for this study
|
||||
context['reports'] = imaging_study.reports.all().order_by('-created_at')
|
||||
context['reports'] = imaging_study.report
|
||||
|
||||
# Get total image count
|
||||
context['total_images'] = DICOMImage.objects.filter(
|
||||
series__study=imaging_study,
|
||||
tenant=self.request.user.tenant
|
||||
series__study__tenant=self.request.user.tenant
|
||||
).count()
|
||||
|
||||
return context
|
||||
@ -596,33 +596,33 @@ class ImagingSeriesListView(LoginRequiredMixin, ListView):
|
||||
context_object_name = 'imaging_series'
|
||||
paginate_by = 25
|
||||
|
||||
# def get_queryset(self):
|
||||
# queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||
#
|
||||
# # Filter by study
|
||||
# study_id = self.request.GET.get('study')
|
||||
# if study_id:
|
||||
# queryset = queryset.filter(study_id=study_id)
|
||||
#
|
||||
# # Search functionality
|
||||
# search = self.request.GET.get('search')
|
||||
# if search:
|
||||
# queryset = queryset.filter(
|
||||
# Q(series_description__icontains=search) |
|
||||
# Q(study__order__patient__first_name__icontains=search) |
|
||||
# Q(study__order__patient__last_name__icontains=search)
|
||||
# )
|
||||
#
|
||||
# return queryset.select_related('study__imaging_order__patient').order_by('-created_at')
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# context.update({
|
||||
# 'studies': ImagingStudy.objects.filter(
|
||||
# tenant=self.request.user.tenant
|
||||
# ).select_related('imaging_order__patient').order_by('-study_datetime')[:50],
|
||||
# })
|
||||
# return context
|
||||
def get_queryset(self):
|
||||
queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||
|
||||
# Filter by study
|
||||
study_id = self.request.GET.get('study')
|
||||
if study_id:
|
||||
queryset = queryset.filter(study_id=study_id)
|
||||
|
||||
# Search functionality
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(series_description__icontains=search) |
|
||||
Q(study__imaging_order__patient__first_name__icontains=search) |
|
||||
Q(study__imaging_order__patient__last_name__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.select_related('study__imaging_order__patient').order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'studies': ImagingStudy.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).select_related('imaging_order__patient').order_by('-study_datetime')[:50],
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
||||
@ -634,7 +634,7 @@ class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
||||
context_object_name = 'imaging_series'
|
||||
|
||||
def get_queryset(self):
|
||||
return ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||
return ImagingSeries.objects.filter(study__tenant=self.request.user.tenant)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@ -428,21 +428,21 @@ 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 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');
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<button class="btn btn-xs btn-success me-2" onclick="generateReport()">
|
||||
<i class="fa fa-play"></i> Generate
|
||||
</button>
|
||||
<a href="{% url 'analytics:report_update' object.pk %}" class="btn btn-xs btn-primary me-2">
|
||||
<a href="{% url 'analytics:report_update' object.report_id %}" class="btn btn-xs btn-primary me-2">
|
||||
<i class="fa fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
@ -334,7 +334,7 @@
|
||||
function generateReport() {
|
||||
if (confirm('Generate this report now?')) {
|
||||
$.ajax({
|
||||
url: '{% url "analytics:report_generate" object.pk %}',
|
||||
url: '{% url "analytics:execute_report" object.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
@ -365,66 +365,66 @@ function scheduleReport() {
|
||||
toastr.info('Schedule management functionality will be implemented');
|
||||
}
|
||||
|
||||
function duplicateReport() {
|
||||
if (confirm('Create a copy of this report?')) {
|
||||
$.ajax({
|
||||
url: '{% url "analytics:report_duplicate" object.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
toastr.success('Report duplicated successfully');
|
||||
window.location.href = response.redirect_url;
|
||||
} else {
|
||||
toastr.error('Failed to duplicate report');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('An error occurred while duplicating the report');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function duplicateReport() {#}
|
||||
{# if (confirm('Create a copy of this report?')) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "analytics:report_duplicate" object.pk %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# toastr.success('Report duplicated successfully');#}
|
||||
{# window.location.href = response.redirect_url;#}
|
||||
{# } else {#}
|
||||
{# toastr.error('Failed to duplicate report');#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('An error occurred while duplicating the report');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function exportReport() {
|
||||
window.location.href = '{% url "analytics:report_export_definition" object.pk %}';
|
||||
}
|
||||
{#function exportReport() {#}
|
||||
{# window.location.href = '{% url "analytics:report_export_definition" object.pk %}';#}
|
||||
{# }#}
|
||||
|
||||
function viewError(executionId) {
|
||||
$.ajax({
|
||||
url: '{% url "analytics:execution_error" 0 %}'.replace('0', executionId),
|
||||
success: function(response) {
|
||||
// Show error details in a modal
|
||||
var modal = $('<div class="modal fade" tabindex="-1">' +
|
||||
'<div class="modal-dialog modal-lg">' +
|
||||
'<div class="modal-content">' +
|
||||
'<div class="modal-header">' +
|
||||
'<h5 class="modal-title">Execution Error</h5>' +
|
||||
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body">' +
|
||||
'<pre>' + response.error_message + '</pre>' +
|
||||
'</div>' +
|
||||
'<div class="modal-footer">' +
|
||||
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
|
||||
$('body').append(modal);
|
||||
modal.modal('show');
|
||||
modal.on('hidden.bs.modal', function() {
|
||||
modal.remove();
|
||||
});
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to load error details');
|
||||
}
|
||||
});
|
||||
}
|
||||
{#function viewError(executionId) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "analytics:execution_error" 0 %}'.replace('0', executionId),#}
|
||||
{# success: function(response) {#}
|
||||
{# // Show error details in a modal#}
|
||||
{# var modal = $('<div class="modal fade" tabindex="-1">' +#}
|
||||
{# '<div class="modal-dialog modal-lg">' +#}
|
||||
{# '<div class="modal-content">' +#}
|
||||
{# '<div class="modal-header">' +#}
|
||||
{# '<h5 class="modal-title">Execution Error</h5>' +#}
|
||||
{# '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +#}
|
||||
{# '</div>' +#}
|
||||
{# '<div class="modal-body">' +#}
|
||||
{# '<pre>' + response.error_message + '</pre>' +#}
|
||||
{# '</div>' +#}
|
||||
{# '<div class="modal-footer">' +#}
|
||||
{# '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +#}
|
||||
{# '</div>' +#}
|
||||
{# '</div>' +#}
|
||||
{# '</div>' +#}
|
||||
{# '</div>');#}
|
||||
{# #}
|
||||
{# $('body').append(modal);#}
|
||||
{# modal.modal('show');#}
|
||||
{# modal.on('hidden.bs.modal', function() {#}
|
||||
{# modal.remove();#}
|
||||
{# });#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to load error details');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
|
||||
// Auto-refresh if report is currently running
|
||||
{% if object.is_running %}
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
{% block title %}{% if object %}Edit Report{% else %}Create Report{% endif %} - Analytics{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/codemirror/theme/material.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/codemirror/theme/material.css' %}" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -350,10 +350,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/codemirror/lib/codemirror.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/codemirror/mode/sql/sql.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/codemirror/mode/javascript/javascript.js' %}"></script>
|
||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/codemirror/lib/codemirror.js' %}"></script>
|
||||
<script src="{% static 'plugins/codemirror/mode/sql/sql.js' %}"></script>
|
||||
<script src="{% static 'plugins/codemirror/mode/javascript/javascript.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2
|
||||
@ -460,7 +460,7 @@ function validateQuery() {
|
||||
toastr.warning('Please enter a SQL query first');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "analytics:validate_query" %}',
|
||||
method: 'POST',
|
||||
@ -487,7 +487,7 @@ function testQuery() {
|
||||
toastr.warning('Please enter a SQL query first');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "analytics:test_query" %}',
|
||||
method: 'POST',
|
||||
@ -519,7 +519,7 @@ function formatQuery() {
|
||||
toastr.warning('Please enter a SQL query first');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Simple SQL formatting (basic implementation)
|
||||
var formatted = query
|
||||
.replace(/\bSELECT\b/gi, '\nSELECT')
|
||||
@ -532,7 +532,7 @@ function formatQuery() {
|
||||
.replace(/\bLEFT JOIN\b/gi, '\nLEFT JOIN')
|
||||
.replace(/\bRIGHT JOIN\b/gi, '\nRIGHT JOIN')
|
||||
.replace(/\bINNER JOIN\b/gi, '\nINNER JOIN');
|
||||
|
||||
|
||||
$('.CodeMirror')[0].CodeMirror.setValue(formatted);
|
||||
toastr.success('Query formatted');
|
||||
}
|
||||
|
||||
@ -336,6 +336,7 @@ $(document).ready(function() {
|
||||
actions += '<button class="btn btn-outline-secondary" onclick="editReport(' + data + ')" title="Edit">';
|
||||
actions += '<i class="fa fa-edit"></i></button>';
|
||||
actions += '</div>';
|
||||
console.log(data)
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
@ -392,9 +393,11 @@ function updateBulkActions() {
|
||||
}
|
||||
|
||||
function viewReport(reportId) {
|
||||
window.location.href = '{% url "analytics:report_detail" 0 %}'.replace('0', reportId);
|
||||
const urlTemplate = "{% url 'analytics:report_detail' 0 %}";
|
||||
window.location.href = urlTemplate.replace('0', reportId);
|
||||
}
|
||||
|
||||
|
||||
function editReport(reportId) {
|
||||
window.location.href = '{% url "analytics:report_update" 0 %}'.replace('0', reportId);
|
||||
}
|
||||
|
||||
@ -18,7 +18,9 @@
|
||||
<!-- ================== BEGIN core-css ================== -->
|
||||
|
||||
<link href="{% static 'css/vendor.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/default/app.min.css' %}" rel="stylesheet" />
|
||||
{# <link href="{% static 'css/default/app.min.css' %}" rel="stylesheet" />#}
|
||||
<link href="{% static 'css/apple/app.min.css' %}" rel="stylesheet" />
|
||||
{# <link href="{% static 'css/transparent/app.min.css' %}" rel="stylesheet" />#}
|
||||
<link href="{% static 'css/custom.css' %}" rel="stylesheet" />
|
||||
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
||||
<!-- HTMX -->
|
||||
|
||||
BIN
templates/pharmacy/.DS_Store
vendored
BIN
templates/pharmacy/.DS_Store
vendored
Binary file not shown.
@ -33,7 +33,7 @@
|
||||
<button type="button" class="btn btn-xs btn-success me-2" data-bs-toggle="modal" data-bs-target="#addStockModal">
|
||||
<i class="fa fa-plus"></i> Add Stock
|
||||
</button>
|
||||
<a href="{% url 'pharmacy:inventory_edit' object.pk %}" class="btn btn-xs btn-warning me-2">
|
||||
<a href="{% url 'pharmacy:inventory_update' object.pk %}" class="btn btn-xs btn-warning me-2">
|
||||
<i class="fa fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
@ -301,7 +301,7 @@
|
||||
<button type="button" class="btn btn-info" onclick="performStockCount()">
|
||||
<i class="fa fa-clipboard-check me-2"></i>Stock Count
|
||||
</button>
|
||||
<a href="{% url 'pharmacy:inventory_edit' object.pk %}" class="btn btn-outline-secondary">
|
||||
<a href="{% url 'pharmacy:inventory_update' object.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fa fa-edit me-2"></i>Edit Details
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="printLabel()">
|
||||
@ -427,44 +427,44 @@ function printLabel() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
function createPurchaseOrder() {
|
||||
// Implementation for creating purchase orders
|
||||
window.location.href = '{% url "pharmacy:create_purchase_order" %}?medication={{ object.medication.pk }}&quantity={{ object.suggested_order_quantity }}';
|
||||
}
|
||||
{#function createPurchaseOrder() {#}
|
||||
{# // Implementation for creating purchase orders#}
|
||||
{# window.location.href = '{% url "pharmacy:create_purchase_order" %}?medication={{ object.medication.pk }}&quantity={{ object.suggested_order_quantity }}';#}
|
||||
{# }#}
|
||||
|
||||
function markExpired() {
|
||||
if (confirm('Are you sure you want to mark this inventory as expired?')) {
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:mark_inventory_expired" object.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
toastr.success('Inventory marked as expired');
|
||||
location.reload();
|
||||
},
|
||||
error: function() {
|
||||
toastr.error('Failed to mark inventory as expired');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function markExpired() {#}
|
||||
{# if (confirm('Are you sure you want to mark this inventory as expired?')) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:mark_inventory_expired" object.pk %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# toastr.success('Inventory marked as expired');#}
|
||||
{# location.reload();#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# toastr.error('Failed to mark inventory as expired');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
// Auto-refresh for real-time updates
|
||||
setInterval(function() {
|
||||
// Refresh stock level display
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:inventory_status" object.pk %}',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
// Update stock display if changed
|
||||
if (response.current_stock !== {{ object.current_stock }}) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 30000); // Refresh every 30 seconds
|
||||
{#setInterval(function() {#}
|
||||
{# // Refresh stock level display#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "pharmacy:inventory_status" object.pk %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# success: function(response) {#}
|
||||
{# // Update stock display if changed#}
|
||||
{# if (response.current_stock !== {{ object.current_stock }}) {#}
|
||||
{# location.reload();#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }, 30000); #}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -137,79 +137,79 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3" id="filterForm">
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<label for="search" class="form-label">Search Items</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="search"
|
||||
name="search"
|
||||
value="{{ request.GET.search }}"
|
||||
placeholder="Medication name, lot number, NDC..."
|
||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
||||
hx-target="#inventory-list-container"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
hx-include="#filterForm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="location" class="form-label">Location</label>
|
||||
<select class="form-select" id="location" name="location"
|
||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
||||
hx-target="#inventory-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Locations</option>
|
||||
{% for location in locations %}
|
||||
<option value="{{ location.id }}" {% if request.GET.location == location.id|stringformat:"s" %}selected{% endif %}>{{ location.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status"
|
||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
||||
hx-target="#inventory-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Status</option>
|
||||
<option value="in_stock" {% if request.GET.status == 'in_stock' %}selected{% endif %}>In Stock</option>
|
||||
<option value="low_stock" {% if request.GET.status == 'low_stock' %}selected{% endif %}>Low Stock</option>
|
||||
<option value="out_of_stock" {% if request.GET.status == 'out_of_stock' %}selected{% endif %}>Out of Stock</option>
|
||||
<option value="expired" {% if request.GET.status == 'expired' %}selected{% endif %}>Expired</option>
|
||||
<option value="expiring_soon" {% if request.GET.status == 'expiring_soon' %}selected{% endif %}>Expiring Soon</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label for="controlled" class="form-label">Controlled</label>
|
||||
<select class="form-select" id="controlled" name="controlled"
|
||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
||||
hx-target="#inventory-list-container"
|
||||
hx-trigger="change"
|
||||
hx-include="#filterForm">
|
||||
<option value="">All Items</option>
|
||||
<option value="yes" {% if request.GET.controlled == 'yes' %}selected{% endif %}>Controlled Only</option>
|
||||
<option value="no" {% if request.GET.controlled == 'no' %}selected{% endif %}>Non-Controlled Only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<label class="form-label"> </label>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-search me-2"></i>Filter
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-2"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{# <form method="get" class="row g-3" id="filterForm">#}
|
||||
{# <div class="col-lg-4 col-md-6">#}
|
||||
{# <label for="search" class="form-label">Search Items</label>#}
|
||||
{# <div class="input-group">#}
|
||||
{# <span class="input-group-text"><i class="fas fa-search"></i></span>#}
|
||||
{# <input type="text"#}
|
||||
{# class="form-control"#}
|
||||
{# id="search"#}
|
||||
{# name="search"#}
|
||||
{# value="{{ request.GET.search }}"#}
|
||||
{# placeholder="Medication name, lot number, NDC..."#}
|
||||
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||
{# hx-target="#inventory-list-container"#}
|
||||
{# hx-trigger="keyup changed delay:500ms"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# #}
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="location" class="form-label">Location</label>#}
|
||||
{# <select class="form-select" id="location" name="location"#}
|
||||
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||
{# hx-target="#inventory-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Locations</option>#}
|
||||
{# {% for location in locations %}#}
|
||||
{# <option value="{{ location.id }}" {% if request.GET.location == location.id|stringformat:"s" %}selected{% endif %}>{{ location.name }}</option>#}
|
||||
{# {% endfor %}#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# #}
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="status" class="form-label">Status</label>#}
|
||||
{# <select class="form-select" id="status" name="status"#}
|
||||
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||
{# hx-target="#inventory-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Status</option>#}
|
||||
{# <option value="in_stock" {% if request.GET.status == 'in_stock' %}selected{% endif %}>In Stock</option>#}
|
||||
{# <option value="low_stock" {% if request.GET.status == 'low_stock' %}selected{% endif %}>Low Stock</option>#}
|
||||
{# <option value="out_of_stock" {% if request.GET.status == 'out_of_stock' %}selected{% endif %}>Out of Stock</option>#}
|
||||
{# <option value="expired" {% if request.GET.status == 'expired' %}selected{% endif %}>Expired</option>#}
|
||||
{# <option value="expiring_soon" {% if request.GET.status == 'expiring_soon' %}selected{% endif %}>Expiring Soon</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# #}
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label for="controlled" class="form-label">Controlled</label>#}
|
||||
{# <select class="form-select" id="controlled" name="controlled"#}
|
||||
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||
{# hx-target="#inventory-list-container"#}
|
||||
{# hx-trigger="change"#}
|
||||
{# hx-include="#filterForm">#}
|
||||
{# <option value="">All Items</option>#}
|
||||
{# <option value="yes" {% if request.GET.controlled == 'yes' %}selected{% endif %}>Controlled Only</option>#}
|
||||
{# <option value="no" {% if request.GET.controlled == 'no' %}selected{% endif %}>Non-Controlled Only</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# #}
|
||||
{# <div class="col-lg-2 col-md-6">#}
|
||||
{# <label class="form-label"> </label>#}
|
||||
{# <div class="d-grid gap-2">#}
|
||||
{# <button type="submit" class="btn btn-primary">#}
|
||||
{# <i class="fas fa-search me-2"></i>Filter#}
|
||||
{# </button>#}
|
||||
{# <button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">#}
|
||||
{# <i class="fas fa-times me-2"></i>Clear#}
|
||||
{# </button>#}
|
||||
{# </div>#}
|
||||
{# </div>#}
|
||||
{# </form>#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
438
templates/pharmacy/partials/add_stock_modal.html
Normal file
438
templates/pharmacy/partials/add_stock_modal.html
Normal file
@ -0,0 +1,438 @@
|
||||
<!-- Add Stock Modal -->
|
||||
<div class="modal fade" id="addStockModal" tabindex="-1" aria-labelledby="addStockModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addStockModalLabel">
|
||||
<i class="fas fa-plus-circle me-2"></i>Add Stock
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="addStockForm" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Item Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h6 class="card-title mb-1" id="add-item-name">Item Name</h6>
|
||||
<p class="card-text small text-muted mb-1">
|
||||
<span class="me-3">
|
||||
<i class="fas fa-barcode me-1"></i>
|
||||
<span id="add-item-code">Item Code</span>
|
||||
</span>
|
||||
<span class="me-3">
|
||||
<i class="fas fa-layer-group me-1"></i>
|
||||
<span id="add-item-category">Category</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted mb-0">
|
||||
<i class="fas fa-map-marker-alt me-1"></i>
|
||||
<span id="add-item-location">Location</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="current-stock">
|
||||
<small class="text-muted">Current Stock</small>
|
||||
<h4 class="mb-0" id="add-current-stock">0</h4>
|
||||
<small class="text-muted" id="add-stock-unit">units</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock Addition Details -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_quantity">
|
||||
Quantity to Add <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="add_quantity"
|
||||
name="add_quantity" min="0.01" step="0.01" required>
|
||||
<span class="input-group-text" id="add-quantity-unit">units</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_unit_cost">
|
||||
Unit Cost
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="add_unit_cost"
|
||||
name="add_unit_cost" min="0" step="0.01" placeholder="0.00">
|
||||
</div>
|
||||
<small class="form-text text-muted">Cost per unit</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_supplier">
|
||||
Supplier
|
||||
</label>
|
||||
<select class="form-select" id="add_supplier" name="add_supplier">
|
||||
<option value="">Select supplier...</option>
|
||||
{% for supplier in suppliers %}
|
||||
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_purchase_order">
|
||||
Purchase Order Number
|
||||
</label>
|
||||
<input type="text" class="form-control" id="add_purchase_order"
|
||||
name="add_purchase_order" placeholder="PO-XXXX">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_batch_number">
|
||||
Batch/Lot Number
|
||||
</label>
|
||||
<input type="text" class="form-control" id="add_batch_number"
|
||||
name="add_batch_number" placeholder="Batch number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_expiry_date">
|
||||
Expiry Date
|
||||
</label>
|
||||
<input type="date" class="form-control" id="add_expiry_date"
|
||||
name="add_expiry_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_received_date">
|
||||
Received Date <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="date" class="form-control" id="add_received_date"
|
||||
name="add_received_date" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_received_by">
|
||||
Received By
|
||||
</label>
|
||||
<select class="form-select" id="add_received_by" name="add_received_by">
|
||||
<option value="">Select staff member...</option>
|
||||
{% for staff in pharmacy_staff %}
|
||||
<option value="{{ staff.id }}" {% if staff.id == request.user.id %}selected{% endif %}>
|
||||
{{ staff.get_full_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="add_notes">
|
||||
Notes
|
||||
</label>
|
||||
<textarea class="form-control" id="add_notes" name="add_notes"
|
||||
rows="3" placeholder="Additional notes about this stock addition..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost Calculation -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-success" id="cost-calculation" style="display: none;">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-calculator me-2"></i>Cost Calculation
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<div class="calc-item">
|
||||
<small class="text-muted">Quantity</small>
|
||||
<h5 class="mb-0" id="calc-quantity">0</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="calc-item">
|
||||
<small class="text-muted">Unit Cost</small>
|
||||
<h5 class="mb-0" id="calc-unit-cost">$0.00</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="calc-item">
|
||||
<small class="text-muted">Total Cost</small>
|
||||
<h5 class="mb-0 text-success" id="calc-total-cost">$0.00</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="calc-item">
|
||||
<small class="text-muted">New Stock</small>
|
||||
<h5 class="mb-0 text-primary" id="calc-new-stock">0</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div id="add-stock-alerts" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span id="alert-message">Alert message</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" id="add-submit-btn" disabled>
|
||||
<i class="fas fa-plus me-1"></i>Add Stock
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize add stock modal
|
||||
$('#addStockModal').on('show.bs.modal', function(event) {
|
||||
const button = $(event.relatedTarget);
|
||||
const itemId = button.data('item-id');
|
||||
const itemName = button.data('item-name');
|
||||
const itemCode = button.data('item-code');
|
||||
const itemCategory = button.data('item-category');
|
||||
const itemLocation = button.data('item-location');
|
||||
const currentStock = button.data('current-stock');
|
||||
const stockUnit = button.data('stock-unit');
|
||||
|
||||
// Update modal content
|
||||
$('#add-item-name').text(itemName);
|
||||
$('#add-item-code').text(itemCode);
|
||||
$('#add-item-category').text(itemCategory);
|
||||
$('#add-item-location').text(itemLocation);
|
||||
$('#add-current-stock').text(currentStock);
|
||||
$('#add-stock-unit').text(stockUnit);
|
||||
$('#add-quantity-unit').text(stockUnit);
|
||||
|
||||
// Store item data
|
||||
$('#addStockForm').data('item-id', itemId);
|
||||
$('#addStockForm').data('current-stock', currentStock);
|
||||
$('#addStockForm').data('stock-unit', stockUnit);
|
||||
|
||||
// Set default received date to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
$('#add_received_date').val(today);
|
||||
|
||||
// Reset form
|
||||
$('#addStockForm')[0].reset();
|
||||
$('#add_received_date').val(today);
|
||||
$('#cost-calculation').hide();
|
||||
$('#add-stock-alerts').hide();
|
||||
$('#add-submit-btn').prop('disabled', true);
|
||||
});
|
||||
|
||||
// Handle input changes for calculation
|
||||
$('#add_quantity, #add_unit_cost').on('input', function() {
|
||||
updateCostCalculation();
|
||||
});
|
||||
|
||||
// Handle form validation
|
||||
$('#add_quantity, #add_received_date').on('input change', function() {
|
||||
validateAddForm();
|
||||
});
|
||||
|
||||
// Handle expiry date validation
|
||||
$('#add_expiry_date').on('change', function() {
|
||||
const expiryDate = new Date($(this).val());
|
||||
const today = new Date();
|
||||
|
||||
if (expiryDate <= today) {
|
||||
$('#alert-message').text('Warning: The expiry date is in the past or today.');
|
||||
$('#add-stock-alerts .alert').removeClass('alert-info').addClass('alert-warning');
|
||||
$('#add-stock-alerts').show();
|
||||
} else {
|
||||
$('#add-stock-alerts').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#addStockForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const itemId = $(this).data('item-id');
|
||||
|
||||
// Add item ID to form data
|
||||
formData.append('item_id', itemId);
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = $('#add-submit-btn');
|
||||
const originalText = submitBtn.html();
|
||||
submitBtn.html('<i class="fas fa-spinner fa-spin me-1"></i>Adding...').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: '',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
showToast('success', 'Stock added successfully');
|
||||
|
||||
// Close modal
|
||||
$('#addStockModal').modal('hide');
|
||||
|
||||
// Refresh page or update stock display
|
||||
if (typeof refreshStockData === 'function') {
|
||||
refreshStockData();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
showToast('error', response.message || 'Error adding stock');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Error adding stock';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
showToast('error', errorMessage);
|
||||
},
|
||||
complete: function() {
|
||||
// Restore button state
|
||||
submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function updateCostCalculation() {
|
||||
const quantity = parseFloat($('#add_quantity').val()) || 0;
|
||||
const unitCost = parseFloat($('#add_unit_cost').val()) || 0;
|
||||
const currentStock = parseFloat($('#addStockForm').data('current-stock')) || 0;
|
||||
|
||||
if (quantity > 0) {
|
||||
const totalCost = quantity * unitCost;
|
||||
const newStock = currentStock + quantity;
|
||||
|
||||
// Update calculation display
|
||||
$('#calc-quantity').text(quantity);
|
||||
$('#calc-unit-cost').text('$' + unitCost.toFixed(2));
|
||||
$('#calc-total-cost').text('$' + totalCost.toFixed(2));
|
||||
$('#calc-new-stock').text(newStock);
|
||||
|
||||
// Show calculation
|
||||
$('#cost-calculation').show();
|
||||
} else {
|
||||
$('#cost-calculation').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function validateAddForm() {
|
||||
const quantity = parseFloat($('#add_quantity').val()) || 0;
|
||||
const receivedDate = $('#add_received_date').val();
|
||||
|
||||
const isValid = quantity > 0 && receivedDate;
|
||||
|
||||
$('#add-submit-btn').prop('disabled', !isValid);
|
||||
|
||||
if (isValid) {
|
||||
$('#add-submit-btn').removeClass('btn-outline-success').addClass('btn-success');
|
||||
} else {
|
||||
$('#add-submit-btn').removeClass('btn-success').addClass('btn-outline-success');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(type, message) {
|
||||
// Simple toast notification
|
||||
const toastClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const toastHtml = `
|
||||
<div class="alert ${toastClass} alert-dismissible fade show position-fixed"
|
||||
style="top: 20px; right: 20px; z-index: 9999;">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
$('body').append(toastHtml);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.calc-item {
|
||||
padding: 10px;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.calc-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.current-stock {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#cost-calculation .card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.calc-item {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.calc-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
418
templates/pharmacy/partials/adjust_stock_modal.html
Normal file
418
templates/pharmacy/partials/adjust_stock_modal.html
Normal file
@ -0,0 +1,418 @@
|
||||
<!-- Adjust Stock Modal -->
|
||||
<div class="modal fade" id="adjustStockModal" tabindex="-1" aria-labelledby="adjustStockModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="adjustStockModalLabel">
|
||||
<i class="fas fa-balance-scale me-2"></i>Adjust Stock
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="adjustStockForm" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Item Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h6 class="card-title mb-1" id="adjust-item-name">Item Name</h6>
|
||||
<p class="card-text small text-muted mb-1">
|
||||
<span class="me-3">
|
||||
<i class="fas fa-barcode me-1"></i>
|
||||
<span id="adjust-item-code">Item Code</span>
|
||||
</span>
|
||||
<span class="me-3">
|
||||
<i class="fas fa-layer-group me-1"></i>
|
||||
<span id="adjust-item-category">Category</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted mb-0">
|
||||
<i class="fas fa-map-marker-alt me-1"></i>
|
||||
<span id="adjust-item-location">Location</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="current-stock">
|
||||
<small class="text-muted">Current Stock</small>
|
||||
<h4 class="mb-0" id="adjust-current-stock">0</h4>
|
||||
<small class="text-muted" id="adjust-stock-unit">units</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adjustment Details -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="adjustment_type">
|
||||
Adjustment Type <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="adjustment_type" name="adjustment_type" required>
|
||||
<option value="">Select adjustment type...</option>
|
||||
<option value="increase">Increase Stock</option>
|
||||
<option value="decrease">Decrease Stock</option>
|
||||
<option value="set">Set Exact Amount</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="adjustment_quantity">
|
||||
<span id="quantity-label">Quantity</span> <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="adjustment_quantity"
|
||||
name="adjustment_quantity" min="0" step="0.01" required>
|
||||
<span class="input-group-text" id="adjust-quantity-unit">units</span>
|
||||
</div>
|
||||
<small class="form-text text-muted" id="quantity-help">
|
||||
Enter the amount to adjust
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="adjustment_reason">
|
||||
Reason <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="adjustment_reason" name="adjustment_reason" required>
|
||||
<option value="">Select reason...</option>
|
||||
<option value="damaged">Damaged/Expired</option>
|
||||
<option value="lost">Lost/Stolen</option>
|
||||
<option value="found">Found/Recovered</option>
|
||||
<option value="recount">Physical Recount</option>
|
||||
<option value="return">Return to Supplier</option>
|
||||
<option value="donation">Donation</option>
|
||||
<option value="correction">Data Correction</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="adjustment_reference">
|
||||
Reference Number
|
||||
</label>
|
||||
<input type="text" class="form-control" id="adjustment_reference"
|
||||
name="adjustment_reference" placeholder="Optional reference number">
|
||||
<small class="form-text text-muted">
|
||||
PO number, incident report, etc.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="adjustment_notes">
|
||||
Notes
|
||||
</label>
|
||||
<textarea class="form-control" id="adjustment_notes" name="adjustment_notes"
|
||||
rows="3" placeholder="Additional notes about this adjustment..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calculation Preview -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-info" id="calculation-preview" style="display: none;">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-calculator me-2"></i>Adjustment Preview
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4">
|
||||
<div class="preview-item">
|
||||
<small class="text-muted">Current Stock</small>
|
||||
<h5 class="mb-0" id="preview-current">0</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="preview-item">
|
||||
<small class="text-muted">Adjustment</small>
|
||||
<h5 class="mb-0" id="preview-adjustment">0</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="preview-item">
|
||||
<small class="text-muted">New Stock</small>
|
||||
<h5 class="mb-0 text-primary" id="preview-new">0</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
<div id="adjustment-warnings" style="display: none;">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span id="warning-message">Warning message</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="adjust-submit-btn" disabled>
|
||||
<i class="fas fa-save me-1"></i>Apply Adjustment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize adjust stock modal
|
||||
$('#adjustStockModal').on('show.bs.modal', function(event) {
|
||||
const button = $(event.relatedTarget);
|
||||
const itemId = button.data('item-id');
|
||||
const itemName = button.data('item-name');
|
||||
const itemCode = button.data('item-code');
|
||||
const itemCategory = button.data('item-category');
|
||||
const itemLocation = button.data('item-location');
|
||||
const currentStock = button.data('current-stock');
|
||||
const stockUnit = button.data('stock-unit');
|
||||
|
||||
// Update modal content
|
||||
$('#adjust-item-name').text(itemName);
|
||||
$('#adjust-item-code').text(itemCode);
|
||||
$('#adjust-item-category').text(itemCategory);
|
||||
$('#adjust-item-location').text(itemLocation);
|
||||
$('#adjust-current-stock').text(currentStock);
|
||||
$('#adjust-stock-unit').text(stockUnit);
|
||||
$('#adjust-quantity-unit').text(stockUnit);
|
||||
|
||||
// Store item data
|
||||
$('#adjustStockForm').data('item-id', itemId);
|
||||
$('#adjustStockForm').data('current-stock', currentStock);
|
||||
$('#adjustStockForm').data('stock-unit', stockUnit);
|
||||
|
||||
// Reset form
|
||||
$('#adjustStockForm')[0].reset();
|
||||
$('#calculation-preview').hide();
|
||||
$('#adjustment-warnings').hide();
|
||||
$('#adjust-submit-btn').prop('disabled', true);
|
||||
});
|
||||
|
||||
// Handle adjustment type change
|
||||
$('#adjustment_type').on('change', function() {
|
||||
const type = $(this).val();
|
||||
const quantityLabel = $('#quantity-label');
|
||||
const quantityHelp = $('#quantity-help');
|
||||
|
||||
switch(type) {
|
||||
case 'increase':
|
||||
quantityLabel.text('Increase By');
|
||||
quantityHelp.text('Enter amount to add to current stock');
|
||||
break;
|
||||
case 'decrease':
|
||||
quantityLabel.text('Decrease By');
|
||||
quantityHelp.text('Enter amount to subtract from current stock');
|
||||
break;
|
||||
case 'set':
|
||||
quantityLabel.text('Set To');
|
||||
quantityHelp.text('Enter the exact stock amount');
|
||||
break;
|
||||
default:
|
||||
quantityLabel.text('Quantity');
|
||||
quantityHelp.text('Enter the amount to adjust');
|
||||
}
|
||||
|
||||
updateCalculationPreview();
|
||||
});
|
||||
|
||||
// Handle quantity change
|
||||
$('#adjustment_quantity, #adjustment_reason').on('input change', function() {
|
||||
updateCalculationPreview();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#adjustStockForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const itemId = $(this).data('item-id');
|
||||
|
||||
// Add item ID to form data
|
||||
formData.append('item_id', itemId);
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = $('#adjust-submit-btn');
|
||||
const originalText = submitBtn.html();
|
||||
submitBtn.html('<i class="fas fa-spinner fa-spin me-1"></i>Processing...').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "pharmacy:adjust_inventory" 0 %}'.replace('0', itemId);
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
showToast('success', 'Stock adjusted successfully');
|
||||
|
||||
// Close modal
|
||||
$('#adjustStockModal').modal('hide');
|
||||
|
||||
// Refresh page or update stock display
|
||||
if (typeof refreshStockData === 'function') {
|
||||
refreshStockData();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
showToast('error', response.message || 'Error adjusting stock');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Error adjusting stock';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
showToast('error', errorMessage);
|
||||
},
|
||||
complete: function() {
|
||||
// Restore button state
|
||||
submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function updateCalculationPreview() {
|
||||
const type = $('#adjustment_type').val();
|
||||
const quantity = parseFloat($('#adjustment_quantity').val()) || 0;
|
||||
const reason = $('#adjustment_reason').val();
|
||||
const currentStock = parseFloat($('#adjustStockForm').data('current-stock')) || 0;
|
||||
|
||||
if (!type || !quantity || !reason) {
|
||||
$('#calculation-preview').hide();
|
||||
$('#adjustment-warnings').hide();
|
||||
$('#adjust-submit-btn').prop('disabled', true);
|
||||
return;
|
||||
}
|
||||
|
||||
let newStock = currentStock;
|
||||
let adjustmentDisplay = '';
|
||||
|
||||
switch(type) {
|
||||
case 'increase':
|
||||
newStock = currentStock + quantity;
|
||||
adjustmentDisplay = '+' + quantity;
|
||||
break;
|
||||
case 'decrease':
|
||||
newStock = currentStock - quantity;
|
||||
adjustmentDisplay = '-' + quantity;
|
||||
break;
|
||||
case 'set':
|
||||
newStock = quantity;
|
||||
adjustmentDisplay = quantity + ' (set)';
|
||||
break;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
$('#preview-current').text(currentStock);
|
||||
$('#preview-adjustment').text(adjustmentDisplay);
|
||||
$('#preview-new').text(newStock);
|
||||
|
||||
// Show/hide warnings
|
||||
let hasWarning = false;
|
||||
if (newStock < 0) {
|
||||
$('#warning-message').text('This adjustment will result in negative stock!');
|
||||
$('#adjustment-warnings').show();
|
||||
hasWarning = true;
|
||||
} else if (newStock === 0 && type === 'decrease') {
|
||||
$('#warning-message').text('This adjustment will deplete all stock.');
|
||||
$('#adjustment-warnings').show();
|
||||
hasWarning = true;
|
||||
} else {
|
||||
$('#adjustment-warnings').hide();
|
||||
}
|
||||
|
||||
// Show preview
|
||||
$('#calculation-preview').show();
|
||||
|
||||
// Enable/disable submit button
|
||||
$('#adjust-submit-btn').prop('disabled', false);
|
||||
}
|
||||
|
||||
function showToast(type, message) {
|
||||
// Simple toast notification
|
||||
const toastClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const toastHtml = `
|
||||
<div class="alert ${toastClass} alert-dismissible fade show position-fixed"
|
||||
style="top: 20px; right: 20px; z-index: 9999;">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
$('body').append(toastHtml);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.preview-item {
|
||||
padding: 10px;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.preview-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.current-stock {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#calculation-preview .card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.preview-item {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.preview-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
505
templates/pharmacy/partials/transfer_stock_modal.html
Normal file
505
templates/pharmacy/partials/transfer_stock_modal.html
Normal file
@ -0,0 +1,505 @@
|
||||
<!-- Transfer Stock Modal -->
|
||||
<div class="modal fade" id="transferStockModal" tabindex="-1" aria-labelledby="transferStockModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="transferStockModalLabel">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Stock
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="transferStockForm" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<!-- Item Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h6 class="card-title mb-1" id="transfer-item-name">Item Name</h6>
|
||||
<p class="card-text small text-muted mb-1">
|
||||
<span class="me-3">
|
||||
<i class="fas fa-barcode me-1"></i>
|
||||
<span id="transfer-item-code">Item Code</span>
|
||||
</span>
|
||||
<span class="me-3">
|
||||
<i class="fas fa-layer-group me-1"></i>
|
||||
<span id="transfer-item-category">Category</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="card-text small text-muted mb-0">
|
||||
<i class="fas fa-map-marker-alt me-1"></i>
|
||||
<strong>From:</strong> <span id="transfer-from-location">Location</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="current-stock">
|
||||
<small class="text-muted">Available Stock</small>
|
||||
<h4 class="mb-0" id="transfer-available-stock">0</h4>
|
||||
<small class="text-muted" id="transfer-stock-unit">units</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer Details -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_to_location">
|
||||
Transfer To <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="transfer_to_location" name="transfer_to_location" required>
|
||||
<option value="">Select destination...</option>
|
||||
{% for location in pharmacy_locations %}
|
||||
<option value="{{ location.id }}" data-location-name="{{ location.name }}">
|
||||
{{ location.name }} - {{ location.location_type }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_quantity">
|
||||
Quantity to Transfer <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="transfer_quantity"
|
||||
name="transfer_quantity" min="0.01" step="0.01" required>
|
||||
<span class="input-group-text" id="transfer-quantity-unit">units</span>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
Maximum: <span id="max-transfer-quantity">0</span> units
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_reason">
|
||||
Transfer Reason <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="transfer_reason" name="transfer_reason" required>
|
||||
<option value="">Select reason...</option>
|
||||
<option value="restock">Restock Location</option>
|
||||
<option value="redistribution">Stock Redistribution</option>
|
||||
<option value="patient_care">Patient Care Need</option>
|
||||
<option value="emergency">Emergency Request</option>
|
||||
<option value="maintenance">Equipment Maintenance</option>
|
||||
<option value="consolidation">Stock Consolidation</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_priority">
|
||||
Priority
|
||||
</label>
|
||||
<select class="form-select" id="transfer_priority" name="transfer_priority">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="emergency">Emergency</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_requested_by">
|
||||
Requested By
|
||||
</label>
|
||||
<select class="form-select" id="transfer_requested_by" name="transfer_requested_by">
|
||||
<option value="">Select staff member...</option>
|
||||
{% for staff in hospital_staff %}
|
||||
<option value="{{ staff.id }}" {% if staff.id == request.user.id %}selected{% endif %}>
|
||||
{{ staff.get_full_name }} - {{ staff.department }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_reference">
|
||||
Reference Number
|
||||
</label>
|
||||
<input type="text" class="form-control" id="transfer_reference"
|
||||
name="transfer_reference" placeholder="Optional reference">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="transfer_notes">
|
||||
Transfer Notes
|
||||
</label>
|
||||
<textarea class="form-control" id="transfer_notes" name="transfer_notes"
|
||||
rows="3" placeholder="Additional notes about this transfer..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transfer Summary -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-info" id="transfer-summary" style="display: none;">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h6 class="card-title mb-0">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Transfer Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="transfer-detail">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>From:</strong>
|
||||
<span id="summary-from-location">Source Location</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>To:</strong>
|
||||
<span id="summary-to-location">Destination Location</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>Quantity:</strong>
|
||||
<span id="summary-quantity">0 units</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="stock-impact">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>Remaining at Source:</span>
|
||||
<span class="fw-bold" id="summary-remaining">0 units</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span>New Stock at Destination:</span>
|
||||
<span class="fw-bold text-success" id="summary-new-stock">0 units</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Transfer Value:</span>
|
||||
<span class="fw-bold text-primary" id="summary-value">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
<div id="transfer-warnings" style="display: none;">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span id="transfer-warning-message">Warning message</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Required -->
|
||||
<div id="approval-required" style="display: none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-user-check me-2"></i>
|
||||
This transfer requires supervisor approval due to quantity or priority level.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info me-2" id="save-draft-btn" style="display: none;">
|
||||
<i class="fas fa-save me-1"></i>Save as Draft
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="transfer-submit-btn" disabled>
|
||||
<i class="fas fa-exchange-alt me-1"></i>
|
||||
<span id="submit-btn-text">Submit Transfer</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize transfer stock modal
|
||||
$('#transferStockModal').on('show.bs.modal', function(event) {
|
||||
const button = $(event.relatedTarget);
|
||||
const itemId = button.data('item-id');
|
||||
const itemName = button.data('item-name');
|
||||
const itemCode = button.data('item-code');
|
||||
const itemCategory = button.data('item-category');
|
||||
const fromLocation = button.data('from-location');
|
||||
const availableStock = button.data('available-stock');
|
||||
const stockUnit = button.data('stock-unit');
|
||||
const unitValue = button.data('unit-value') || 0;
|
||||
|
||||
// Update modal content
|
||||
$('#transfer-item-name').text(itemName);
|
||||
$('#transfer-item-code').text(itemCode);
|
||||
$('#transfer-item-category').text(itemCategory);
|
||||
$('#transfer-from-location').text(fromLocation);
|
||||
$('#transfer-available-stock').text(availableStock);
|
||||
$('#transfer-stock-unit').text(stockUnit);
|
||||
$('#transfer-quantity-unit').text(stockUnit);
|
||||
$('#max-transfer-quantity').text(availableStock);
|
||||
|
||||
// Store item data
|
||||
$('#transferStockForm').data('item-id', itemId);
|
||||
$('#transferStockForm').data('from-location', fromLocation);
|
||||
$('#transferStockForm').data('available-stock', availableStock);
|
||||
$('#transferStockForm').data('stock-unit', stockUnit);
|
||||
$('#transferStockForm').data('unit-value', unitValue);
|
||||
|
||||
// Set max quantity
|
||||
$('#transfer_quantity').attr('max', availableStock);
|
||||
|
||||
// Reset form
|
||||
$('#transferStockForm')[0].reset();
|
||||
$('#transfer-summary').hide();
|
||||
$('#transfer-warnings').hide();
|
||||
$('#approval-required').hide();
|
||||
$('#transfer-submit-btn').prop('disabled', true);
|
||||
});
|
||||
|
||||
// Handle input changes for validation and summary
|
||||
$('#transfer_to_location, #transfer_quantity, #transfer_reason, #transfer_priority').on('input change', function() {
|
||||
updateTransferSummary();
|
||||
validateTransferForm();
|
||||
});
|
||||
|
||||
// Handle quantity validation
|
||||
$('#transfer_quantity').on('input', function() {
|
||||
const quantity = parseFloat($(this).val()) || 0;
|
||||
const available = parseFloat($('#transferStockForm').data('available-stock')) || 0;
|
||||
|
||||
if (quantity > available) {
|
||||
$(this).val(available);
|
||||
$('#transfer-warning-message').text('Quantity cannot exceed available stock.');
|
||||
$('#transfer-warnings').show();
|
||||
} else {
|
||||
$('#transfer-warnings').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle priority change for approval requirements
|
||||
$('#transfer_priority').on('change', function() {
|
||||
const priority = $(this).val();
|
||||
const quantity = parseFloat($('#transfer_quantity').val()) || 0;
|
||||
|
||||
if (priority === 'emergency' || quantity > 100) { // Example threshold
|
||||
$('#approval-required').show();
|
||||
$('#submit-btn-text').text('Submit for Approval');
|
||||
$('#save-draft-btn').show();
|
||||
} else {
|
||||
$('#approval-required').hide();
|
||||
$('#submit-btn-text').text('Submit Transfer');
|
||||
$('#save-draft-btn').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#transferStockForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const itemId = $(this).data('item-id');
|
||||
|
||||
// Add item ID to form data
|
||||
formData.append('item_id', itemId);
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = $('#transfer-submit-btn');
|
||||
const originalText = submitBtn.html();
|
||||
submitBtn.html('<i class="fas fa-spinner fa-spin me-1"></i>Processing...').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: '',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
const message = response.requires_approval ?
|
||||
'Transfer request submitted for approval' :
|
||||
'Stock transferred successfully';
|
||||
showToast('success', message);
|
||||
|
||||
// Close modal
|
||||
$('#transferStockModal').modal('hide');
|
||||
|
||||
// Refresh page or update stock display
|
||||
if (typeof refreshStockData === 'function') {
|
||||
refreshStockData();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
showToast('error', response.message || 'Error processing transfer');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Error processing transfer';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
showToast('error', errorMessage);
|
||||
},
|
||||
complete: function() {
|
||||
// Restore button state
|
||||
submitBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save as draft functionality
|
||||
$('#save-draft-btn').on('click', function() {
|
||||
// Similar to form submission but with draft status
|
||||
const formData = new FormData(document.getElementById('transferStockForm'));
|
||||
formData.append('save_as_draft', 'true');
|
||||
|
||||
// Process draft save...
|
||||
showToast('info', 'Transfer saved as draft');
|
||||
$('#transferStockModal').modal('hide');
|
||||
});
|
||||
});
|
||||
|
||||
function updateTransferSummary() {
|
||||
const toLocationSelect = $('#transfer_to_location');
|
||||
const toLocationName = toLocationSelect.find('option:selected').data('location-name');
|
||||
const quantity = parseFloat($('#transfer_quantity').val()) || 0;
|
||||
const fromLocation = $('#transferStockForm').data('from-location');
|
||||
const availableStock = parseFloat($('#transferStockForm').data('available-stock')) || 0;
|
||||
const stockUnit = $('#transferStockForm').data('stock-unit');
|
||||
const unitValue = parseFloat($('#transferStockForm').data('unit-value')) || 0;
|
||||
|
||||
if (toLocationName && quantity > 0) {
|
||||
const remaining = availableStock - quantity;
|
||||
const transferValue = quantity * unitValue;
|
||||
|
||||
// Update summary
|
||||
$('#summary-from-location').text(fromLocation);
|
||||
$('#summary-to-location').text(toLocationName);
|
||||
$('#summary-quantity').text(quantity + ' ' + stockUnit);
|
||||
$('#summary-remaining').text(remaining + ' ' + stockUnit);
|
||||
$('#summary-new-stock').text(quantity + ' ' + stockUnit); // Simplified - would need destination current stock
|
||||
$('#summary-value').text('$' + transferValue.toFixed(2));
|
||||
|
||||
// Show summary
|
||||
$('#transfer-summary').show();
|
||||
} else {
|
||||
$('#transfer-summary').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function validateTransferForm() {
|
||||
const toLocation = $('#transfer_to_location').val();
|
||||
const quantity = parseFloat($('#transfer_quantity').val()) || 0;
|
||||
const reason = $('#transfer_reason').val();
|
||||
const available = parseFloat($('#transferStockForm').data('available-stock')) || 0;
|
||||
|
||||
const isValid = toLocation && quantity > 0 && quantity <= available && reason;
|
||||
|
||||
$('#transfer-submit-btn').prop('disabled', !isValid);
|
||||
|
||||
if (isValid) {
|
||||
$('#transfer-submit-btn').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
} else {
|
||||
$('#transfer-submit-btn').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(type, message) {
|
||||
// Simple toast notification
|
||||
let toastClass;
|
||||
switch(type) {
|
||||
case 'success':
|
||||
toastClass = 'alert-success';
|
||||
break;
|
||||
case 'error':
|
||||
toastClass = 'alert-danger';
|
||||
break;
|
||||
case 'info':
|
||||
toastClass = 'alert-info';
|
||||
break;
|
||||
default:
|
||||
toastClass = 'alert-secondary';
|
||||
}
|
||||
|
||||
const toastHtml = `
|
||||
<div class="alert ${toastClass} alert-dismissible fade show position-fixed"
|
||||
style="top: 20px; right: 20px; z-index: 9999;">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
$('body').append(toastHtml);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.current-stock {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.transfer-detail, .stock-impact {
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#transfer-summary .card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Priority badges */
|
||||
.priority-normal { color: #28a745; }
|
||||
.priority-urgent { color: #ffc107; }
|
||||
.priority-emergency { color: #dc3545; }
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.transfer-detail, .stock-impact {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.d-flex.justify-content-between span:last-child {
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
templates/radiology/.DS_Store
vendored
BIN
templates/radiology/.DS_Store
vendored
Binary file not shown.
@ -548,7 +548,6 @@ function viewImages(seriesId) {
|
||||
}
|
||||
|
||||
function exportSeries(seriesId) {
|
||||
// Export series functionality
|
||||
if (confirm('Export this imaging series?')) {
|
||||
window.location.href = `/radiology/series/${seriesId}/export/`;
|
||||
}
|
||||
|
||||
@ -331,7 +331,7 @@
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'radiology:imaging_study_edit' study.pk %}">
|
||||
<li><a class="dropdown-item" href="{% url 'radiology:imaging_study_update' study.pk %}">
|
||||
<i class="fa fa-edit me-2"></i>Edit
|
||||
</a></li>
|
||||
{% if study.dicom_files.exists %}
|
||||
@ -340,9 +340,9 @@
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">
|
||||
<i class="fa fa-trash me-2"></i>Delete
|
||||
</a></li>
|
||||
{# <li><a class="dropdown-item text-danger" href="{% url 'radiology:im' study.pk %}">#}
|
||||
{# <i class="fa fa-trash me-2"></i>Delete#}
|
||||
{# </a></li>#}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -428,7 +428,7 @@
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'radiology:imaging_study_edit' study.pk %}">
|
||||
<li><a class="dropdown-item" href="{% url 'radiology:imaging_study_update' study.pk %}">
|
||||
<i class="fa fa-edit me-2"></i>Edit
|
||||
</a></li>
|
||||
{% if study.dicom_files.exists %}
|
||||
@ -437,9 +437,9 @@
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">
|
||||
<i class="fa fa-trash me-2"></i>Delete
|
||||
</a></li>
|
||||
{# <li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">#}
|
||||
{# <i class="fa fa-trash me-2"></i>Delete#}
|
||||
{# </a></li>#}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -626,7 +626,7 @@ function bulkAction(action) {
|
||||
if (confirm(`Are you sure you want to ${actionText[action]} ${selectedStudies.length} selected studies?`)) {
|
||||
// Perform bulk action via AJAX
|
||||
$.ajax({
|
||||
url: '{% url "radiology:bulk_study_action" %}',
|
||||
url: '',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'action': action,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user