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(
|
self.fields['data_source'].queryset = DataSource.objects.filter(
|
||||||
tenant=user.tenant,
|
tenant=user.tenant,
|
||||||
is_active=True
|
is_active=True
|
||||||
).order_by('source_name')
|
).order_by('name')
|
||||||
|
|
||||||
def clean_report_name(self):
|
def clean_report_name(self):
|
||||||
name = self.cleaned_data['report_name']
|
name = self.cleaned_data['report_name']
|
||||||
|
|||||||
@ -46,8 +46,8 @@ urlpatterns = [
|
|||||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||||
path('reports/create/', views.ReportCreateView.as_view(), name='report_create'),
|
path('reports/create/', views.ReportCreateView.as_view(), name='report_create'),
|
||||||
path('reports/<uuid:pk>/', views.ReportDetailView.as_view(), name='report_detail'),
|
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/<uuid: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>/delete/', views.ReportDeleteView.as_view(), name='report_delete'),
|
||||||
path('ajax/report-list/', views.report_list, name='report_list_data'),
|
path('ajax/report-list/', views.report_list, name='report_list_data'),
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -82,7 +82,7 @@ urlpatterns = [
|
|||||||
# ACTION URLS FOR WORKFLOW OPERATIONS
|
# ACTION URLS FOR WORKFLOW OPERATIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
path('data-sources/<int:data_source_id>/test/', views.test_data_source, name='test_data_source'),
|
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'),
|
path('metrics/<int:metric_id>/calculate/', views.calculate_metric, name='calculate_metric'),
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -384,7 +384,7 @@ class DataSourceListView(LoginRequiredMixin, ListView):
|
|||||||
if test_status:
|
if test_status:
|
||||||
queryset = queryset.filter(last_test_status=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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@ -413,12 +413,12 @@ class DataSourceDetailView(LoginRequiredMixin, DetailView):
|
|||||||
# Related reports
|
# Related reports
|
||||||
context['related_reports'] = Report.objects.filter(
|
context['related_reports'] = Report.objects.filter(
|
||||||
data_source=data_source
|
data_source=data_source
|
||||||
).order_by('report_name')
|
).order_by('name')
|
||||||
|
|
||||||
# Related metrics
|
# Related metrics
|
||||||
context['related_metrics'] = MetricDefinition.objects.filter(
|
context['related_metrics'] = MetricDefinition.objects.filter(
|
||||||
data_source=data_source
|
data_source=data_source
|
||||||
).order_by('metric_name')
|
).order_by('name')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -557,7 +557,7 @@ class ReportDetailView(LoginRequiredMixin, DetailView):
|
|||||||
# Recent executions
|
# Recent executions
|
||||||
context['recent_executions'] = ReportExecution.objects.filter(
|
context['recent_executions'] = ReportExecution.objects.filter(
|
||||||
report=report
|
report=report
|
||||||
).order_by('-execution_time')[:10]
|
).order_by('-started_at')[:10]
|
||||||
|
|
||||||
# Execution statistics
|
# Execution statistics
|
||||||
context['total_executions'] = ReportExecution.objects.filter(
|
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
|
# Main views
|
||||||
path('', views.EMRDashboardView.as_view(), name='dashboard'),
|
path('', views.EMRDashboardView.as_view(), name='dashboard'),
|
||||||
path('encounters/', views.EncounterListView.as_view(), name='encounter_list'),
|
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>/update/', views.EncounterUpdateView.as_view(), name='encounter_update'),
|
||||||
# path('encounters/<int:pk>/delete/', views.EncounterDeleteView.as_view(), name='encounter_delete'),
|
# path('encounters/<int:pk>/delete/', views.EncounterDeleteView.as_view(), name='encounter_delete'),
|
||||||
path('encounters/create/', views.EncounterCreateView.as_view(), name='encounter_create'),
|
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 = [
|
urlpatterns = [
|
||||||
# Main views
|
# Main views
|
||||||
path('', views.PatientListView.as_view(), name='patient_list'),
|
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('register/', views.PatientCreateView.as_view(), name='patient_registration'),
|
||||||
path('update/<int:pk>/', views.PatientUpdateView.as_view(), name='patient_update'),
|
path('update/<int:pk>/', views.PatientUpdateView.as_view(), name='patient_update'),
|
||||||
path('delete/<int:pk>/', views.PatientDeleteView.as_view(), name='patient_delete'),
|
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>/verify/', views.verify_prescription, name='verify_prescription'),
|
||||||
path('prescription/<int:prescription_id>/dispense/', views.dispense_medication, name='dispense_medication'),
|
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/<int:item_id>/update/', views.update_inventory, name='update_inventory'),
|
||||||
|
path('inventory-adjustment/<int:item_id>/', views.adjust_inventory, name='adjust_inventory'),
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
# path('api/', include('pharmacy.api.urls')),
|
# path('api/', include('pharmacy.api.urls')),
|
||||||
|
|||||||
@ -417,10 +417,10 @@ class InventoryItemListView(LoginRequiredMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = InventoryItem.objects.filter(
|
queryset = InventoryItem.objects.filter(
|
||||||
medication__tenant=self.request.user.tenant
|
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
|
# Apply search filters
|
||||||
form = InventorySearchForm(data=self.request.GET, user=self.request.user)
|
form = InventoryItemForm(data=self.request.GET)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
search = form.cleaned_data.get('search')
|
search = form.cleaned_data.get('search')
|
||||||
if search:
|
if search:
|
||||||
@ -458,9 +458,9 @@ class InventoryItemListView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['search_form'] = InventorySearchForm(
|
context['search_form'] = InventoryItemForm(
|
||||||
data=self.request.GET,
|
data=self.request.GET,
|
||||||
user=self.request.user
|
# user=self.request.user
|
||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -487,7 +487,7 @@ class InventoryItemDetailView(LoginRequiredMixin, DetailView):
|
|||||||
inventory_item=inventory_item
|
inventory_item=inventory_item
|
||||||
).select_related(
|
).select_related(
|
||||||
'prescription__patient', 'dispensed_by'
|
'prescription__patient', 'dispensed_by'
|
||||||
).order_by('-dispensed_date')
|
).order_by('-date_dispensed')
|
||||||
|
|
||||||
# Calculate total dispensed
|
# Calculate total dispensed
|
||||||
context['total_dispensed'] = DispenseRecord.objects.filter(
|
context['total_dispensed'] = DispenseRecord.objects.filter(
|
||||||
@ -495,8 +495,8 @@ class InventoryItemDetailView(LoginRequiredMixin, DetailView):
|
|||||||
).aggregate(total=Sum('quantity_dispensed'))['total'] or 0
|
).aggregate(total=Sum('quantity_dispensed'))['total'] or 0
|
||||||
|
|
||||||
# Days until expiry
|
# Days until expiry
|
||||||
if inventory_item.expiry_date:
|
if inventory_item.expiration_date:
|
||||||
days_until_expiry = (inventory_item.expiry_date - timezone.now().date()).days
|
days_until_expiry = (inventory_item.expiration_date - timezone.now().date()).days
|
||||||
context['days_until_expiry'] = days_until_expiry
|
context['days_until_expiry'] = days_until_expiry
|
||||||
context['is_expiring_soon'] = days_until_expiry <= 30
|
context['is_expiring_soon'] = days_until_expiry <= 30
|
||||||
|
|
||||||
@ -2054,55 +2054,55 @@ def update_inventory(request, item_id):
|
|||||||
# })
|
# })
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
@login_required
|
||||||
# def adjust_inventory(request, pk):
|
def adjust_inventory(request, pk):
|
||||||
# """
|
"""
|
||||||
# Action view to adjust inventory quantity.
|
Action view to adjust inventory quantity.
|
||||||
# """
|
"""
|
||||||
# inventory_item = get_object_or_404(
|
inventory_item = get_object_or_404(
|
||||||
# InventoryItem,
|
InventoryItem,
|
||||||
# pk=pk,
|
pk=pk,
|
||||||
# medication__tenant=request.user.tenant
|
medication__tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# adjustment = int(request.POST.get('adjustment', 0))
|
adjustment = int(request.POST.get('adjustment', 0))
|
||||||
# reason = request.POST.get('reason', '')
|
reason = request.POST.get('reason', '')
|
||||||
#
|
|
||||||
# if adjustment == 0:
|
if adjustment == 0:
|
||||||
# messages.error(request, 'Adjustment amount cannot be zero.')
|
messages.error(request, 'Adjustment amount cannot be zero.')
|
||||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||||
#
|
|
||||||
# old_quantity = inventory_item.quantity_on_hand
|
old_quantity = inventory_item.quantity_on_hand
|
||||||
# inventory_item.quantity_on_hand += adjustment
|
inventory_item.quantity_on_hand += adjustment
|
||||||
#
|
|
||||||
# if inventory_item.quantity_on_hand < 0:
|
if inventory_item.quantity_on_hand < 0:
|
||||||
# messages.error(request, 'Adjustment would result in negative inventory.')
|
messages.error(request, 'Adjustment would result in negative inventory.')
|
||||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||||
#
|
|
||||||
# inventory_item.save()
|
inventory_item.save()
|
||||||
#
|
|
||||||
# # Create audit log
|
# Create audit log
|
||||||
# AuditLogEntry.objects.create(
|
AuditLogEntry.objects.create(
|
||||||
# tenant=request.user.tenant,
|
tenant=request.user.tenant,
|
||||||
# user=request.user,
|
user=request.user,
|
||||||
# action='UPDATE',
|
action='UPDATE',
|
||||||
# model_name='InventoryItem',
|
model_name='InventoryItem',
|
||||||
# object_id=inventory_item.id,
|
object_id=inventory_item.id,
|
||||||
# changes={
|
changes={
|
||||||
# 'old_quantity': old_quantity,
|
'old_quantity': old_quantity,
|
||||||
# 'new_quantity': inventory_item.quantity_on_hand,
|
'new_quantity': inventory_item.quantity_on_hand,
|
||||||
# 'adjustment': adjustment,
|
'adjustment': adjustment,
|
||||||
# 'reason': reason
|
'reason': reason
|
||||||
# }
|
}
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# messages.success(request, f'Inventory adjusted by {adjustment} units.')
|
messages.success(request, f'Inventory adjusted by {adjustment} units.')
|
||||||
# return redirect('pharmacy:inventory_detail', pk=pk)
|
return redirect('pharmacy:inventory_detail', pk=pk)
|
||||||
#
|
|
||||||
# return render(request, 'pharmacy/adjust_inventory.html', {
|
return render(request, 'pharmacy/adjust_inventory.html', {
|
||||||
# 'inventory_item': inventory_item
|
'inventory_item': inventory_item
|
||||||
# })
|
})
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# # Export Views
|
# # Export Views
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -44,8 +44,8 @@ urlpatterns = [
|
|||||||
# IMAGING SERIES URLS (RESTRICTED CRUD - Clinical Data)
|
# IMAGING SERIES URLS (RESTRICTED CRUD - Clinical Data)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# urls.py
|
# urls.py
|
||||||
path('studies/<uuid:study_pk>/series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
path('studies/<int:pk>/series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
||||||
path('series/<uuid:pk>/', views.ImagingSeriesDetailView.as_view(), name='imaging_series_detail'),
|
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/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>/edit/', views.ImagingSeriesUpdateView.as_view(), name='imaging_series_edit'),
|
||||||
# path('series/<uuid:pk>/delete/', views.ImagingSeriesDeleteView.as_view(), name='imaging_series_delete'),
|
# path('series/<uuid:pk>/delete/', views.ImagingSeriesDeleteView.as_view(), name='imaging_series_delete'),
|
||||||
|
|||||||
@ -413,7 +413,7 @@ class ImagingOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
|||||||
"""
|
"""
|
||||||
model = ImagingOrder
|
model = ImagingOrder
|
||||||
fields = ['status', 'notes'] # Restricted fields for clinical orders
|
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'
|
permission_required = 'radiology.change_imagingorder'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -462,9 +462,9 @@ class ImagingStudyListView(LoginRequiredMixin, ListView):
|
|||||||
if search:
|
if search:
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(study_id__icontains=search) |
|
Q(study_id__icontains=search) |
|
||||||
Q(order__patient__first_name__icontains=search) |
|
Q(imaging_order__patient__first_name__icontains=search) |
|
||||||
Q(order__patient__last_name__icontains=search) |
|
Q(imaging_order__patient__last_name__icontains=search) |
|
||||||
Q(order__patient__mrn__icontains=search)
|
Q(imaging_order__patient__mrn__icontains=search)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter by status
|
# Filter by status
|
||||||
@ -475,9 +475,9 @@ class ImagingStudyListView(LoginRequiredMixin, ListView):
|
|||||||
# Filter by modality
|
# Filter by modality
|
||||||
modality = self.request.GET.get('modality')
|
modality = self.request.GET.get('modality')
|
||||||
if 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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@ -494,7 +494,7 @@ class ImagingStudyDetailView(LoginRequiredMixin, DetailView):
|
|||||||
"""
|
"""
|
||||||
model = ImagingStudy
|
model = ImagingStudy
|
||||||
template_name = 'radiology/studies/imaging_study_detail.html'
|
template_name = 'radiology/studies/imaging_study_detail.html'
|
||||||
context_object_name = 'imaging_study'
|
context_object_name = 'study'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ImagingStudy.objects.filter(tenant=self.request.user.tenant)
|
return ImagingStudy.objects.filter(tenant=self.request.user.tenant)
|
||||||
@ -504,15 +504,15 @@ class ImagingStudyDetailView(LoginRequiredMixin, DetailView):
|
|||||||
imaging_study = self.object
|
imaging_study = self.object
|
||||||
|
|
||||||
# Get series for this study
|
# 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
|
# Get reports for this study
|
||||||
context['reports'] = imaging_study.reports.all().order_by('-created_at')
|
context['reports'] = imaging_study.report
|
||||||
|
|
||||||
# Get total image count
|
# Get total image count
|
||||||
context['total_images'] = DICOMImage.objects.filter(
|
context['total_images'] = DICOMImage.objects.filter(
|
||||||
series__study=imaging_study,
|
series__study=imaging_study,
|
||||||
tenant=self.request.user.tenant
|
series__study__tenant=self.request.user.tenant
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -596,33 +596,33 @@ class ImagingSeriesListView(LoginRequiredMixin, ListView):
|
|||||||
context_object_name = 'imaging_series'
|
context_object_name = 'imaging_series'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
# def get_queryset(self):
|
def get_queryset(self):
|
||||||
# queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||||
#
|
|
||||||
# # Filter by study
|
# Filter by study
|
||||||
# study_id = self.request.GET.get('study')
|
study_id = self.request.GET.get('study')
|
||||||
# if study_id:
|
if study_id:
|
||||||
# queryset = queryset.filter(study_id=study_id)
|
queryset = queryset.filter(study_id=study_id)
|
||||||
#
|
|
||||||
# # Search functionality
|
# Search functionality
|
||||||
# search = self.request.GET.get('search')
|
search = self.request.GET.get('search')
|
||||||
# if search:
|
if search:
|
||||||
# queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
# Q(series_description__icontains=search) |
|
Q(series_description__icontains=search) |
|
||||||
# Q(study__order__patient__first_name__icontains=search) |
|
Q(study__imaging_order__patient__first_name__icontains=search) |
|
||||||
# Q(study__order__patient__last_name__icontains=search)
|
Q(study__imaging_order__patient__last_name__icontains=search)
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# return queryset.select_related('study__imaging_order__patient').order_by('-created_at')
|
return queryset.select_related('study__imaging_order__patient').order_by('-created_at')
|
||||||
#
|
|
||||||
# def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# context.update({
|
context.update({
|
||||||
# 'studies': ImagingStudy.objects.filter(
|
'studies': ImagingStudy.objects.filter(
|
||||||
# tenant=self.request.user.tenant
|
tenant=self.request.user.tenant
|
||||||
# ).select_related('imaging_order__patient').order_by('-study_datetime')[:50],
|
).select_related('imaging_order__patient').order_by('-study_datetime')[:50],
|
||||||
# })
|
})
|
||||||
# return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
||||||
@ -634,7 +634,7 @@ class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = 'imaging_series'
|
context_object_name = 'imaging_series'
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**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();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSources() {
|
{#function exportSources() {#}
|
||||||
const selectedIds = Array.from(document.querySelectorAll('.row-checkbox:checked'))
|
{# const selectedIds = Array.from(document.querySelectorAll('.row-checkbox:checked'))#}
|
||||||
.map(cb => cb.value);
|
{# .map(cb => cb.value);#}
|
||||||
|
{# #}
|
||||||
if (selectedIds.length === 0) {
|
{# if (selectedIds.length === 0) {#}
|
||||||
alert('Please select data sources to export');
|
{# alert('Please select data sources to export');#}
|
||||||
return;
|
{# return;#}
|
||||||
}
|
{# }#}
|
||||||
|
{# #}
|
||||||
// Create export URL with selected IDs
|
{# // Create export URL with selected IDs#}
|
||||||
const params = new URLSearchParams();
|
{# const params = new URLSearchParams();#}
|
||||||
selectedIds.forEach(id => params.append('ids', id));
|
{# selectedIds.forEach(id => params.append('ids', id));#}
|
||||||
|
{# #}
|
||||||
window.open(`{% url 'analytics:data_source_export' %}?${params.toString()}`);
|
{# window.open(`{% url 'analytics:data_source_export' %}?${params.toString()}`);#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
function testConnection(sourceId) {
|
function testConnection(sourceId) {
|
||||||
const btn = event.target.closest('button');
|
const btn = event.target.closest('button');
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
<button class="btn btn-xs btn-success me-2" onclick="generateReport()">
|
<button class="btn btn-xs btn-success me-2" onclick="generateReport()">
|
||||||
<i class="fa fa-play"></i> Generate
|
<i class="fa fa-play"></i> Generate
|
||||||
</button>
|
</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
|
<i class="fa fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></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() {
|
function generateReport() {
|
||||||
if (confirm('Generate this report now?')) {
|
if (confirm('Generate this report now?')) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{% url "analytics:report_generate" object.pk %}',
|
url: '{% url "analytics:execute_report" object.pk %}',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||||
@ -365,66 +365,66 @@ function scheduleReport() {
|
|||||||
toastr.info('Schedule management functionality will be implemented');
|
toastr.info('Schedule management functionality will be implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateReport() {
|
{#function duplicateReport() {#}
|
||||||
if (confirm('Create a copy of this report?')) {
|
{# if (confirm('Create a copy of this report?')) {#}
|
||||||
$.ajax({
|
{# $.ajax({#}
|
||||||
url: '{% url "analytics:report_duplicate" object.pk %}',
|
{# url: '{% url "analytics:report_duplicate" object.pk %}',#}
|
||||||
method: 'POST',
|
{# method: 'POST',#}
|
||||||
data: {
|
{# data: {#}
|
||||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||||
},
|
{# },#}
|
||||||
success: function(response) {
|
{# success: function(response) {#}
|
||||||
if (response.success) {
|
{# if (response.success) {#}
|
||||||
toastr.success('Report duplicated successfully');
|
{# toastr.success('Report duplicated successfully');#}
|
||||||
window.location.href = response.redirect_url;
|
{# window.location.href = response.redirect_url;#}
|
||||||
} else {
|
{# } else {#}
|
||||||
toastr.error('Failed to duplicate report');
|
{# toastr.error('Failed to duplicate report');#}
|
||||||
}
|
{# }#}
|
||||||
},
|
{# },#}
|
||||||
error: function() {
|
{# error: function() {#}
|
||||||
toastr.error('An error occurred while duplicating the report');
|
{# toastr.error('An error occurred while duplicating the report');#}
|
||||||
}
|
{# }#}
|
||||||
});
|
{# });#}
|
||||||
}
|
{# }#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
function exportReport() {
|
{#function exportReport() {#}
|
||||||
window.location.href = '{% url "analytics:report_export_definition" object.pk %}';
|
{# window.location.href = '{% url "analytics:report_export_definition" object.pk %}';#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
function viewError(executionId) {
|
{#function viewError(executionId) {#}
|
||||||
$.ajax({
|
{# $.ajax({#}
|
||||||
url: '{% url "analytics:execution_error" 0 %}'.replace('0', executionId),
|
{# url: '{% url "analytics:execution_error" 0 %}'.replace('0', executionId),#}
|
||||||
success: function(response) {
|
{# success: function(response) {#}
|
||||||
// Show error details in a modal
|
{# // Show error details in a modal#}
|
||||||
var modal = $('<div class="modal fade" tabindex="-1">' +
|
{# var modal = $('<div class="modal fade" tabindex="-1">' +#}
|
||||||
'<div class="modal-dialog modal-lg">' +
|
{# '<div class="modal-dialog modal-lg">' +#}
|
||||||
'<div class="modal-content">' +
|
{# '<div class="modal-content">' +#}
|
||||||
'<div class="modal-header">' +
|
{# '<div class="modal-header">' +#}
|
||||||
'<h5 class="modal-title">Execution Error</h5>' +
|
{# '<h5 class="modal-title">Execution Error</h5>' +#}
|
||||||
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
|
{# '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +#}
|
||||||
'</div>' +
|
{# '</div>' +#}
|
||||||
'<div class="modal-body">' +
|
{# '<div class="modal-body">' +#}
|
||||||
'<pre>' + response.error_message + '</pre>' +
|
{# '<pre>' + response.error_message + '</pre>' +#}
|
||||||
'</div>' +
|
{# '</div>' +#}
|
||||||
'<div class="modal-footer">' +
|
{# '<div class="modal-footer">' +#}
|
||||||
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +
|
{# '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' +#}
|
||||||
'</div>' +
|
{# '</div>' +#}
|
||||||
'</div>' +
|
{# '</div>' +#}
|
||||||
'</div>' +
|
{# '</div>' +#}
|
||||||
'</div>');
|
{# '</div>');#}
|
||||||
|
{# #}
|
||||||
$('body').append(modal);
|
{# $('body').append(modal);#}
|
||||||
modal.modal('show');
|
{# modal.modal('show');#}
|
||||||
modal.on('hidden.bs.modal', function() {
|
{# modal.on('hidden.bs.modal', function() {#}
|
||||||
modal.remove();
|
{# modal.remove();#}
|
||||||
});
|
{# });#}
|
||||||
},
|
{# },#}
|
||||||
error: function() {
|
{# error: function() {#}
|
||||||
toastr.error('Failed to load error details');
|
{# toastr.error('Failed to load error details');#}
|
||||||
}
|
{# }#}
|
||||||
});
|
{# });#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
// Auto-refresh if report is currently running
|
// Auto-refresh if report is currently running
|
||||||
{% if object.is_running %}
|
{% if object.is_running %}
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
{% block title %}{% if object %}Edit Report{% else %}Create Report{% endif %} - Analytics{% endblock %}
|
{% block title %}{% if object %}Edit Report{% else %}Create Report{% endif %} - Analytics{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/codemirror/lib/codemirror.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/codemirror/theme/material.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/codemirror/theme/material.css' %}" rel="stylesheet" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -350,10 +350,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/codemirror/lib/codemirror.js' %}"></script>
|
<script src="{% static 'plugins/codemirror/lib/codemirror.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/codemirror/mode/sql/sql.js' %}"></script>
|
<script src="{% static 'plugins/codemirror/mode/sql/sql.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/codemirror/mode/javascript/javascript.js' %}"></script>
|
<script src="{% static 'plugins/codemirror/mode/javascript/javascript.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize Select2
|
// Initialize Select2
|
||||||
|
|||||||
@ -336,6 +336,7 @@ $(document).ready(function() {
|
|||||||
actions += '<button class="btn btn-outline-secondary" onclick="editReport(' + data + ')" title="Edit">';
|
actions += '<button class="btn btn-outline-secondary" onclick="editReport(' + data + ')" title="Edit">';
|
||||||
actions += '<i class="fa fa-edit"></i></button>';
|
actions += '<i class="fa fa-edit"></i></button>';
|
||||||
actions += '</div>';
|
actions += '</div>';
|
||||||
|
console.log(data)
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -392,9 +393,11 @@ function updateBulkActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function viewReport(reportId) {
|
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) {
|
function editReport(reportId) {
|
||||||
window.location.href = '{% url "analytics:report_update" 0 %}'.replace('0', reportId);
|
window.location.href = '{% url "analytics:report_update" 0 %}'.replace('0', reportId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,9 @@
|
|||||||
<!-- ================== BEGIN core-css ================== -->
|
<!-- ================== BEGIN core-css ================== -->
|
||||||
|
|
||||||
<link href="{% static 'css/vendor.min.css' %}" rel="stylesheet" />
|
<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" />
|
<link href="{% static 'css/custom.css' %}" rel="stylesheet" />
|
||||||
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
||||||
<!-- HTMX -->
|
<!-- 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">
|
<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
|
<i class="fa fa-plus"></i> Add Stock
|
||||||
</button>
|
</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
|
<i class="fa fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></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()">
|
<button type="button" class="btn btn-info" onclick="performStockCount()">
|
||||||
<i class="fa fa-clipboard-check me-2"></i>Stock Count
|
<i class="fa fa-clipboard-check me-2"></i>Stock Count
|
||||||
</button>
|
</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
|
<i class="fa fa-edit me-2"></i>Edit Details
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="printLabel()">
|
<button type="button" class="btn btn-outline-primary" onclick="printLabel()">
|
||||||
@ -427,44 +427,44 @@ function printLabel() {
|
|||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPurchaseOrder() {
|
{#function createPurchaseOrder() {#}
|
||||||
// Implementation for creating purchase orders
|
{# // Implementation for creating purchase orders#}
|
||||||
window.location.href = '{% url "pharmacy:create_purchase_order" %}?medication={{ object.medication.pk }}&quantity={{ object.suggested_order_quantity }}';
|
{# window.location.href = '{% url "pharmacy:create_purchase_order" %}?medication={{ object.medication.pk }}&quantity={{ object.suggested_order_quantity }}';#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
function markExpired() {
|
{#function markExpired() {#}
|
||||||
if (confirm('Are you sure you want to mark this inventory as expired?')) {
|
{# if (confirm('Are you sure you want to mark this inventory as expired?')) {#}
|
||||||
$.ajax({
|
{# $.ajax({#}
|
||||||
url: '{% url "pharmacy:mark_inventory_expired" object.pk %}',
|
{# url: '{% url "pharmacy:mark_inventory_expired" object.pk %}',#}
|
||||||
method: 'POST',
|
{# method: 'POST',#}
|
||||||
data: {
|
{# data: {#}
|
||||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
{# 'csrfmiddlewaretoken': '{{ csrf_token }}'#}
|
||||||
},
|
{# },#}
|
||||||
success: function(response) {
|
{# success: function(response) {#}
|
||||||
toastr.success('Inventory marked as expired');
|
{# toastr.success('Inventory marked as expired');#}
|
||||||
location.reload();
|
{# location.reload();#}
|
||||||
},
|
{# },#}
|
||||||
error: function() {
|
{# error: function() {#}
|
||||||
toastr.error('Failed to mark inventory as expired');
|
{# toastr.error('Failed to mark inventory as expired');#}
|
||||||
}
|
{# }#}
|
||||||
});
|
{# });#}
|
||||||
}
|
{# }#}
|
||||||
}
|
{# }#}
|
||||||
|
|
||||||
// Auto-refresh for real-time updates
|
// Auto-refresh for real-time updates
|
||||||
setInterval(function() {
|
{#setInterval(function() {#}
|
||||||
// Refresh stock level display
|
{# // Refresh stock level display#}
|
||||||
$.ajax({
|
{# $.ajax({#}
|
||||||
url: '{% url "pharmacy:inventory_status" object.pk %}',
|
{# url: '{% url "pharmacy:inventory_status" object.pk %}',#}
|
||||||
method: 'GET',
|
{# method: 'GET',#}
|
||||||
success: function(response) {
|
{# success: function(response) {#}
|
||||||
// Update stock display if changed
|
{# // Update stock display if changed#}
|
||||||
if (response.current_stock !== {{ object.current_stock }}) {
|
{# if (response.current_stock !== {{ object.current_stock }}) {#}
|
||||||
location.reload();
|
{# location.reload();#}
|
||||||
}
|
{# }#}
|
||||||
}
|
{# }#}
|
||||||
});
|
{# });#}
|
||||||
}, 30000); // Refresh every 30 seconds
|
{# }, 30000); #}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -137,79 +137,79 @@
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3" id="filterForm">
|
{# <form method="get" class="row g-3" id="filterForm">#}
|
||||||
<div class="col-lg-4 col-md-6">
|
{# <div class="col-lg-4 col-md-6">#}
|
||||||
<label for="search" class="form-label">Search Items</label>
|
{# <label for="search" class="form-label">Search Items</label>#}
|
||||||
<div class="input-group">
|
{# <div class="input-group">#}
|
||||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
{# <span class="input-group-text"><i class="fas fa-search"></i></span>#}
|
||||||
<input type="text"
|
{# <input type="text"#}
|
||||||
class="form-control"
|
{# class="form-control"#}
|
||||||
id="search"
|
{# id="search"#}
|
||||||
name="search"
|
{# name="search"#}
|
||||||
value="{{ request.GET.search }}"
|
{# value="{{ request.GET.search }}"#}
|
||||||
placeholder="Medication name, lot number, NDC..."
|
{# placeholder="Medication name, lot number, NDC..."#}
|
||||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||||
hx-target="#inventory-list-container"
|
{# hx-target="#inventory-list-container"#}
|
||||||
hx-trigger="keyup changed delay:500ms"
|
{# hx-trigger="keyup changed delay:500ms"#}
|
||||||
hx-include="#filterForm">
|
{# hx-include="#filterForm">#}
|
||||||
</div>
|
{# </div>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
|
{# #}
|
||||||
<div class="col-lg-2 col-md-6">
|
{# <div class="col-lg-2 col-md-6">#}
|
||||||
<label for="location" class="form-label">Location</label>
|
{# <label for="location" class="form-label">Location</label>#}
|
||||||
<select class="form-select" id="location" name="location"
|
{# <select class="form-select" id="location" name="location"#}
|
||||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||||
hx-target="#inventory-list-container"
|
{# hx-target="#inventory-list-container"#}
|
||||||
hx-trigger="change"
|
{# hx-trigger="change"#}
|
||||||
hx-include="#filterForm">
|
{# hx-include="#filterForm">#}
|
||||||
<option value="">All Locations</option>
|
{# <option value="">All Locations</option>#}
|
||||||
{% for location in locations %}
|
{# {% for location in locations %}#}
|
||||||
<option value="{{ location.id }}" {% if request.GET.location == location.id|stringformat:"s" %}selected{% endif %}>{{ location.name }}</option>
|
{# <option value="{{ location.id }}" {% if request.GET.location == location.id|stringformat:"s" %}selected{% endif %}>{{ location.name }}</option>#}
|
||||||
{% endfor %}
|
{# {% endfor %}#}
|
||||||
</select>
|
{# </select>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
|
{# #}
|
||||||
<div class="col-lg-2 col-md-6">
|
{# <div class="col-lg-2 col-md-6">#}
|
||||||
<label for="status" class="form-label">Status</label>
|
{# <label for="status" class="form-label">Status</label>#}
|
||||||
<select class="form-select" id="status" name="status"
|
{# <select class="form-select" id="status" name="status"#}
|
||||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||||
hx-target="#inventory-list-container"
|
{# hx-target="#inventory-list-container"#}
|
||||||
hx-trigger="change"
|
{# hx-trigger="change"#}
|
||||||
hx-include="#filterForm">
|
{# hx-include="#filterForm">#}
|
||||||
<option value="">All Status</option>
|
{# <option value="">All Status</option>#}
|
||||||
<option value="in_stock" {% if request.GET.status == 'in_stock' %}selected{% endif %}>In Stock</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="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="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="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>
|
{# <option value="expiring_soon" {% if request.GET.status == 'expiring_soon' %}selected{% endif %}>Expiring Soon</option>#}
|
||||||
</select>
|
{# </select>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
|
{# #}
|
||||||
<div class="col-lg-2 col-md-6">
|
{# <div class="col-lg-2 col-md-6">#}
|
||||||
<label for="controlled" class="form-label">Controlled</label>
|
{# <label for="controlled" class="form-label">Controlled</label>#}
|
||||||
<select class="form-select" id="controlled" name="controlled"
|
{# <select class="form-select" id="controlled" name="controlled"#}
|
||||||
hx-get="{% url 'pharmacy:inventory_search' %}"
|
{# hx-get="{% url 'pharmacy:inventory_search' %}"#}
|
||||||
hx-target="#inventory-list-container"
|
{# hx-target="#inventory-list-container"#}
|
||||||
hx-trigger="change"
|
{# hx-trigger="change"#}
|
||||||
hx-include="#filterForm">
|
{# hx-include="#filterForm">#}
|
||||||
<option value="">All Items</option>
|
{# <option value="">All Items</option>#}
|
||||||
<option value="yes" {% if request.GET.controlled == 'yes' %}selected{% endif %}>Controlled Only</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>
|
{# <option value="no" {% if request.GET.controlled == 'no' %}selected{% endif %}>Non-Controlled Only</option>#}
|
||||||
</select>
|
{# </select>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
|
{# #}
|
||||||
<div class="col-lg-2 col-md-6">
|
{# <div class="col-lg-2 col-md-6">#}
|
||||||
<label class="form-label"> </label>
|
{# <label class="form-label"> </label>#}
|
||||||
<div class="d-grid gap-2">
|
{# <div class="d-grid gap-2">#}
|
||||||
<button type="submit" class="btn btn-primary">
|
{# <button type="submit" class="btn btn-primary">#}
|
||||||
<i class="fas fa-search me-2"></i>Filter
|
{# <i class="fas fa-search me-2"></i>Filter#}
|
||||||
</button>
|
{# </button>#}
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
|
{# <button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">#}
|
||||||
<i class="fas fa-times me-2"></i>Clear
|
{# <i class="fas fa-times me-2"></i>Clear#}
|
||||||
</button>
|
{# </button>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
</form>
|
{# </form>#}
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
function exportSeries(seriesId) {
|
||||||
// Export series functionality
|
|
||||||
if (confirm('Export this imaging series?')) {
|
if (confirm('Export this imaging series?')) {
|
||||||
window.location.href = `/radiology/series/${seriesId}/export/`;
|
window.location.href = `/radiology/series/${seriesId}/export/`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -331,7 +331,7 @@
|
|||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="fa fa-ellipsis-h"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<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
|
<i class="fa fa-edit me-2"></i>Edit
|
||||||
</a></li>
|
</a></li>
|
||||||
{% if study.dicom_files.exists %}
|
{% if study.dicom_files.exists %}
|
||||||
@ -340,9 +340,9 @@
|
|||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">
|
{# <li><a class="dropdown-item text-danger" href="{% url 'radiology:im' study.pk %}">#}
|
||||||
<i class="fa fa-trash me-2"></i>Delete
|
{# <i class="fa fa-trash me-2"></i>Delete#}
|
||||||
</a></li>
|
{# </a></li>#}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -428,7 +428,7 @@
|
|||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="fa fa-ellipsis-h"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<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
|
<i class="fa fa-edit me-2"></i>Edit
|
||||||
</a></li>
|
</a></li>
|
||||||
{% if study.dicom_files.exists %}
|
{% if study.dicom_files.exists %}
|
||||||
@ -437,9 +437,9 @@
|
|||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">
|
{# <li><a class="dropdown-item text-danger" href="{% url 'radiology:imaging_study_delete' study.pk %}">#}
|
||||||
<i class="fa fa-trash me-2"></i>Delete
|
{# <i class="fa fa-trash me-2"></i>Delete#}
|
||||||
</a></li>
|
{# </a></li>#}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -626,7 +626,7 @@ function bulkAction(action) {
|
|||||||
if (confirm(`Are you sure you want to ${actionText[action]} ${selectedStudies.length} selected studies?`)) {
|
if (confirm(`Are you sure you want to ${actionText[action]} ${selectedStudies.length} selected studies?`)) {
|
||||||
// Perform bulk action via AJAX
|
// Perform bulk action via AJAX
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '{% url "radiology:bulk_study_action" %}',
|
url: '',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
'action': action,
|
'action': action,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user